28

I has the following class that represents a View that is touchable and draw a Slide Bar.

public class SlideBar extends View {
private int progress;
private int max;

private Paint background;
private Paint upground;

private RectF bar;

private boolean firstDraw;

public SlideBar(Context context, AttributeSet attrs) {
    super(context, attrs);
    progress = 0;

    upground = new Paint();
    upground.setColor(Color.parseColor("#C2296C"));

    background = new Paint();
    background.setColor(Color.parseColor("#777777"));
}

private void onFirstDraw() {
    max = getWidth();
    bar = new RectF(0, 19, max, 21);
}

public void onDraw(Canvas canvas) {
    if (!firstDraw) {
        onFirstDraw();
        progress = max;
        firstDraw = true;
    }

    canvas.save();
    canvas.drawRoundRect(bar, 5, 5, background);
    canvas.drawCircle(progress, 20, 9, upground);
    canvas.restore();
}

public void setValue(int value) {
    progress = value;
}

public boolean onTouchEvent(MotionEvent evt) {
    System.out.println(evt.getAction());
    progress = (int) evt.getX();
    invalidate();
    return false;
}
}

But when touching and dragging it, I receive a ACTION_DOWN, some ACTION_MOVEs then receive a ACTION_CANCEL and no further events.

Why it's happens? I don't want to cancel the event and enable it to keep dragging bar.

Marcos Vasconcelos
  • 18,136
  • 30
  • 106
  • 167

4 Answers4

61

This will happen when parent container will intercept your touch event. Any ViewGroup that overrides ViewGroup.onInterceptTouchEvent(MotionEvent) can do that (ScrollView or ListView for instance).

Proper way to deal with this is to call ViewParent.requestDisallowInterceptTouchEvent(boolean) method on your parent view once you think you need to keep the motion event.

Here's a quick example (attemptClaimDrag method is taken from android source code):

/**
 * Tries to claim the user's drag motion, and requests disallowing any
 * ancestors from stealing events in the drag.
 */
private void attemptClaimDrag() {
    //mParent = getParent();
    if (mParent != null) {
        mParent.requestDisallowInterceptTouchEvent(true);
    }
}

@Override
public boolean onTouchEvent(MotionEvent event) {
    if (event.getAction() == MotionEvent.ACTION_DOWN) {
        if (iWantToKeepThisEventForMyself(event)) {
            attemptClaimDrag();
        }
        //your logic here
    } else {
        //your logic here
    }
}
Alexey
  • 7,262
  • 4
  • 48
  • 68
  • 3
    This worked for me when I had a ViewPager with swipeable content. The content wasn't receiving horizontal swipes because they were being intercepted by the ViewPager. I simply passed in a reference to the ViewPager into the child view, and the child now calls mParent.requestDisallowInterceptTouchEvent(true) for all ACTION_DOWN events. – OldSchool4664 Oct 19 '12 at 00:05
  • If you are using ViewPager, you should probably use ViewPager#canScroll(android.view.View, boolean, int, int, int) instead. This protected method of ViewPager is made specificly to determine whenever ViewPager should scroll content of a page or switch to a next page. – Alexey Nov 26 '12 at 13:15
  • how can i requestDisallowInterceptTouchEvent(true); –  May 23 '16 at 14:07
  • 1
    Wow,, I lost so much time trying to solve this problem.. Thank you guys. mParent.requestDisallowInterceptTouchEvent(true); did the trick, and I don't event have to do mParent.requestDisallowInterceptTouchEvent(false); on the up event. It handle it itself. My viewpager always was scrolling now matter what I did. – Pedro Varela Nov 14 '16 at 16:32
  • Call mParent.requestDisallowInterceptTouchEvent(true); inside onTouchEvent. Outside [eg. on fragment level] it's not working. – Grzegorz Dev Apr 14 '17 at 12:16
  • Wow what a great answer! Thank you! Worked to set up onTouchListener on ViewHolder in RecycleView. Previously, SelectionTracker was interrupting the onTouchListener to be canceled and return incorrect event.y – coolcool1994 Jun 13 '20 at 07:05
30

An ACTION_CANCEL happens when a parent view takes over control of one of its children views.

Take a look at the documentation around ViewGroup.onInterceptTouchEvent(MotionEvent) method. From the link:

  1. You will receive the down event here.
  2. The down event will be handled either by a child of this view group, or given to your own onTouchEvent() method to handle; this means you should implement onTouchEvent() to return true, so you will continue to see the rest of the gesture (instead of looking for a parent view to handle it). Also, by returning true from onTouchEvent(), you will not receive any following events in onInterceptTouchEvent() and all touch processing must happen in onTouchEvent() like normal.
  3. For as long as you return false from this function, each following event (up to and including the final up) will be delivered first here and then to the target's onTouchEvent().
  4. If you return true from here, you will not receive any following events: the target view will receive the same event but with the action ACTION_CANCEL, and all further events will be delivered to your onTouchEvent() method and no longer appear here
Rahul Sharma
  • 2,867
  • 2
  • 27
  • 40
nicholas.hauschild
  • 42,483
  • 9
  • 127
  • 120
1

Need to disallow parent view to intercept the touch event:

override fun dispatchTouchEvent(event: MotionEvent): Boolean {
    when (event.action) {
        MotionEvent.ACTION_DOWN -> {
            (parent as? ViewGroup?)?.requestDisallowInterceptTouchEvent(true)
        }
        MotionEvent.ACTION_UP -> {
            (parent as? ViewGroup?)?.requestDisallowInterceptTouchEvent(false)
        }
        else -> {
        }
    }
    return true
}
xTwisteDx
  • 2,152
  • 1
  • 9
  • 25
ali M
  • 11
  • 1
  • 1
0

Another alternative way it is: You have ViewGroup with View inside.

  1. The ViewGroup's method onInterceptTouchEvent() return false for ACTION_DOWN and true for other cases
  2. The View always returns true in onTouch or onTouchEvent
  3. A user makes a guest -ACTION_DOWN, ACTION_MOVE...

As a result you will see the next flow

  1. ACTION_DOWN iteration:

    ViewGroup dispatchTouchEvent >start< ev = ACTION_DOWN
    ViewGroup onInterceptTouchEvent false
        View dispatchTouchEvent >start< ev = ACTION_DOWN
        View onTouch true
        View dispatchTouchEvent >finish< true
    ViewGroup dispatchTouchEvent >finish< true
    
  2. ACTION_MOVE iteration:

    ViewGroup dispatchTouchEvent >start< ev = ACTION_MOVE
    ViewGroup onInterceptTouchEvent true //<- start intercepting
        View dispatchTouchEvent >start< ev = ACTION_CANCEL //<- View is notified that control was intercepted
        View onTouch true
        View dispatchTouchEvent >finish< true
    ViewGroup dispatchTouchEvent >finish< true
    
  3. ACTION_MOVE iteration:

    ViewGroup dispatchTouchEvent >start< ev = ACTION_MOVE
    ViewGroup dispatchTouchEvent >finish< false
    

[Touch event flow]

yoAlex5
  • 29,217
  • 8
  • 193
  • 205