80

Consider the following scheme below (for sake of better understanding of my problem). enter image description here

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 ?

A-Sharabiani
  • 17,750
  • 17
  • 113
  • 128
Gaurav
  • 3,614
  • 3
  • 30
  • 51
  • see if this solves your question: http://stackoverflow.com/questions/13283827/onintercepttouchevent-only-gets-action-down – user123321 Apr 03 '13 at 23:15

4 Answers4

249

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.

Community
  • 1
  • 1
Danpe
  • 18,668
  • 21
  • 96
  • 131
  • 7
    I've always been confused as to why this is true. Is there an explanation of the logic anywhere? – Tony Chan May 03 '14 at 00:38
  • 3
    @Turbo Added a logical explenation, Still I think Android should explain it better or fix the behavior to be more logical. – Danpe May 03 '14 at 08:26
  • 5
    I thought `consumed` means the event won't be passed on to the UI layer beneath of the consumer. I may still want to be aware of the touches but still pass them on. Will returning true prevent from the lower to layered components from getting the touch event? – AlikElzin-kilaka Dec 23 '15 at 08:33
  • @AlikElzin-kilaka I also thought the same.. but returning true will still pass it to the "lower layered components" – Danpe Dec 23 '15 at 17:12
  • 1
    I added a click listener and still return false in `onTouch`. This makes the touch listener be aware of all the events and still not consume them. – AlikElzin-kilaka Dec 23 '15 at 17:58
  • @Danpe in my case I return false in onInterceptTouchEvent otherwise the child view which happens to be listview wont scroll, I get down and move events but not up even, what could be the reason? – Jack Nov 10 '18 at 23:51
  • In my case I had to return `true`, because Lint warns "onTouch lambda should call View#performClick when a click is detected". So, I had to add `v.performClick()`, and after one click 3 or more events arised (ACTION_DOWN x 2, ACTION_MOVE, ACTION_UP). When I changed `false` to `true` no unneeded clicks were added (that appeared because of `performClick()`). – CoolMind Jul 07 '20 at 12:25
34

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;
}
TheIT
  • 11,919
  • 4
  • 64
  • 56
  • 1
    Not sure why but for some reason I had to add cancel to catch the up. – Rarw Jun 16 '16 at 02:52
  • 1
    This one fixed my issue. After scrolling the recycler view horizontally, the MotionEvent.ACTION_UP was never called. Instead MotionEvent.ACTION_CANCEL was being fired – Juan Manuel Amoros May 04 '18 at 19:18
  • Had to break my head for a whole day! FYI I was also playing with "requestDisallowInterceptTouchEvent" for a different reason.This was the missing piece I was looking for. – Jaswanth Manigundan Jul 03 '18 at 06:10
8

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

Chrysanthemumer
  • 401
  • 4
  • 4
  • I also launched a a Roman Nurik project, and it worked. When I tried to apply the code to my views, it didn't work. When I changed `return false;` to `return true;` it helped. – CoolMind Nov 29 '16 at 19:18
0

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!)

fawaad
  • 341
  • 6
  • 12
Gene Bo
  • 11,284
  • 8
  • 90
  • 137