I've wrote this onTouchListener to hide/show actionBar on listview scroll, similar to current functionality in Instagram app. Everything works fine except listView will not fling. I know I've blocked that action somehow, but I'm still a novice and cant figure it out. Can anyone help me understand what's going on here?
list.setOnTouchListener(new OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
int newTextPosition = (int) (text.getY() * -1);
int move;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
startY = event.getY();
maxPath = text.getHeight();
break;
case MotionEvent.ACTION_UP:
v.performClick();
break;
case MotionEvent.ACTION_MOVE:
newY = event.getY();
move = (int) (newY - startY);
// Going UP
if(newY < startY){
if((text.getY() * -1) == text.getHeight()){
startY = event.getY();
return false;
}
int maxPath = (int) (text.getHeight() + text.getY());
if((move * -1) > maxPath){
move = maxPath * -1;
}
text.setY(text.getY() + move);
list.setY(list.getY() + move);
LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) list.getLayoutParams();
params.height = list.getHeight() + (move * -1);
list.setLayoutParams(params);
break;
}
// Going DOWN
if(newY > startY){
if((text.getY() * -1) == 0){
startY = event.getY();
return false;
}
if(text.getY() >= 0){
move = 0;
}
if(move > newTextPosition)
move = newTextPosition;
text.setY(text.getY() + move);
list.setY(list.getY() + move);
LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) list.getLayoutParams();
params.height = list.getHeight() + (move * -1);
list.setLayoutParams(params);
break;
}
}
return true;
}
});
EDIT: From some reason returning false at the end did the job. Any explanation is welcome
Captain here with an explanation!
When you return true from overriding onTouchEvent() on Views or from the OnTouchListeners onTouch() method, you tell android that this touch event has been 'consumed'. This way the touch event won't be passed back to the underlying views and will stop there. This is good when you need to override what happens on a touch event, but if you need the default action to happen you must return false, this way you tell android that the event wasn't 'consumed' and it must notify every other listener that a touch event has happened.
So by returning true, you had overridden the default behavior, which was the swipe and so it wasn't called on any touch event, because you 'consumed' all touch events.
Related
I'm using motion event, action_down, action_move etc, to detect when there is no finger movement but the finger is still on the screen. For instance, the user moves in a vertical line from top of the screen to the bottom and then stops movement without lifting the finger. How do I detect when there is no movement but the finger is still on the screen after the drag/swipe?
EDIT: What I'm trying to do is count every time I change direction of movement in a vertical direction. And to do that I'm trying to detect when I stop movement against the change of movement. For instance, I move down the screen and then move back up, that counts as two counts. Here is my code, please don't provide me with code as a direct answer but hints or clues so that I can try and figure it out myself (my code might look a bit confusing, I'm just trying out different things):
#Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
oldX = event.getX();
oldY = event.getY();
oldSpeedY = System.currentTimeMillis();
break;
case MotionEvent.ACTION_MOVE:
posY = event.getY();
posSpeedY = System.currentTimeMillis();
float timeElapsed = (posSpeedY - oldSpeedY) / 1000;
float diffSpeed = posSpeedY - oldSpeedY;
if (changeOfMovement(posY, oldY)) {
//if (diffSpeed == 0)
count += 1;
}
break;
case MotionEvent.ACTION_UP:
// count seconds here
break;
}
Toast.makeText(getApplicationContext(), "Swipe: " + count,
Toast.LENGTH_SHORT).show();
return false;
}
public boolean changeOfMovement(float posY, float oldY) {
int newY = Math.round(posY);
double distance = Math.abs(newY - oldY);
oldY = newY;
//float speed = (float) (distance / time);
//if (distance < 25)
//return false;
//if (speed == 0)
//return true;
if (distance < 25)
return true;
return false;
}
The finger touches the screen until you receive either of the MotionEvent actions ACTION_UP or ACTION_CANCEL
The ACTION_DOWN Event is still valid until the finger is lifted from the screen or you can wait till a ACTION_UP Event is detected
I'm not sure if my situation is like yours, but mine I was want to detect if the user stopped moving inside circle within one second and starts moving again, by comparing between last two currentTimeMillis.
So, what I've done is I initialized fixed ArrayList to save the last two times in move event:
public class FixedArrayList extends ArrayList {
int fixedSize = 10;
public FixedArrayList(){}
public FixedArrayList(int fixedSize){
this.fixedSize = fixedSize;
}
#SuppressWarnings("All")
public void addItem(Object object){
if(size() < fixedSize){
add(object);
} else{
remove(0);
add(object);
}
}
}
Now, I've initialized my new class with fixed 2 items to be saved:
FixedArrayList savedLastMove = new FixedArrayList(2);
int secondsToWait = 1;
public boolean onTouchEvent(MotionEvent event) {
int action = MotionEventCompat.getActionMasked(event);
switch (action) {
case (MotionEvent.ACTION_MOVE):
currentSystemTime = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis());
savedLastMove.addItem(currentSystemTime);
if(savedLastMove.size() >= 2 && ((long)savedLastMove.get(1) - (long)savedLastMove.get(0)) >= secondsToWait){
//Do what you want after secondsToWait
}
return true;
}
I hope that will help! Because it solved my problem.
Consider the following scheme below (for sake of better understanding of my problem).
As you can see, I am considering a list view surrounded by padding. Now, If a user presses a listview item, as the action I have provided it light blue background color. Now, My application is dealing with onTouch Events itself to determine actions like
Click
Left to Right Swipe
Right to Left Swipe
Here is my code.
public boolean onTouch(View v, MotionEvent event) {
if(v == null)
{
mSwipeDetected = Action.None;
return false;
}
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN: {
downX = event.getRawX();
downY = event.getRawY();
mSwipeDetected = Action.Start;
// Find the child view that was touched (perform a hit test)
Rect rect = new Rect();
int childCount = listView.getChildCount();
int[] listViewCoords = new int[2];
listView.getLocationOnScreen(listViewCoords);
int x = (int) event.getRawX() - listViewCoords[0];
int y = (int) event.getRawY() - listViewCoords[1];
View child;
for (int i = 0; i < childCount; i++) {
child = listView.getChildAt(i);
child.getHitRect(rect);
if (rect.contains(x, y)) {
mDownView = child;
break;
}
}
return false; // allow other events like Click to be processed
}
case MotionEvent.ACTION_MOVE: {
upX = event.getRawX();
upY = event.getRawY();
float deltaX=0,deltaY=0;
deltaX = downX - upX;
deltaY = downY - upY;
if(deltaY < VERTICAL_MIN_DISTANCE)
{
setTranslationX(mDownView, -(deltaX));
setAlpha(mDownView, Math.max(0f, Math.min(1f, 1f - 2f * Math.abs(deltaX) / listView.getWidth())));
return false;
}
else
{
forceBringBack(v);
}
return false;
}
case MotionEvent.ACTION_UP:
{
stopX = event.getX();
float stopValueY = event.getRawY() - downY;
float stopValue = stopX - downX;
if(!mDownView.isPressed())
{
forceBringBack(mDownView);
return false;
}
boolean dismiss = false;
boolean dismissRight = false;
if(Math.abs(stopValue)<10)
{
mSwipeDetected = Action.Start;
}
else
{
mSwipeDetected = Action.None;
}
String log = "";
Log.d(log, "Here is Y" + Math.abs(stopValueY));
Log.d(log, "First Comparison of Stop Value > with/4" + (Math.abs(stopValue) > (listView.getWidth() /4)));
Log.d(log, "Second Comparison " + (Math.abs(stopValueY)<VERTICAL_MIN_DISTANCE));
Log.d(log, "Action Detected is " + mSwipeDetected + " with Stop Value " + stopValue);
if((Math.abs(stopValue) > (listView.getWidth() /4))&&(Math.abs(stopValueY)<VERTICAL_MIN_DISTANCE))
{
dismiss = true;
dismissRight = stopValue > 0;
if(stopValue>0)
{
mSwipeDetected = Action.LR;
}
else
mSwipeDetected = Action.RL;
}
Log.d(log, "Action Detected is " + mSwipeDetected + " with Stop Value after dissmiss" + stopValue);
if(dismiss)
{
if(dismissRight)
mSwipeDetected = Action.LR;
else
mSwipeDetected = Action.RL;
animate(mDownView)
.translationX(dismissRight ? listView.getWidth() : - listView.getWidth())
.alpha(0)
.setDuration(mAnimationTime)
.setListener(new AnimatorListenerAdapter() {
public void onAnimationEnd(Animator animation)
{
}
});
}
else
{
animate(mDownView)
.translationX(0)
.alpha(1)
.setDuration(mAnimationTime)
.setListener(null);
}
break;
}
}
return false;
}
As you can see, I determine the performed action in MotionEvent.ACTION_UP and set the value of Enum Action accordingly. This logic works like a charm if the user does not crosses the list view boundary.
Now, if the user, while sliding (or specifically), moving his finger along the list item moves from blue to orange, the MotionEvent.ACTION_UP would not be given to listview, which causes my code not to make a decision and due to translationX() method and setAlpha(), since no Action is ever determined in this case, that particular list item gets blank.
The problem does not stops here, since, I am not inflating view each time, same translatedX() row gets inflated each time leading to multiple occurance of a blank/white list item.
Is there anything possible to do so that even if I didn't encounter MotionEvent.ACTION_UP, I could still make some decison ?
You should return true; in case MotionEvent.ACTION_DOWN:, so the MotionEvent.ACTION_UP will be handled.
As explained on View.OnTouchListener:
Returns:
True if the listener has consumed the event, false otherwise.
MotionEvent.ACTION_UP Won't get called until the MotionEvent.ACTION_DOWN occurred, a logical explanation for this is that it's impossible for an ACTION_UP to occur if an ACTION_DOWN never occurred before it.
This logic enables the developer to block further events after ACTION_DOWN.
Also note that under certain circumstances (eg. screen rotations) the gesture may be cancelled, in which case a MotionEvent.ACTION_UP will NOT be sent. Instead a MotionEvent.ACTION_CANCEL is sent instead. Therefore a normal action switch statement should look something like this:
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
// check if we want to handle touch events, return true
// else don't handle further touch events, return false
break;
// ... handle other cases
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
// finish handling touch events
// note that these methods won't be called if 'false' was returned
// from any previous events related to the gesture
break;
}
I don't think adding return true;to case MotionEvent.ACTION_DOWN: would eventually solve the problem. It just complicated the situation where return false could have done the job like a charm.
What to notice is: MotionEvent.ACTION_DOWN: /*something*/ return true; will block any other Listener callbacks available for the view, even onClickListenerm, while correctly return false in MotionEvent.ACTION_UP: can help the MotionEvent be propagated to the right destination.
Reference to his original code sourse: https://github.com/romannurik/android-swipetodismiss
As Danpe explained in his concise answer - I had to add the ACTION_DOWN code in order to have ACTION_UP recognized.
case MotionEvent.ACTION_DOWN:
return true;
case MotionEvent.ACTION_UP:
XyPos xyPos = new XyPos();
xyPos.x = last_x;
xyPos.y = last_y;
handleViewElementPositionUpdate(xyPos);
break;
I had the entire onTouch(..) method returning true anyway, so I'm not sure why that wasn't enough ... but nice to have this quick solution .. (thanks!)
So I have a custom view widget I am allowing a user to drag around by their finger. They are dragging it in a custom scrolling view based on a FrameLayout (code here: http://blog.gorges.us/2010/06/android-two-dimensional-scrollview/) that contains a RelativeLayout, so that they can place it where they want and have plenty of space that is not visible on screen to place the item as well.
What I would like to happen is if the user is moving the widget near the edge of the visible scroll area is to scroll the area in the appropriate direction until they either let go (ACTION_UP, in which case no more events would take place anyway) or have moved away from the edge. The code right now has two problems:
1) My phone (T-Mobile G2) continues to trigger ACTION_MOVE even when my finger is still where as my Nexus 7 tablet does not continue to trigger until it has moved. I need to reliably continue to trigger this event until ACTION_UP.
2) The area scrolls but only as I move my finger towards the edge of the screen. In otherwords, I'd like for a finger that is staying still to continue to scroll and move the object when it is near the edge, but this is not happening.
What am I missing? Here is the onTouchEvent that handles the moving of the widget
#Override
public boolean onTouchEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_MOVE
&& this.getParent() != null) {
getParent().requestDisallowInterceptTouchEvent(true);
}
// mScrollParent gets set in a setter and points to the TwoDScrollView
// mOffsetX/Y is based on where the user touches versus the position of the custom view
// mWidth/mHeight gets set once in the GestureDetector based on the custom view's getWidth()/getHeight()
// mTitleBarHeight gets set once in the GestureDetctor onDown event
// lp is a ViewGroup.LayoutParams type
if (!mGestureDetector.onTouchEvent(event)) {
if (event.getAction() == MotionEvent.ACTION_MOVE) {
int finalX = (int) (event.getRawX() - mOffsetX);
int finalY = (int) (event.getRawY() - mOffsetY - (mTitleBarHeight > 0 ? mTitleBarHeight * 2
: 0));
if (finalX < 0 || finalY < 0)
return false;
int scrollLeft = (int) Math.ceil(mScrollParent.getScrollX() * 1.05);
int scrollTop = (int) Math.ceil(mScrollParent.getScrollY() * 1.05);
int scrollRight = (int) Math.floor((scrollLeft + mScrollParent.getRight()) * 0.95);
int scrollBottom = (int) Math.floor((scrollTop + mScrollParent.getBottom()) * 0.95);
int scrollX = 0;
int scrollY = 0;
if (scrollLeft > finalX)
scrollX = finalX - scrollLeft;
if (scrollRight < (finalX + mWidth))
scrollX = (finalX + mWidth)- scrollRight;
if (scrollTop > finalY)
scrollY = finalY - scrollTop;
if (scrollBottom < (finalY + mHeight))
scrollY = (finalY + mHeight) - scrollBottom;
if (scrollX != 0 || scrollY != 0)
mScrollParent.scrollBy(scrollX * 4, scrollY * 4);
lp.setMargins(finalX + scrollX, finalY + scrollY, 0, 0);
setLayoutParams(lp);
return true;
}
} else if (event.getAction() == MotionEvent.ACTION_UP) {
return true;
} else {
return super.onTouchEvent(event);
}
} else {
return true;
}
}
I am trying to drag and drop a crosshair onto a MapView. I have an ImageView button on the screen. When I touch it, the button disappears and a new ImageView appears. The new ImageView is supposed to sit centered under my finger until I release it somewhere, but for some reason the offset is incorrect. The crosshair appears down-right of where I am touching.
The image does move proportionately so I believe the problem is with the offset_x and offset_y which I define in the ACTION_DOWN section. Then, in ACTION_UP I need to createMarker(x,y) on the correct coordinates under my finger, but that is offset incorrectly as well.
I have tried various ways to make it centered, and some are better than others. I have so far been unable to make it work without using magic numbers.
As it is, I am using the click location and subtracting half the picture size. This makes sense to me. It's closer to correct if I subtract the whole picture size. I have tried various examples from the web, all of them suffer from inaccuracies with the View location.
Can you give me some advice?
crosshair = (ImageView)findViewById(R.id.crosshair);
frameLayout = (FrameLayout)findViewById(R.id.mapframe);
params = new LayoutParams(LayoutParams.WRAP_CONTENT,
LayoutParams.WRAP_CONTENT);
crosshairImage = BitmapFactory.decodeResource(getResources(), R.drawable.crosshair);
crosshair.setOnTouchListener(new OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
dragStatus = START_DRAGGING;
// hide the button and grab a mobile crosshair
v.setVisibility(View.INVISIBLE);
image = new ImageView(getBaseContext());
image.setImageBitmap(crosshairImage);
// set the image offsets to center the crosshair under my touch
offset_x = (int)event.getRawX() - (int)(crosshairImage.getWidth()/2) ;
offset_y = (int)event.getRawY() - (int)(crosshairImage.getHeight()/2) ;
// set the image location on the screen using padding
image.setPadding(offset_x, offset_y, 0, 0);
// add it to the screen so it shows up
frameLayout.addView(image, params);
frameLayout.invalidate();
if(LOG_V) Log.v(TAG, "Pressed Crosshair");
return true;
} else if (event.getAction() == MotionEvent.ACTION_UP) {
if(dragStatus == START_DRAGGING) {
dragStatus = STOP_DRAGGING;
// place a marker on this spot
makeMarker((int)event.getX() + offset_x, (int)event.getY() + offset_y);
// make the button visible again
v.setVisibility(View.VISIBLE);
// remove the mobile crosshair
frameLayout.removeView(image);
if(LOG_V) Log.v(TAG, "Dropped Crosshair");
return true;
}
} else if (event.getAction() == MotionEvent.ACTION_MOVE) {
if (dragStatus == START_DRAGGING) {
// compute how far it has moved from 'start' offset
int x = (int)event.getX() + offset_x;
int y = (int)event.getY() + offset_y;
// check that it's in bounds
int w = getWindowManager().getDefaultDisplay().getWidth();
int h = getWindowManager().getDefaultDisplay().getHeight();
if(x > w)
x = w;
if(y > h)
y = h;
// set the padding to change the image loc
image.setPadding(x, y, 0 , 0);
// draw it
image.invalidate();
frameLayout.requestLayout();
if(LOG_V) Log.v(TAG, "(" + offset_x + ", " + offset_y + ")");
return true;
}
}
return false;
}
});
You have written this code within touch_Down.
offset_x = (int)event.getRawX() - (int)(crosshairImage.getWidth()/2) ;
offset_y = (int)event.getRawY() - (int)(crosshairImage.getHeight()/2) ;
// set the image location on the screen using padding
image.setPadding(offset_x, offset_y, 0, 0);
Try writing it outside the if conditions, just to make sure, it is applied to all the touch events.
You can use new api available in drag and drop
public boolean onTouch(View view, MotionEvent motionEvent) {
if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) {
DragShadowBuilder shadowBuilder = new View.DragShadowBuilder(view);
view.startDrag(null, shadowBuilder, view, 0);
view.setVisibility(View.INVISIBLE);
return true;
} else {
return false;
}
}
Greets,
Does anyone how I go about detecting when a user presses on a bitmap which is inside a canvas?
Thanks
You should work with something like so:
public boolean onTouchEvent(MotionEvent event){
int action = event.getAction();
int x = event.getX() // or getRawX();
int y = event.getY();
switch(action){
case MotionEvent.ACTION_DOWN:
if (x >= xOfYourBitmap && x < (xOfYourBitmap + yourBitmap.getWidth())
&& y >= yOfYourBitmap && y < (yOfYourBitmap + yourBitmap.getHeight())) {
//tada, if this is true, you've started your click inside your bitmap
}
break;
}
}
That's a start, but you need to handle case MotionEvent.ACTION_MOVE and case MotionEvent.ACTION_UP to make sure you properly deal with the user input. The onTouchEvent method gets called every time the user: puts a finger down, moves a finger already on the screen or lifts a finger. Each time the event carries the X and Y coordinates of where the finger is. For example if you want to check for someone tapping inside your bitmap, you should do something like set a boolean that the touch started inside the bitmap on ACTION_DOWN, and then check on ACTION_UP that you're still inside the bitmap.
Steve,
Google has a great article and tutorial for handling UI Events # http://developer.android.com/guide/topics/ui/ui-events.html. This is what got me started and it was great for me :-)
This will detect a touch and check if it is not transparent. You need this if your bitmaps are not rectangles. myBitmap is just a simple container class I use.
private boolean clickOnBitmap(MyBitmap myBitmap, MotionEvent event) {
float xEnd = myBitmap.originX() + myBitmap.width();
float yEnd = myBitmap.originY() + myBitmap.height();;
if ((event.getX() >= myBitmap.originX() && event.getX() <= xEnd)
&& (event.getY() >= myBitmap.originY() && event.getY() <= yEnd) ) {
int pixX = (int) (event.getX() - myBitmap.originX());
int pixY = (int) (event.getY() - myBitmap.originY());
if (!(myBitmap.getBitmap().getPixel(pixX, pixY) == 0)) {
return true;
} else {
return false;
}
}
return false;
}
This is the on touch code for completeness
#Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
if (clickOnBitmap(dog, event)) {
Toast.makeText(getContext(), "dog", Toast.LENGTH_SHORT).show();
} else if (clickOnBitmap(mouse,event)) {
Toast.makeText(getContext(), "mouse", Toast.LENGTH_SHORT).show();
}
return true;
case MotionEvent.ACTION_OUTSIDE:
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
return true;
}
return false;
}