In trying to tackle custom views, I'm attempting to work with touch events and partial invalidation. For this, it's just a row of numbers in squares spaced to fill the screen.
Now when I press on a single block, I get the block's rectangle using this:
private Rect getDirtyRegion(float e){
// The value is the slot number
mValue = ((int)e / mBlockSize);
// start X of the "stall"
int x1 = mValue * mBlockSize;
int y1 = 0;
int x2 = x1 + mBlockSize;
int y2 = getMeasuredHeight();
return new Rect(x1, y1, x2, y2);
}
It works as expected. When there's just a few on screen it works great. Here's my onTouchEvent:
#Override
public boolean onTouchEvent(MotionEvent e){
switch(e.getAction()){
case MotionEvent.ACTION_DOWN:
Log.d(TAG, "ActionDown");
setPaint(PinEntry.PAINT_PRESSED);
invalidate(getDirtyRegion(e.getX()));
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
setPaint(PinEntry.PAINT_NORMAL);
invalidate();
break;
}
return true;
}
(This has been rewritten quite a few times, so the call to invalidate without a rectangle hasn't always been the case.)
What I'm after is, when I tap on a number, it redraws to indicate a PRESSED state by whatever I do in setPaint. When I release, reset.
When I have multiple views in a ScrollView, however it breaks. When I press and release, or even drag outside the bound (triggering ACTION_CANCEL), it resets. However, going back to that row causes the whole thing to invalidate as "PRESSED".
Is this a TouchEvent logic issue, a drawing issue, or some combination of my inexperience with creating custom views?
I ended up splitting it into two different classes, one for the container (parent), and another for each individual block and using the draw(Canvas) method of the View class.
Related
We have an Android app that is meant to be mounted on the dash of a vehicle that is operating in rough terrain, so everything is really shaky. We've found that making a single tap on the screen in this kind of situation is difficult, as the taps are often interpreted as small drags.
What I need is for touch events that have a little wiggle before the finger comes up to be interpreted as clicks, not drags. I've been doing some reading into the way the 'touch slop' works in Android and I can see that they already account for this. All I really need to understand is how to increase the 'touch slop' for a subclass of an Android button widget.
Is this possible in just a couple of lines of code? Or do I need to do my own implementations of onInterceptTouchEvent and 'onTouchEvent`? If the latter, can anyone give me some direction on how this would work?
Here is what I did, hope that help you guys.
private Rect mBtnRect;
yourView.setOnTouchListener(new OnTouchListener() {
private boolean isCancelled = false;
#Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
isCancelled = false;
parent.requestDisallowInterceptTouchEvent(true); // prevent parent and its ancestors to intercept touch events
createClickArea(v);
// your logic touch down
return true;
case MotionEvent.ACTION_UP:
if(!isCancelled) {
// Click logic
}
// release mBtnRect when cancel or up event
mBtnRect = null;
return true;
case MotionEvent.ACTION_CANCEL:
isCancelled = true;
releaseTouch(parent, v);
return true;
case MotionEvent.ACTION_MOVE:
if(!isBelongTouchArea(event.getRawX(), event.getRawY())) {
isCancelled = true;
releaseTouch(parent, v);
}
return true;
default:
break;
}
}
// Create the area from the view that user is touching
private final void createClickArea(View v) {
// for increase rect area of button, pixel in used.
final int delta = (int) mContext.getResources().getDimension(R.dimen.extension_area);
final int[] location = new int[2];
// Get the location of button call on screen
v.getLocationOnScreen(location);
// Create the rect area with an extension defined distance.
mBtnRect = new Rect(v.getLeft() - delta, location[1] + v.getTop() - delta, v.getRight(), location[1] + v.getBottom() + delta);
}
//Check the area that contains the moved position or not.
private final boolean isBelongTouchArea(float rawX, float rawY) {
if(mBtnRect != null && mBtnRect.contains((int)rawX, (int)rawY)) {
return true;
}
return false;
}
private void releaseTouch(final ListView parent, View v) {
parent.requestDisallowInterceptTouchEvent(false);
mBtnRect = null;
// your logic
}
For our simple use-case with a android.opengl.GLSurfaceView, we solved the same problem you have by saying, "if the distance moved in the gesture is less than some_threshold, interpret it as a click". We used standard Euclidean distance between two 2D points = sqrt((deltaX)^2 + (deltaY)^2)), where deltaX and deltaY are the X and Y components of the distance moved in the user's motion gesture.
More precisely, let (x1,y1) be coordinates where user's finger gesture 'started' and let (x2,y2) be coordinates where the gesture 'ended'.
Then, deltaX = x2 - x1 (or it can also be x1 - x2 but sign doesn't matter for the distance 'cz we square the value) and similarly for deltaY.
From these deltas, we calculate the Euclidean distance and if it's smaller than a threshold, the user probably intended it to be a click but because of the device's touch slop, it got classified as a move gesture instead of click.
Implementation-wise, yes we overrode android.view.View#onTouchEvent(MotionEvent) and
recorded (x1,y1) when masked action was android.view.MotionEvent#ACTION_POINTER_DOWN or android.view.MotionEvent#ACTION_DOWN
and when masked action was android.view.MotionEvent#ACTION_MOVE, (x2,y2) are readily available from the event's getX and getY methods (see corresponding documentation)
I created a class that should be used to allow user to horizontally relocate a view by sliding his finger on a bar, the code is this
public class Dragger implements View.OnTouchListener {
private View toDrag;
private Point startDragPoint;
private int offset;
public Dragger (View bar, View toDragg){
toDrag = toDragg;
dragging = false;
bar.setOnTouchListener(this);
}
public boolean onTouch(View v, MotionEvent evt) {
switch (evt.getAction()) {
case MotionEvent.ACTION_DOWN:
startDragPoint = new Point((int) evt.getX(), (int) evt.getY());
offset = startDragPoint.x - (int) toDrag.getX();
break;
case MotionEvent.ACTION_MOVE:
int currentX = (int)evt.getX();
Log.d("LOG","currentX=" + currentX);
toDrag.setX(currentX-offset);
break;
case MotionEvent.ACTION_UP:
int difference = (startDragPoint.x-offset)-(int)toDrag.getX();
if(difference<-125){
((ViewManager) toDrag.getParent()).removeView(toDrag);
}else{
toDrag.setX(startDragPoint.x-offset);
}
dragging = false;
}
return true;
}
}
In the oncreate of the main activity i just use it by calling his constructor like this:
new Dragger(barItem,itemToDrag);
It apparently work, but it seems to tremble a lot while dragging... I tried to understand why does it work so strangely, so i added a Log instruction in the ACTION_MOVE, and the result is that... even if i only move to right pixel by pixel sometimes the currentX instead of increasing by 1 it decrease of a variable number, sometimes 10, sometimes 18 and when i go to the next pixel it turn back to normal by increasing of 1...
That really doesen't make sense...
Some ideas on how to fix it?
First guess: There are other fingers on the device which causes move to work erratically
Second guess: Right now you seem to be using the setX method more like a moveX and specifying the increment by which the function moves. This might not be what it was designed for
Third Point:
Sometimes the move function acts finicky. It is always run (as in whenever there is a pointer on the screen) and depends on the position of the finger. I would recommend changing the function to make it like a step
Suggestion
Make the action move like a step function that takes one value
case MotionEvent.ACTION_MOVE:
int currentX = (int)evt.getX();
if(abs(currentX)>10)currentX=(int) currentX/5;
toDrag.setX(currentX-offset);
UPDATE: please read entire question again :-)
Background:
I have created a grid of dots by creating a custom view and then adding these views to a TableLayout. The aim is for me to be able to draw a line from one of these dots to another dot in a way so that when a dot is pressed, a line is started from the center of the pressed dot to the point where the finger is currently touching. When the finger is dragged over another dot, the line then finishes at the center of that dot. The result being a line drawn from the center of the pressed dot to the center of the dot which was dragged over by the finger.
To create a line going over the TableLayout, I created a new custom view which just created a line between points with the canvas.drawLine() method. I then created a FrameLayout in which I put the TableLayout and the LineView. This meant the line view would be able to be drawn on top of the TableLayout.
Current Situation:
I have added touch listener in the DotView class so that it can have its own “touch feedback”. There is also another touch listener in the activity which I am trying to use to get the points needed to draw the line. The touch listeners are working fine as far as I can tell - they are not interfering with each other.
The problem is getting the correct coordinates to plot the line. I have tried a number of ways to get the coordinates of the center of the dot that was pressed (the starting point of the line), but I still have not managed it. This question, has more information: Get the coordinates of the center of a view . The second part is getting the correct end point of the line. For the purposes of this question, I would just like the end of the line to follow the position of the finger. I thought it was as easy as motionevent.getX() / getY() but this hasn’t worked. What happened instead was that there was a mix up between the coordinates on a scale relative to the layout of the dot and the coordinates relative to the whole layout/screen.
Simply put: the getX() and getY() values are incorrect, and this is what I am trying to solve here.
As shown in the screenshots, when I press down on a dot, the start point of the line is roughly in the right place, but the end point is way off. I can’t really explain why.
I have tried getRawX() and getRawY() and they return more much more accurate values, but they are still incorrect by the amount of padding (or something like that - I don’t 100% understand).
This shows my code
In my Activity :
LineView test;
FrameLayout fl;
float startPointX = 0;
float startPointY = 0;
// Removed
#Override
public boolean onTouch(View view, MotionEvent event) {
float eventX = event.getX();
float eventY = event.getY();
int[] loc = new int[2];
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
if (view instanceof DotView) {
DotView touchedDv = (DotView) view;
int dotColor = touchedDv.getColor();
test.setColor(dotColor);
float[] f = touchedDv.getDotCenterLocationOnScreen();
startPointX = f[0];
startPointY = f[1];
test.setPoints(startPointX, startPointY, eventX, eventY);
fl.addView(test);
}
vib.vibrate(35);
return false;
case MotionEvent.ACTION_MOVE:
test.setPoints(startPointX, startPointY, eventX, eventY);
break;
case MotionEvent.ACTION_UP:
fl.removeView(test);
return false;
default:
return false;
}
return true;
}
And finally the LineView:
public class LineView extends View {
public static final int LINE_WIDTH = 10;
Paint paint = new Paint();
float startingX, startingY, endingX, endingY;
public LineView(Context context) {
super(context);
paint.setColor(Color.MAGENTA);
paint.setStrokeWidth(LINE_WIDTH);
}
public void setPoints(float startX, float startY, float endX, float endY) {
startingX = startX;
startingY = startY;
endingX = endX;
endingY = endY;
invalidate();
}
#Override
public void onDraw(Canvas canvas) {
canvas.drawLine(startingX, startingY, endingX, endingY, paint);
}
NOTES:
I am calling the top left hand dot "dot 1".
The screenshots may not be entirely accurate as my finger moves
slightly when I take the screenshot, but the behaviour I described is
happening.
If any other information/code is wanted, I will happily provide it.
Thanks for taking the time to read this - sorry it is so long!
I have added touch listener in the DotView class so that it can have
its own “touch feedback”. There is also another touch listener in the
activity which I am trying to use to get the points needed to draw the
line. The touch listeners are working fine as far as I can tell - they
are not interfering with each other.
I don't quite see why you need both touch listener. To draw the line the touch listener on the TableLayout should be more than enough.
The problem with your code(or at least from what I've seen) is that you use the getLocationOnScreen(coordsArray) method without translating the returned values back to the coordinates system of the LineView. For example, you get the coordinates of a DotView which will be x and y. You then use this values in the LineView. But, the LineView when it will do its drawings will use its own(standard) coordinates system which places the top-left of the view at (0,0) and the coordinates will not match. Here's an example: suppose you touch the very first DotView which has a height of 50 and the y returned by the getLocationOnScreen() method is 100. You calculate the center of the DotView wich will come be at 125(100 + 50 / 2). Using this value in the LineView will be way off the normal position as the actual drawing will be done at 125, which in screen coordinates will visually translate to a y of 225 (100 returned by getLocationOnScreen() + 125).
Anyway I've made a small example using the getLocationOnScreen() method to do what you're trying to do(which you can find here).
I was wondering how to get accurate, live get(x) and get(y) values for a MotionEvent? What is happening is that when I touch a specific area on the screen, I tell an action to happen.
The problem is that once I touch the screen and take my finger off, it still thinks my finger is at the same location (since that was the last location I touched). So when I have more than one Down event (for multitouch) it throws everything off. Is there a way to reset the X and Y values so when I let off the screen, they go back to 0 or null (or whatever)? Thanks
What you are describing isn't a problem. You the programmer are responsible for keeping track of the touch locations and what they mean. If you care about motion you need to keep track of the previous touch and the current touch each time a touch occurs. I find something like this works great:
public int x=-1,y=-1,prevX=-1, prevY=-1;
public boolean onTouch(View v, MotionEvent event)
{
prevX = x;
prevY = y;
int x = (int)event.getX();
int y = (int)event.getY();
switch(event.getAction()){
case MotionEvent.ACTION_DOWN:
// there is no prev touch
prevX = -1;
prevY = -1;
// touch down code
break;
case MotionEvent.ACTION_MOVE:
// touch move code
break;
case MotionEvent.ACTION_UP:
// touch up code
break;
}
return true;
}
Just catch the MotionEvent.ACTION_UP or ACTION_MOVE event, depending on when the action needs to happen (since you want it live, you should use the ACTION_MOVE event). You should handle setting external variables when ACTION_DOWN occurs, and handle the results on ACTION_MOVE or ACTION_UP.
Desired effect
I have a bunch of small images that I'd like to show on a "wall" and then let the user fling this wall in any direction and select an image.
Initial Idea
as a possible implementation I was thinking a GridView that is larger than the screen can show - but all examples of using this widget indicate that the Gallery doesn't extend beyond the size of the screen.
Question
What is the best widget to use to implement the desired effect ?
A code sample would be especially beneficial.
EDIT...
if someone has example code that will let me put about 30 images on a "wall" (table would be good) then I will accept that as the answer. Note that the "wall" should look like it extends beyond the edges of the display and allow a user to use the finger to drag the "wall" up down left right. Dragging should be in "free-form" mode. A single tap on an image selects it and a callback shall be detectable. I have created a bounty for this solution.
The solution is actually quite simple, but a pain. I had to implement exactly this within a program I recently did. There are a few tricksy things you have to get around though. It must be noted that different OS versions have different experiences. Certain expertise or cludging around is required to make this work as drawing is sometimes adversely affected by this method. While I have a working copy, the code I provide is not for everyone, and is subject to whatever other changes or customizations have been made in your code.
set android:layout_width to wrap_content
set android:layout_height to wrap_content
In your code:
determine how many rows you will have.
divide number of items by the number of rows.
add gridView.setStretchMode(NO_STRETCH);
add gridView.setNumColumns( number of Columns );
add gridView.setColumnWidth( an explicit width in pixels );
To Scroll:
You may simply use gridView.scrollBy() in your onTouchEvent()
These are the steps. All are required in order to get it to work. The key is the NO_STRETCH with an explicit number of columns at a specific width. Further details can be provided, if you need clarification. You may even use a VelocityTracker or onFling to handle flinging. I have snappingToPage enabled in mine. Using this solution, there is not even a requirement to override onDraw() or onLayout(). Those three lines of code get rid of the need for a WebView or any other embedding or nesting.
Bi-directional scrolling is also quite easy to implement. Here is a simple code solution:
First, in your class begin tracking X and Y positions by making two class members. Also add State tracking;
// Holds the last position of the touch event (including movement)
int myLastX;
int myLastY;
// Tracks the state of the Touch handling
final static private int TOUCH_STATE_REST = 0;
final static private int TOUCH_STATE_SCROLLING = 1;
int myState = TOUCH_STATE_REST;
Second, make sure to check for Scrolling, that way you can still click or longclick the images themselves.
#Override public boolean onInterceptTouchEvent(final MotionEvent ev)
{//User is already scrolling something. If we haven't interrupted this already,
// then something else is handling its own scrolling and we should let this be.
// Once we return TRUE, this event no longer fires and instead all goes to
// onTouch() until the next TouchEvent begins (often beginning with ACTION_DOWN).
if ((ev.getAction() == MotionEvent.ACTION_MOVE)
&& (myState != TOUCH_STATE_REST))
return false;
// Grab the X and Y positions of the MotionEvent
final float _x = ev.getX();
final float _y = ev.getY();
switch (ev.getAction())
{ case MotionEvent.ACTION_MOVE:
final int _diffX = (int) Math.abs(_x - myLastX);
final int _diffY = (int) Math.abs(_y - myLastY);
final boolean xMoved = _diffX > 0;
final boolean yMoved = _diffY > 0;
if (xMoved || yMoved)
myState = TOUCH_STATE_SCROLLING;
break;
case MotionEvent.ACTION_DOWN:
// Remember location of down touch
myLastX = _x;
myLastY = _y;
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
if (myState == TOUCH_STATE_SCROLLING)
// Release the drag
myState = TOUCH_STATE_REST;
}
//If we are not At Rest, start handling in our own onTouch()
return myState != TOUCH_STATE_REST;
}
After the GridView knows that you are Scrolling, it will send all Touch Events to onTouch. Do your Scrolling here.
#Override public boolean onTouchEvent(final MotionEvent ev)
{
final int action = ev.getAction();
final float x = ev.getX();
final float y = ev.getY();
final View child;
switch (action)
{
case MotionEvent.ACTION_DOWN:
//Supplemental code, if needed
break;
case MotionEvent.ACTION_MOVE:
// This handles Scrolling only.
if (myState == TOUCH_STATE_SCROLLING)
{
// Scroll to follow the motion event
// This will update the vars as long as your finger is down.
final int deltaX = (int) (myLastX - x);
final int deltaY = (int) (myLastY - y);
myLastX = x;
myLastY = y;
scrollBy(deltaX, deltaY);
}
break;
case MotionEvent.ACTION_UP:
// If Scrolling, stop the scroll so we can scroll later.
if (myState == TOUCH_STATE_SCROLLING)
myState = TOUCH_STATE_REST;
break
case MotionEvent.ACTION_CANCEL:
// This is just a failsafe. I don't even know how to cancel a touch event
myState = TOUCH_STATE_REST;
}
return true;
}
If course, this solution moves at X and Y at the same time. If you want to move just one direction at a time, you can differentiate easily by checking the greater of the X and Y differences. (i.e. Math.abs(deltaX) > Math.abs(deltaY)) Below is a partial sample for one directional scrolling, but can switch between X or Y direction.
3b. Change this in your OnTouch() if you want to handle one direction at a time:
case MotionEvent.ACTION_MOVE:
if (myState == TOUCH_STATE_SCROLLING)
{
//This will update the vars as long as your finger is down.
final int deltaX = (int) (myLastX - x);
final int deltaY = (int) (myLastY - y);
myLastX = x;
myLastY = y;
// Check which direction is the bigger of the two
if (Math.abs(deltaX) > Math.abs(deltaY))
scrollBy(deltaX, 0);
else if (Math.abs(deltaY) > Math.abs(deltaX))
scrollBy(0, deltaY);
// We do nothing if they are equal.
}
break;
FuzzicalLogic
A GridView can have content that extends beyond the size of the screen, but only in the vertical direction.
Question: What is the best widget to use to implement the desired effect ?
The are no default widgets (at least prior to 3.0) which implement 2D scrolling. All of them implement 1D (scrolling in one direction) only. Sadly the solution to your problem is not that easy.
You could:
Place a TableLayout inside a custom ScrollView class, and handle the touch events yourself. This would allow you to do proper 2D panning, but would not be so good for large data-sets since there would be no view recycling
Modify GridView if possible to enable horizontal scrolling functionality (probably the best solution if you can do it)
Try putting a TableLayout inside a ScrollView (with layout_width="wrap_content) which you put inside a HorizontalScrollView - though this may work it wouldn't be the optimum solution
Do something with OpenGL and draw straight to a canvas - this is how the default Gallery 3D app does it's "wall of pictures"
I think that the only Android native solution for this is to have an embebed WebView in which you can have that bi-dimensional scroll (and by the way is the only thing that has this capability).
Be pressing an image you can control the behavior by a callback from JavaScript to JAva.
This is not very "pretty" but... it's the only viable way to do what you want "out of the box".
I have implemented a GridView that overrides the onInterceptTouchEvent methods. I set the OnTouchListeneras explained by Fuzzical Logic. My issue is that setting a custom OnTouchListener, the objects of my gridView are not clickable anymore. Is that because the default OnTouchListener of a GridView calls onItemClickListener ?
I would like to have a 2 directions scrollable GridView but that has still cliackable objects. Should I implement my own method to check whether a touch matches a item or is there a easier way to achieve this ?
Grodak