dm78
dm78

Reputation: 1620

Intercepting MotionEvents and preventing parent from processing them in ViewPager

I am placing custom child views into a ViewPager. I don't have any significant experience with intercepting touch events so this is all quite new to me.

I've looked at a large number of questions related to this on SO and other blogs, but so far none of the suggested advice has helped me too much.

My custom view has two panels which overlap one another. I need to allow the user to swipe away the front-most panel without passing those touch events to the parent ViewPager.

I'm having a lot of difficulty understanding this since it seems that my view's onTouchEvent is always called despite returning false when appropriate in my view's onInterceptTouchEvent. My understanding may be wrong here, but I have the impression that returning false should mean that my view's onTouchEvent should not be called.

Here is the code for my custom view:

package com.example.dm78.viewpagertouchexample;

import android.content.Context;
import android.support.v4.view.GestureDetectorCompat;
import android.util.AttributeSet;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.TextView;

public class MyCustomView extends FrameLayout {

    public static final String TAG = MyCustomView.class.getSimpleName();

    private GestureDetectorCompat mGestureDetector;
    private PanelData mPanelData;
    private LinearLayout mFrontPanel;
    private float xAdditive;
    private float mFrontPanelInitialX = Float.NaN;
    private float mDownX = Float.NaN;
    private float frontPanelXSwipeThreshold = 100;  // arbitrary value

    public MyCustomView(Context context, PanelData holder) {
        super(context);
        mPanelData = holder;
        init();
    }

    public MyCustomView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    private void init() {
        mGestureDetector = new GestureDetectorCompat(getContext(), new GestureDetector.OnGestureListener() {
            @Override
            public boolean onDown(MotionEvent e) {
                return false;
            }

            @Override
            public void onShowPress(MotionEvent e) {

            }

            @Override
            public boolean onSingleTapUp(MotionEvent e) {
                return false;
            }

            @Override
            public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
                return false;
            }

            @Override
            public void onLongPress(MotionEvent e) {

            }

            @Override
            public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
                return false;
            }
        });

        View view = View.inflate(getContext(), R.layout.my_custom_view, this);
        mFrontPanel = (LinearLayout) view.findViewById(R.id.front_panel);
        FrameLayout mRearPanel = (FrameLayout) findViewById(R.id.rear_panel);
        TextView mFrontTextView = (TextView) findViewById(R.id.front_textView);
        TextView mRearTextView = (TextView) findViewById(R.id.rear_textView);

        mFrontTextView.setText(mPanelData.frontText);
        mRearTextView.setText(mPanelData.rearText);

        if (mPanelData.showFrontPanel) {
            mFrontPanel.setVisibility(View.VISIBLE);
        }
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);

        mFrontPanelInitialX = mFrontPanel.getX();
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        return super.dispatchTouchEvent(ev) || mGestureDetector.onTouchEvent(ev);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (mPanelData.showFrontPanel) {
            getParent().requestDisallowInterceptTouchEvent(true);
            return true;
        }
        return false;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        float dX;
        long duration;
        boolean result = false;
        int xDir = 0;

        if (mDownX != Float.NaN) {
            xDir = (int) Math.abs(event.getRawX() - mDownX);
        }

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                // for later ACTION_MOVE events, we'll add xAdditive to event.getRawX() to get a dX for animations
                xAdditive = mFrontPanel.getX() - event.getRawX();
                mDownX = event.getRawX();
                result = true;
                break;
            case MotionEvent.ACTION_MOVE:
                // left movement detected
                if (xDir < 0) {
                    // animate panel interactively with new events coming in

                    // assume we haven't passed the point of no return
                    dX = event.getRawX() + xAdditive;
                    duration = 0;

                    if ((mFrontPanel.getX() + dX) < frontPanelXSwipeThreshold) {
                        // go ahead and animate the panel away
                        dX = -mFrontPanel.getWidth();
                        duration = 200;
                    }

                    mFrontPanel.animate().x(dX).setDuration(duration).start();
                    result = true;
                }
                break;
            case MotionEvent.ACTION_UP:
                // test value here is arbitrary
                if (xDir < -10) {
                    int newX;
                    // if panel has been moved left enough, just animate it away
                    if (mFrontPanel.getX() < frontPanelXSwipeThreshold) {
                        newX = -mFrontPanel.getWidth();
                    }
                    // otherwise, animate return to initial position
                    else {
                        newX = -getContext().getResources().getDisplayMetrics().widthPixels;
                    }

                    mFrontPanel.animate().x(newX).setDuration(200).start();
                    result = true;
                    getParent().requestDisallowInterceptTouchEvent(false);
                }
                break;
            default:
                result = super.onTouchEvent(event) || mGestureDetector.onTouchEvent(event);
        }

        return result;
    }

    public static class PanelData {
        public String rearText;
        public String frontText;
        public boolean showFrontPanel;
    }
}

Right now, there is nothing happening that doesn't normally happen in a ViewPager. My child view completely ignores the touch input. I'm sure I'm doing something stupid here, but I don't have enough experience with this portion of the Android SDK to know what it is.

I've tossed a GestureDetector.OnGestureListener in on the suggestion of some blog, but I haven't found much use for it and don't know how it might help me to begin with.

Can you find something obviously wrong with my code? Am I even on the right track?

Update 2015-09-01: I've discovered that ViewParent#requestDisallowInterceptTouchEvent(boolean) method should probably be called on my view's parent at a couple of points. It seems like perhaps onInterceptTouchEvent might be a good place to set the flag and in onTouchEvent in the case for ACTION_UP might be a good place to cancel it. This is obviously not correct, since when I scroll to a page that has a front panel that the user needs to swipe away, the ViewPager stops scrolling entirely and there is no user-visible response from the app. How can I make this work?

Working example app repo: https://gitlab.com/dm78/ViewPagerTouchExample/tree/master

Upvotes: 2

Views: 6354

Answers (3)

dm78
dm78

Reputation: 1620

There were a number of problems with my code.

  • I forgot to remove a call to Math.abs() when determining the direction of motion for an event.
  • I needed to tweak some values I'm checking against when deciding how to animate the panel.
  • I needed to call requestDisallowInterceptTouchEvent(boolean) on my view's parent in a few places to enable/disable the parent from swallowing up all of the events being generated on my view.

By using requestDisallowInterceptTouchEvent(boolean) in my view, this allows the child to handle the events rather than tightly coupling the view to the ViewPager that is displaying it, which I feel is far preferable since this technique can be applied to (as far as I can imagine) just about any view anywhere in any UI context to get the desired behavior. It doesn't rely on the other elements of the view knowing intimate details about what is going on in order for the solution to function correctly.

After solving the above problems, it was just a matter of tweaking how the animations are triggered to achieve the desired interaction.

My solution still has one minor problem, but that is for another SO question.

Upvotes: 5

vishnus
vishnus

Reputation: 728

By default the parent receives touch events before its children in android. So the view pager's onInterceptTouchEvent will be called when a touch is detected.

So to disable touch on the ViewPager, subclass ViewPager and return false from onInterceptTouchEvent. e.g

public class MyViewPager extends ViewPager {
 //constructors n rest of the code here...

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
       //custom logic here...
       return false;
    }
}

And yes you are right, returning false will prevent onTouchEvent from being called for that particular viewgroup.

I hope this was helpful. Cheers :)

Upvotes: 0

maciekjanusz
maciekjanusz

Reputation: 4775

From the documentation of onInterceptTouchEvent(MotionEvent ev)

Return true to steal motion events from the children and have them dispatched to this ViewGroup through onTouchEvent(). The current target will receive an ACTION_CANCEL event, and no further messages will be delivered here.

Hence, you want to return true when child views are not supposed to get touch events. As default, MotionEvents are "passed" from the root of layout stack to its child views until one of them doesn't have any children or intercepts this event.

As I understand, you want to temporarily disable paging of the ViewPager when performing swipe gestures on some child view within that pager, so the pager mustn't intercept the event and also it cannot consume the touch event (has to return false from onTouchEvent(MotionEvent ev)). I came upon similar problem and this answer helped, and it could be a solution to your problem.

Upvotes: 0

Related Questions