拦截 MotionEvents 并阻止父级在 ViewPager 中处理它们

Intercepting MotionEvents and preventing parent from processing them in ViewPager

我正在将自定义子视图放入 ViewPager。我在拦截触摸事件方面没有任何重要经验,所以这对我来说都是全新的。

我在 SO 和其他博客上查看了大量与此相关的问题,但到目前为止 none 的建议对我帮助太大了。

我的自定义视图有两个相互重叠的面板。我需要允许用户轻扫最前面的面板而不将这些触摸事件传递给父 ViewPager。

我很难理解这一点,因为我的视图 onTouchEvent 似乎总是被调用,尽管在我的视图 onInterceptTouchEvent 中适当时返回 false。我的理解可能在这里是错误的,但我的印象是返回 false 应该意味着我的视图的 onTouchEvent 不应该被调用。

这是我的自定义视图的代码:

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;
    }
}

现在,在 ViewPager 中通常不会发生任何事情。我的子视图完全忽略了触摸输入。我确定我在这里做了一些愚蠢的事情,但我对 Android SDK 的这一部分没有足够的经验来知道它是什么。

我根据一些博客的建议投入了 GestureDetector.OnGestureListener,但我没有发现它有多大用处,也不知道它对我有什么帮助。

你能发现我的代码有什么明显的错误吗?我走对了吗?

2015-09-01更新: 我发现 ViewParent#requestDisallowInterceptTouchEvent(boolean) 方法可能应该在我的视图的父级上在几个点上调用。看起来 onInterceptTouchEvent 可能是设置标志的好地方,而在 onTouchEventACTION_UP 可能是取消它的好地方。这显然是不正确的,因为当我滚动到具有用户需要滑动的前面板的页面时,ViewPager 完全停止滚动并且应用程序没有用户可见的响应。我怎样才能使这项工作?

工作示例应用回购:https://gitlab.com/dm78/ViewPagerTouchExample/tree/master

来自 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.

因此,当子视图不应获得触摸事件时,您希望 return 为真。默认情况下,MotionEvents "passed" 从布局堆栈的根到它的子视图,直到其中一个没有任何子视图或拦截此事件。

据我了解,您想在该寻呼机中的某些子视图上执行滑动手势时暂时禁用 ViewPager 的寻呼,因此寻呼机不得拦截该事件,也不能消耗触摸事件(必须return 来自 onTouchEvent(MotionEvent ev)). I came upon similar problem and this answer 的 false 有所帮助,它可能是您问题的解决方案。

默认情况下,在 android 中,父级先于其子级接收触摸事件。因此,当检测到触摸时,将调用视图寻呼机的 onInterceptTouchEvent。

所以要禁用 ViewPager 上的触摸,从 onInterceptTouchEvent 继承 ViewPager 和 return false。例如

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

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

是的,你是对的,return设置为 false 将阻止为特定视图组调用 onTouchEvent。

希望对您有所帮助。干杯:)

我的代码有很多问题。

  • 我在确定事件的运动方向时忘记删除对 Math.abs() 的调用。
  • 在决定如何为面板设置动画时,我需要调整一些我正在检查的值。
  • 我需要在我的视图 parent 的几个地方调用 requestDisallowInterceptTouchEvent(boolean) 来 enable/disable parent 避免吞噬我视图中生成的所有事件.

通过在我的视图中使用 requestDisallowInterceptTouchEvent(boolean),这允许 child 处理事件,而不是将视图与显示它的 ViewPager 紧密耦合,我认为这是更可取的是,这种技术可以应用于(据我所知)任何 UI 上下文中任何地方的几乎任何视图,以获得所需的行为。它不依赖视图的其他元素了解正在发生的事情的详细信息,以便解决方案正常运行。

解决了上述问题后,只需调整动画的触发方式即可实现所需的交互。

我的解决方案还有一个小问题,但那是针对另一个 SO 问题的。