Android 滑动冲突拦截法

在开发过程中遇到ViewPager嵌套ListView或者ScrollView嵌套ListView等情况时,可能会出现滑动冲突问题,这是因为ListView是纵向滑动,而ViewPager是横向滑动。

虽然ListView、ViewPager类内部源码已经做好了相应的处理,使他们能同时使用。但是有时为了满足自己项目的特殊需求,还是需要我们解决滑动冲突。

解决滑动冲突问题有两种方法:内部拦截法和外部拦截法。滑动冲突也存在2种场景: 横竖滑动冲突、同向滑动冲突。同向滑动和横竖滑动思路是一样的,只是用来判断是否拦截的那块逻辑不同而已。 

推荐:当子元素占满父元素空间时,使用外部拦截法;当没有占满时使用内部拦截。

1.外部拦截方法

外部拦截法是指所有的触摸事件都会先经过经过父容器的传递,从而父容器在需要此触摸事件的时候就可以拦截此触摸事件,否则就传递给子View。这样就可以解决滑动冲突的问题,这种方法比较符合触摸事件的传递、处理机制。

外部拦截法需要重写父容器的onInterceptTouchEvent方法,在该方法中根据滑动冲突处理规则做相应的拦截。

外部拦截法的思路是:重写父控件的onInterceptTouchEvent方法,然后根据具体的需求,来决定父控件是否拦截事件。如果拦截返回返回true,不拦截返回false。如果父控件拦截了事件,则在父控件的onTouchEvent进行相应的事件处理。 

以一个横向滑动的ViewGroup里面包含了3个竖向滑动的ListView为例:

自定义横向滑动控件——HorizontalEx.java:

扫描二维码关注公众号,回复: 14229887 查看本文章

public class HorizontalEx extends ViewGroup {

    private boolean isFirstTouch = true;

    private int childIndex;

    private int childCount;

    private int lastXIntercept, lastYIntercept, lastX, lastY;

    private Scroller mScroller;

    private VelocityTracker mVelocityTracker;

    public HorizontalEx(Context context) {

        super(context);

        init();

    }

    public HorizontalEx(Context context, AttributeSet attrs) {

        super(context, attrs);

        init();

    }

    public HorizontalEx(Context context, AttributeSet attrs, int defStyleAttr) {

        super(context, attrs, defStyleAttr);

        init();

    }

    private void init() {

        mScroller = new Scroller(getContext());

        mVelocityTracker = VelocityTracker.obtain();

    }

    @Override

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        int width = MeasureSpec.getSize( widthMeasureSpec);

        int height = MeasureSpec.getSize( heightMeasureSpec);

        int widthMode = MeasureSpec.getMode( widthMeasureSpec);

        int heightMode = MeasureSpec.getMode( heightMeasureSpec);

        childCount = getChildCount();

        measureChildren(widthMeasureSpec, heightMeasureSpec);

        if (childCount == 0) {

            setMeasuredDimension(0, 0);

        } else if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {

            width = childCount * getChildAt(0).getMeasuredWidth();

            height = getChildAt(0).getMeasuredHeight();

            setMeasuredDimension(width, height);

        } else if (widthMode == MeasureSpec.AT_MOST) {

            width = childCount * getChildAt(0).getMeasuredWidth();

            setMeasuredDimension(width, height);

        } else {

            height = getChildAt(0).getMeasuredHeight();

            setMeasuredDimension(width, height);

        }

    }

    @Override

    protected void onLayout(boolean changed, int l, int t, int r, int b) {

        int left = 0;

        for (int i = 0; i < getChildCount(); i++) {

            final View child = getChildAt(i);

            child.layout(left + l, t, r + left, b);

            left += child.getMeasuredWidth();

        }

    }

    @Override

    public boolean onInterceptTouchEvent( MotionEvent ev) {

        boolean intercepted = false;

        int x = (int) ev.getX();

        int y = (int) ev.getY();

        switch (ev.getAction()) {

            /*如果拦截了Down事件,则子类不会拿到这个事件序列*/

            case MotionEvent.ACTION_DOWN:

                lastXIntercept = x;

                lastYIntercept = y;

                intercepted = false;

                if (!mScroller.isFinished()) {

                    mScroller.abortAnimation();

                    intercepted = true;

                }

                break;

            case MotionEvent.ACTION_MOVE:

                final int deltaX = x - lastXIntercept;

                final int deltaY = y - lastYIntercept;

                /*根据条件判断是否拦截该事件*/

              if (Math.abs(deltaX) > Math.abs(deltaY)){

                    intercepted = true;

                } else {

                    intercepted = false;

                }

                break;

            case MotionEvent.ACTION_UP:

                intercepted = false;

                break;

        }

        lastXIntercept = x;

        lastYIntercept = y;

        return intercepted;

    }

    @Override

    public boolean onTouchEvent(MotionEvent event) {

        int x = (int) event.getX();

        int y = (int) event.getY();

        mVelocityTracker.addMovement(event);

        ViewConfiguration configuration = ViewConfiguration.get(getContext());

        switch (event.getAction()) {

            case MotionEvent.ACTION_DOWN:

                if (!mScroller.isFinished()) {

                    mScroller.abortAnimation();

                }

                break;

            case MotionEvent.ACTION_MOVE:

                /*因为这里父控件拿不到Down事件,所以使用一个布尔值, 当事件第一次来到父控件时,对lastX,lastY赋值*/

                if (isFirstTouch) {

                    lastX = x;

                    lastY = y;

                    isFirstTouch = false;

                }

                final int deltaX = x - lastX;

                scrollBy(-deltaX, 0);

                break;

            case MotionEvent.ACTION_UP:

                int scrollX = getScrollX();

                final int childWidth = getChildAt(0).getWidth();

           mVelocityTracker.computeCurrentVelocity(1000, configuration.getScaledMaximumFlingVelocity());

                float xVelocity = mVelocityTracker.getXVelocity();

                if (Math.abs(xVelocity) > configuration.getScaledMinimumFlingVelocity()){

                    childIndex = xVelocity < 0 ? childIndex + 1 : childIndex - 1;

                } else {

                    childIndex = (scrollX + childWidth / 2) / childWidth;

                }

                childIndex = Math.min(getChildCount() - 1, Math.max(childIndex, 0));

                smoothScrollBy(childIndex * childWidth - scrollX, 0);

                mVelocityTracker.clear();

                isFirstTouch = true;

                break;

        }

        lastX = x;

        lastY = y;

        return true;

    }

    void smoothScrollBy(int dx, int dy) {

        mScroller.startScroll(getScrollX(), getScrollY(), dx, dy, 500);

        invalidate();

    }

    @Override

    public void computeScroll() {

        if (mScroller.computeScrollOffset()) {

            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());

            invalidate();

        }

    }

   @Override

    protected void onDetachedFromWindow() {

        super.onDetachedFromWindow();

        mVelocityTracker.recycle();

    }

}

调用代码:
public void showOutHVData(List<String> data1, List<String> data2, List<String> data3) {
    ListView listView1 = new ListView(this);
    ArrayAdapter<String> adapter1 = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, data1);
    listView1.setAdapter(adapter1);

    ListView listView2 = new ListView(this);
    ArrayAdapter<String> adapter2 = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, data2);
    listView2.setAdapter(adapter2);

    ListView listView3 = new ListView(this);
    ArrayAdapter<String> adapter3 = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, data3);
    listView3.setAdapter(adapter3);

    ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,ViewGroup.LayoutParams.MATCH_PARENT);

    mHorizontalEx.addView(listView1, params);
    mHorizontalEx.addView(listView2, params);
    mHorizontalEx.addView(listView3, params);
}

其实外部拦截的主要思想都在于对onInterceptTouchEvent的重写。

注意:

①重写onInterceptTouchEvent方法时一定不要在ACTION_DOWN 中返回 true,否则会让子VIew没有机会得到事件,因为如果在ACTION_DOWN的时候返回了 true,同一个事件序列ViewGroup的disPatchTouchEvent就不会再调用onInterceptTouchEvent方法了。 

②在ACTION_UP中返回false,因为如果父控件拦截了ACTION_UP,那么子View将得不到UP事件,那么将会影响子View的 Onclick方法等。但这对父控件是没有影响的,因为如果是父控件在ACITON_MOVE中就拦截了事件,那么UP事件必定也会交给它处理,因为有那么一条定律叫做:父控件一但拦截了事件,那么同一个事件序列的所有事件都将交给他处理。

③最后就是在 ACTION_MOVE中根据需求决定是否拦截。

2.内部拦截法

内部拦截法是指父容器不拦截任何触摸事件,所有的触摸事件都传递给子元素,如果子元素需要此触摸事件就直接消耗掉,否则就交由父容器进行处理,这种方法和Android中的事件传递、处理机制不一致,需要配合requestDisallowInterceptTouchEvent方法才能正常工作,使用起来比外部拦截法稍显复杂。这种方法需要重写子元素的dispatchTouchEvent方法和父容器的onInterceptTouchEvent方法。

内部拦截主要依赖于requestDisallowInterceptTouchEvent方法,它用于设置父控件的FLAG_DISALLOW_INTERCEPT标志,这个标志可以决定父控件是否拦截事件,如果设置了这个标志则不拦截;如果没设这个标志,就会调用父控件的onInterceptTouchEvent来询问父控件是否拦截。但这个标志对Down事件无效。

那么如果我们想使用内部拦截法拦截事件,就需要:

①第一步: 要重写父控件的onInterceptTouchEvent方法,在ACTION_DOWN的时候返回false,否则的话子View调用requestDisallowInterceptTouchEvent也无能为力。 然后除ACTON_DOWN以外的其他事件都返回true,这样就把能否拦截事件的权利交给了子View。

②第二步: 在子View的dispatchTouchEvent中来决定是否让父控件拦截事件。 先在ACTION_DOWN的时候使用requestDisallowInterceptTouchEvent(true),否则,下一个事件到来时,就交给父控件了。 然后在ACTION_MOVE的时候根据业务逻辑决定是否调用requestDisallowInterceptTouchEvent(false)来决定父控件是否拦截事件。 

上代码: 

public class HorizontalEx2 extends ViewGroup {

    ……

    //不拦截Down事件,其他一律拦截

    @Override

    public boolean onInterceptTouchEvent( MotionEvent ev) {

        if (ev.getAction() == MotionEvent.ACTION_DOWN) {

            if (!mScroller.isFinished()) {

                mScroller.abortAnimation();

                return true;

            }

            return false;

        } else {

            return true;

        }

    }

    ……

}

自定义ListView——ListViewEx.java:

public class ListViewEx extends ListView {

    private int lastXIntercepted, lastYIntercepted;

    private HorizontalEx2 mHorizontalEx2;

   ……//3个默认构造方法

    public void setmHorizontalEx2(HorizontalEx2 mHorizontalEx2) {

        this.mHorizontalEx2 = mHorizontalEx2;

    }

    //使用requestDisallowInterceptTouchEvent();来决定父控件是否对事件进行拦截

    @Override

    public boolean dispatchTouchEvent( MotionEvent ev) {

        int x = (int) ev.getX();

        int y = (int) ev.getY();

        switch (ev.getAction()) {

            case MotionEvent.ACTION_DOWN:

              mHorizontalEx2. requestDisallowInterceptTouchEvent(true);

                break;

            case MotionEvent.ACTION_MOVE:

                final int deltaX = x-lastYIntercepted;

                final int deltaY = y-lastYIntercepted;

                if(Math.abs(deltaX)>Math.abs(deltaY)){

                    mHorizontalEx2. requestDisallowInterceptTouchEvent(false);

                }

                break;

            case MotionEvent.ACTION_UP:

                break;

        }

        lastXIntercepted = x;

        lastYIntercepted = y;

        return super.dispatchTouchEvent(ev);

    }

}

调用代码:

public void showInnerHVData(List<String> data1, List<String> data2, List<String> data3) {

    ListViewEx listView1 = new ListViewEx(this);

    ArrayAdapter<String> adapter1 = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, data1);

    listView1.setAdapter(adapter1);

        listView1. setmHorizontalEx2(mHorizontalEx2);

    ListViewEx listView2 = new ListViewEx(this);

    ArrayAdapter<String> adapter2 = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, data2);

    listView2.setAdapter(adapter2);

    listView2. setmHorizontalEx2(mHorizontalEx2);

    ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,ViewGroup.LayoutParams. MATCH_PARENT);

    mHorizontalEx2.addView(listView1, params);

    mHorizontalEx2.addView(listView2, params);

}

内部拦截法关键点:

①父容器onInterceptTouchEvent默认拦截ACTION_MOVE,但是ACTION_DOWN不能拦截。因为一旦拦截ACTION_DOWN,整个事件序列都会被拦截,子View就没机会处理ACTION_MOVE了。

②子View接收到ACTION_DOWN后,调用getParent().requestDisallowInterceptTouchEvent(true)来取消父容器的拦截,告诉父容器接下来的事件序列不要拦截,这样后面的事件子View都能接收到。

③子View接收到ACTION_MOVE后,判断滑动方向,根据方向确定由谁处理事件。如果判断为父容器处理滑动,则调用getParent().requestDisallowInterceptTouchEvent(false)开启父容器拦截,父容器就能处理ACTION_MOVE了;如果判断为子View处理滑动,由于子View接收到ACTION_DOWN时,已经取消了父容器的拦截,子view直接处理滑动即可。

举个例子:

使用内部拦截法,需要修改把父容器拦截方法:

@Override

public boolean onInterceptTouchEvent( MotionEvent event) {

    int action = event.getAction();

    if(action == MotionEvent.ACTION_DOWN) {

        return false;

    } else {

        return true;

    }

}

同时修改子View的dispatchTouchEvent方法:

@Override

public boolean dispatchTouchEvent(MotionEvent ev) {

    int x = (int) ev.getX();

    int y = (int) ev.getY();

    switch (ev.getAction()) {

        case MotionEvent.ACTION_DOWN:

            //接到ACTION_DOWN开始取消父容器的事件拦截

           getParent( ).requestDisallowInterceptTouchEvent(true);

            break;

        case MotionEvent.ACTION_MOVE:

            int deltaX = x - mLastX;

            int deltaY = y - mLastY;

            //横向偏移大,判定为横向滑动,交给父容器ViewPager处理

           if (Math.abs(deltaX) > Math.abs(deltaY)) {

                //父容器恢复事件拦截,事件交给父容器处理

                getParent( ).requestDisallowInterceptTouchEvent(false);

            }

               break;

            case MotionEvent.ACTION_UP:

                break;

        }

        mLastX = x;

        mLastY = y;

        return super.dispatchTouchEvent(ev);

    }

}

3.注意

①down事件只会分发一次,move会分发多次
②内部拦截思路是子View在dispatchTouchEvent方法中通过调用requestDisallowInterceptTouchEvent(true)方法,禁止父View拦截事件。并在合适的场景将其置为fase允许拦截
③外部拦截思路是父View通过重写onInterceptTouchEvent方法,在合适的场景拦截事件
注意,如果是down事件,父亲拦截了这个事件,那么子View 将不在收到后续的事件,因此在解决事件冲突的时候,父亲不能拦截down事件,而是在后续的move 事件在合适的场景做拦截,比如滑动的方向。
当然如果需求是父亲要直接拦截子View的事件,那么也可以事件拦截掉down事件

猜你喜欢

转载自blog.csdn.net/zenmela2011/article/details/123721765