Android View 滑动冲突解决方式以及原理

Android View 滑动冲突解决方式以及原理,第1张

概述一.滑动冲突场景以及产生原因产生滑动冲突的场景主要有两种:父ViewGroup和子View的滑动方向一致父ViewGroup和子View的滑动方向不一致那为什么会产生滑动冲突呢,例如在父ViewGroup和子View的滑动方向一致的情况,我需要让两者都可以滑动。在事件分发机制中,ViewGroup的onInterce 一. 滑动冲突场景以及产生原因

产生滑动冲突的场景主要有两种:

父VIEwGroup和子VIEw的滑动方向一致父VIEwGroup和子VIEw的滑动方向不一致

那为什么会产生滑动冲突呢,例如在父VIEwGroup和子VIEw的滑动方向一致的情况,我需要让两者都可以滑动。在事件分发机制中,VIEwGroup的@H_404_11@onIntercepttouchEvent方法默认情况下是返回false,也就是VIEwGroup默认情况下是不会拦截事件的。当VIEwGroup接收到事件时,由于不拦截事件,会去寻找能够处理事件的子VIEw。此时,一旦子VIEw处理了DOWN事件,默认情况下接下来同一事件序列的其他事件都交由子VIEw处理,此时可以看到的效果是子VIEw可以滑动,但是父VIEwGroup始终滑动不了,此时滑动冲突就出现了。

二. 滑动冲突的解决方式

滑动冲突主要有两种解决方式:外部拦截法和内部拦截法

2.1 VIEwPager 滑动冲突

例如我们使用VIEwPager时,往往会结合Fragment,然后Fragment内部为一个ListVIEw。这里VIEwPager已经为我们解决了滑动冲突,因此在使用时并不会冲突。试想下,若VIEwPager未解决滑动冲突,默认情况下VIEwPager的@H_404_11@onIntercepttouchEvent方法返回false,由于ListVIEw可以滚动,代表ListVIEw可以处理事件,所以所有事件都被ListVIEw处理了,因此我们看到的效果会是ListVIEw可以在竖直方向上滚动,但是VIEwPager在水平方向上无法滑动。

可以重写VIEwPager,让VIEwPager的@H_404_11@onIntercepttouchEvent方法返回默认状态下的false,VIEwPager内部是多个ListVIEw。

public class MyVIEwPager extends VIEwPager {    public MyVIEwPager(@NonNull Context context) {        super(context);    }    public MyVIEwPager(@NonNull Context context, @Nullable AttributeSet attrs) {        super(context, attrs);    }    @OverrIDe    public boolean onIntercepttouchEvent(MotionEvent ev) {        return false;    }}

运行效果如图

 

所以VIEwPager是如何解决这样的滑动冲突的呢,由此引出外部拦截法。

2.2 外部拦截法

2.2.1 原理

所谓外部拦截法,就是当事件传递到父容器时,通过父容器去判断自己是否需要此事件,若需要则拦截事件,不需要则不拦截事件,将事件传递给子VIEw。 上述MyVIEwPager和ListVIEw显然产生了滑动冲突,我们来分析下。我们要的效果是在水平方向上滑动时VIEwPager可以水平滚动,在竖直方向上滑动时,ListVIEw可以滚动但VIEwPager不动,因此我们需要为VIEwGroup指定事件处理的条件,于是就有了下面的伪代码。

@OverrIDepublic boolean onIntercepttouchEvent(MotionEvent ev) {    switch (ev.getAction()) {        case MotionEvent.ACTION_MOVE:            if (VIEwPager需要此事件) {                return true;            }            break;        default:            break;    }    return false;}

现在我们来分析下为什么这段代码可以解决滑动冲突。

 

这边首先要注意一点,外部拦截时在重写VIEwGroup的onIntercepttouchEvent方法时,VIEwGroup不能拦截DOWN事件和UP事件。因为一旦VIEwGroup拦截了DOWN事件,也就是和mFirsttouchTarget始终为空,同一事件序列中的其他事件都不会再往下传递;若VIEwGroup拦截了UP事件,则子VIEw就不会触发单击事件,因为子VIEw的单击事件是在UP事件时被触发的。

当VIEwPager接收到DOWN事件,VIEwPager默认不拦截DOWN事件,DOWN事件交由ListVIEw处理,由于ListVIEw可以滚动,即可以消费事件,则VIEwPager的mFirsttouchTarget会被赋值,即找到处理事件的子VIEw。然后VIEwPager接收到MOVE事件,若此事件是VIEwPager不需要,则同样会将事件交由ListVIEw去处理,然后ListVIEw处理事件;若此事件VIEwGroup需要,因为DOWN事件被ListVIEw处理,mFirsttouchEventTarget会被赋值,也就会调用onInterceptedtouchEvent,此时由于VIEwPager对此事件感兴趣,则onInterceptedtouchEvent方法会返回true,表示VIEwPager会拦截事件,此时当前的MOVE事件会消失,变为CANCEL事件,往下传递或者自己处理,同时mFirsttouchTarget被重置为null。当MOVE事件再次来到时,由于mFristtouchTarget为null,所以接下来的事件都交给了VIEwPager。

2.2.2 解决方式

这边VIEwPager处理事件的条件可以有多种方法,例如水平方向和竖直方向上的滑动速度、水平方向和竖直方向的滑动距离等。这边根据滑动距离判断,当水平方向的滑动距离大于竖直方向的滑动距离,则VIEwPager处理事件,反之则将事件传递给ListVIEw。

public class MyVIEwPager extends VIEwPager {    private int mLastX;    private int mLastY;    public MyVIEwPager(@NonNull Context context) {        super(context);    }    public MyVIEwPager(@NonNull Context context, @Nullable AttributeSet attrs) {        super(context, attrs);    }    @OverrIDe    public boolean onIntercepttouchEvent(MotionEvent ev) {        //一些VIEwPager拖拽的标志位要设置,必调super,否则看不到效果        super.onIntercepttouchEvent(ev);        boolean isIntercepted = false;        switch (ev.getAction()) {            case MotionEvent.ACTION_DOWN:                break;            case MotionEvent.ACTION_MOVE:                if (needEvent(ev)) {                    isIntercepted = true;                }                break;            default:        }        mLastX = (int) ev.getX();        mLastY = (int) ev.getY();        LogUtils.d(" lastX = " + mLastX + " lastY = " + mLastY);        return isIntercepted;    }    private boolean needEvent(MotionEvent ev) {        //水平滚动距离大于垂直滚动距离则将事件交由VIEwPager处理        return Math.abs(ev.getX() - mLastX) > Math.abs(ev.getY() - mLastY);    }}

运行效果:

 

2.2.3 总结

外部拦截法主要是父容器去控制事件的拦截,若事件是父容器需要的,则进行拦截,不需要的则向下传递。父容器不能拦截DOWN事件或者UP事件。

2.2.4 通用模板

public boolean onIntercepttouchEvent(MotionEvent ev) {        boolean isIntercept = false;        switch (ev.getAction()) {            case MotionEvent.ACTION_DOWN:                //DOWN事件不能拦截,否则事件将无法分发到子VIEw                isIntercept = false;                break;            case MotionEvent.ACTION_MOVE:                //根据条件判断是否拦截事件                isIntercept = needThisEvent();                break;            case MotionEvent.ACTION_UP:                //一旦父容器拦截了UP事件,子VIEw将无法触发点击事件                isIntercept = false;                break;            default:                break;        }        return isIntercept;    }
2.3 内部拦截法

2.3.1 冲突场景

下面讲一种稍微复杂一点的同向滑动冲突。ScrollVIEw内部的内部的linearLayout存在三个子VIEw,从上到下分别为ImageVIEw、ListVIEw以及TextVIEw。

 

先上下效果图:

可以看到现在需要的效果是触摸ListVIEw外部的区域,ScrollVIEw的滑动不受限制。当触摸ListVIEw区域时,存在多种情况。当ListVIEw滚动到顶部时(ListVIEw处于初始状态),此时若手指往下滑动,则ScrollVIEw往下滑动;当ListVIEw滚动到底部时,若此时手指往上滑动,则ScrollVIEw往上滑动,其余情况下ListVIEw滚动。

2.3.2 原理

内部拦截法: VIEwGroup默认情况下不拦截事件,由子VIEw去控制事件的处理,若子VIEw需要此事件,则自己处理,否则交由父容器处理。

这种方式需要配合VIEwGroup的FLAG_disALLOW_INTERCEPT标志位来使用。设置此标志为可以通过requestdisallowIntercept touchEvent函数来设置,如果设置了此标志位,那么VIEwGroup就无法拦截除了ACTION_DOWN之外的任何事件。这样首先我们保证VIEwGroup的onIntercepttouchEvent方法除了DOWN其他都返回true,DOWN返回false,这样保证了不会拦截DOWN事件,交给它的子VIEw进行处理;重写VIEw的dispatchtouchEvent函数,在DOWN中设置parent.requestdisallowIntercepttouchEvent(true),这样父控件在默认的情况下DOWN之后的所有事件它都拦截不到,交由子VIEw来处理,VIEw在MOVE中判断父控件需要时,调用parent.requestdisallow IntercepttouchEvent(false),这样父控件的拦截又起作用了,相应的事件交给了父控件进行处理。

VIEwGroup 伪代码

@OverrIDepublic boolean onIntercepttouchEvent(MotionEvent ev) {    int action = ev.getAction();    if(action == MotionEvent.ACTION_DOWN){        return false;    } else {        return true;    }}

子VIEw伪代码

public boolean dispatchtouchEvent(MotionEvent ev) {        switch (ev.getAction()){        case MotionEvent.ACTION_DOWN:            getParent().requestdisallowIntercepttouchEvent(true);            break;        case MotionEvent.ACTION_MOVE:            if(父控件需要此点击事件){                getParent().requestdisallowIntercepttouchEvent(false);            }            break;        case MotionEvent.ACTION_UP:            break;    }}

这边我们结合ScrollVIEw和ListVIEw这个具体实例和流程图进行分析。

 

 

首先父容器ScrollVIEw不能拦截DOWN事件,必须将DOWN事件分发至子VIEw,这边子VIEw是 ListVIEw,因为父容器一旦拦截DOWN事件,同一事件序列中的其他事件都不会传递到子VIEw,这点在事件分发源码分析时已经分析了,这里不再赘述。

由于内部拦截是将事件交由子VIEw,由子VIEw去控制事件的处理,所以事件在一开始不能被父VIEwGroup直接拦截,由于DOWN事件被子VIEw处理,此时mFristTonchTarget不为null,在默认情况下会去调用onInterceptedtouchEvent,若针对该事件该方法返回true,则事件就会被父容器拦截了,事件显然不会传递到子VIEw,但是我们需要将事件传递到子VIEw,让子VIEw去控制事件的处理。那我们要怎么将事件传递到子VIEw呢?从源码可以看到在调用onInterceptedtouchEvent方法前还有一个判断。

if (actionMasked == MotionEvent.ACTION_DOWN        || mFirsttouchTarget != null) {    //是否禁止拦截事件,默认为false    final boolean disallowIntercept = (mGroupFlags & FLAG_disALLOW_INTERCEPT) != 0;    if (!disallowIntercept) {        intercepted = onIntercepttouchEvent(ev);        ev.setAction(action); // restore action in case it was changed    } else {        intercepted = false;    }} else {    intercepted = true;}

从源代码可以看到,会根据disallowIntercept的值判断是否要调用onIntercepttouchEvent这个方法,disallowIntercept默认为false。此时若可以将disallowIntercept的值变为true,就可以绕过onIntercepted方法,将事件传递到子VIEw了,也就是我们需要在MOVE事件到来之前给mGroupFlags设置FLAG_disALLOW_INTERCEPT标志位,设置好后,若MOVE事件到来,disallowIntercept的值就会变为true,就会绕过onInterceptedtouchEvent方法的执行,将事件传递到子VIEw了,那如何在MOVE事件到来之前给VIEwGroup设置这个标志位呢?我们可以在VIEwGroup中看到这个方法。

public voID requestdisallowIntercepttouchEvent(boolean disallowIntercept) {    if (disallowIntercept == ((mGroupFlags & FLAG_disALLOW_INTERCEPT) != 0)) {        return;    }    if (disallowIntercept) {        mGroupFlags |= FLAG_disALLOW_INTERCEPT;    } else {        mGroupFlags &= ~FLAG_disALLOW_INTERCEPT;    }    // Pass it up to our parent    if (mParent != null) {        mParent.requestdisallowIntercepttouchEvent(disallowIntercept);    }}

可以看到,若在调用requestdisallowIntercepttouchEvent方法时,参数为true,则mGroupFlags设置了FLAG_disALLOW_INTERCEPT标志位,也就是disallowIntercept的值就会变为true。至于调用时机,我们只需要在子VIEw接收到DOWN事件时调用该方法即可,此后父VIEwGroup会直接将事件传递给处理DOWN事件的子VIEw。

@OverrIDepublic boolean dispatchtouchEvent(MotionEvent ev) {    switch (ev.getAction()) {        case MotionEvent.ACTION_DOWN:            //禁止父容器拦截事件            getParent().requestdisallowIntercepttouchEvent(true);            break;            ...        }        ...    }}

若接下来的事件是子VIEw感兴趣的,则直接处理掉,如果子VIEw对事件不感兴趣,则将事件交还给父VIEw,让它去处理。那么问题又来了,如何将子VIEw不需要的事件重新交还给父VIEw处理?此时可能有人会说,在事件分发中,子VIEw处理不了的事件,不是自动会交给父VIEwGroup处理吗?我们说的子VIEw处理不了的事件会传递给父VIEwGroup处理,这个是针对默认的DOWN事件分发流程,但是在这不是DOWN事件且这里存在人工干预的情况,真的会是这样吗,我们来看看源码。

先明确下当前的情景,子VIEw处理了DOWN事件和部分MOVE事件,此时父VIEwGroup的mFirsttouchEvent肯定是不为null的。接下来的MOVE事件子VIEw不需要,也就是子VIEw不做处理,那么子VIEw的dispatchtouchEvent方法会返回false。

public boolean dispatchtouchEvent(MotionEvent ev) {        ...    if (mFirsttouchTarget == null) {                handled = dispatchtransformedtouchEvent(ev, canceled, null,                        touchTarget.ALL_POINTER_IDS);    } else {        touchTarget predecessor = null;        touchTarget target = mFirsttouchTarget;        while (target != null) {            final touchTarget next = target.next;            if (alreadydispatchedToNewtouchTarget && target == newtouchTarget) {                handled = true;            } else {                final boolean cancelChild = @R_403_5990@CancelNextUpFlag(target.child)                        || intercepted;                //子VIEw 不处理事件, 子VIEw的dispatchtouchEvent返回false,dispatchtransformedtouchEvent为false                if (dispatchtransformedtouchEvent(ev, cancelChild,                        target.child, target.pointerIDBits)) {                    handled = true;                }                if (cancelChild) {                    if (predecessor == null) {                        mFirsttouchTarget = next;                    } else {                        predecessor.next = next;                    }                    target.recycle();                    target = next;                    continue;                }            }            predecessor = target;            target = next;        }    }    if (canceled            || actionMasked == MotionEvent.ACTION_UP            || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {        @R_403_5990@touchState();    } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {        final int actionIndex = ev.getActionIndex();        final int IDBitsToRemove = 1 << ev.getPointerID(actionIndex);        removePointersFromtouchTargets(IDBitsToRemove);    }    if (!handled && minputEventConsistencyVerifIEr != null) {        minputEventConsistencyVerifIEr.onUnhandledEvent(ev, 1);    }    //直接返回false    return handled;}

从源代码可以看到,在这个情景下,VIEwGroup的dispatchtouchEvent方法会直接返回false,不处理当前子VIEw不感兴趣的MOVE事件,父VIEwGroup的父容器也是这样直接返回false,直到传递给Activity,事件被Activity处理或者消失。并且当再一个MOVE事件来临时,MOVE还是会传递到子VIEw,但是子VIEw对当前MOVE事件不感兴趣,也就是说之后的所有MOVE事件都不会被父VIEwGroup处理,这样明显是存在问题的。所以子VIEw在对事件不感兴趣时,要如何事件处理权交给父VIEwGroup?我们在子VIEw 通过调用VIEwGroup的requestdisallowIntercepttouchEvent方法,禁止父VIEwGroup拦截事件,同样也可以在子VIEw对事件不感兴趣时,调用VIEwGroup的requestdisallowIntercepttouchEvent方法,允许父容器去拦截事件。

@OverrIDepublic boolean dispatchtouchEvent(MotionEvent ev) {    switch (ev.getAction()) {        case MotionEvent.ACTION_MOVE:            if (当期VIEw不需要此事件) {                // 允许父容器拦截事件                getParent().requestdisallowIntercepttouchEvent(false);            }            break;        default:            break;    }    return super.dispatchtouchEvent(ev);    }

对子VIEw来说,对事件处理的控制逻辑已经完成了,但是对于父VIEwGroup来说并没有,必须要重写VIEwGroup的onInterceptedtouchEvent方法,让MOVE和UP事件返回true,表示拦截子VIEw不感兴趣的事件,这边父VIEwGroup拦截MOVE事件是可以理解的,但是为什么要拦截UP事件呢,因为父VIEwGroup只有拦截了UP事件才可以接收单击事件。

2.3.3 具体实现

上述分析了原理,现在来真正解决一下ScrollVIEw和ListVIEw的滑动冲突。其实内部拦截的模板已经在伪代码中体现了。只要实现子VIEw 对事件处理的判断即可。我们需要监听ListVIEw滚动到顶部和底部的状态,当ListVIEw滚动到顶部时且手指触摸方向向下或者ListVIEw滚动到底部且手机触摸方向向上,则将事件交由ScrollVIEw处理。

public class MyListVIEw extends ListVIEw implements AbsListVIEw.OnScrollListener {    private boolean isScrollTotop;    private boolean isScrollToBottom;    private int mLastX;    private int mLastY;    public MyListVIEw(Context context) {        this(context, null);    }    public MyListVIEw(Context context, AttributeSet attrs) {        this(context, attrs, -1);    }    public MyListVIEw(Context context, AttributeSet attrs, int defStyleAttr) {        super(context, attrs, defStyleAttr);        init();    }    private voID init() {        setonScrollListener(this);    }    @OverrIDe    public boolean dispatchtouchEvent(MotionEvent ev) {        LogUtils.d("" + Constants.getActionname(ev.getAction()));        switch (ev.getAction()) {            case MotionEvent.ACTION_DOWN:                getParent().requestdisallowIntercepttouchEvent(true);                mLastX = (int) ev.getX();                mLastY = (int) ev.getY();                break;            case MotionEvent.ACTION_MOVE:                if (superdispatchMoveEvent(ev)) {                    getParent().requestdisallowIntercepttouchEvent(false);                }                break;            case MotionEvent.ACTION_UP:                LogUtils.d("ACTION_UP");                break;            default:                break;        }        return super.dispatchtouchEvent(ev);    }    /**     * 将事件交由父容器处理     *     * @param ev     * @return     */    private boolean superdispatchMoveEvent(MotionEvent ev) {        //下滑        boolean canScrollBottom = isScrollTotop && (ev.getY() - mLastY) > 0;        boolean canScrolltop = isScrollToBottom && (ev.getY() - mLastY) < 0;        return canScrollBottom || canScrolltop;    }    @OverrIDe    public voID onScrollStateChanged(AbsListVIEw vIEw, int scrollState) {    }    @OverrIDe    public voID onScroll(AbsListVIEw vIEw, int firstVisibleItem, int visibleItemCount, int totalitemCount) {        isScrollToBottom = false;        isScrollTotop = false;        if (firstVisibleItem == 0) {            androID.vIEw.VIEw firstVisibleItemVIEw = getChildAt(0);            if (firstVisibleItemVIEw != null && firstVisibleItemVIEw.gettop() == 0) {                LogUtils.d("##### 滚动到顶部 ######");                isScrollTotop = true;            }        }        if ((firstVisibleItem + visibleItemCount) == totalitemCount) {            VIEw lastVisibleItemVIEw = getChildAt(getChildCount() - 1);            if (lastVisibleItemVIEw != null && lastVisibleItemVIEw.getBottom() == getHeight()) {                LogUtils.d("##### 滚动到底部 ######");                isScrollToBottom = true;            }        }    }}

至于ScrollVIEw,默认在拖拽状态下会拦截MOVE事件,默认不拦截UP事件,若需要拦截UP事件,可重写ScrollVIEw的onIntercepttouchEvent方法,但不是必须拦截UP事件,若父VIEwGroup不需要触发单击事件,则可以不拦截。

public class MyScrollVIEw extends ScrollVIEw {    public MyScrollVIEw(Context context) {        super(context);    }    public MyScrollVIEw(Context context, AttributeSet attrs) {        super(context, attrs);    }    public MyScrollVIEw(Context context, AttributeSet attrs, int defStyleAttr) {        super(context, attrs, defStyleAttr);    }    @OverrIDe    public boolean onIntercepttouchEvent(MotionEvent ev) {        boolean intercepted  = super.onIntercepttouchEvent(ev);        if (ev.getAction() == MotionEvent.ACTION_UP) {            intercepted = true;        }        return intercepted;    }}

2.3.4 总结

内部拦截法是将事件控制权交给子VIEw,若子VIEw需要事件,则对事件进行处理,不需要则将事件传递给父VIEwGroup,让父VIEwGroup处理。子VIEw通过调用父VIEwGroup的requestdisallowIntercepttouchEvent来干预父VIEwGroup对事件的拦截状况父VIEwGroup不能拦截DOWN事件,至于MOVE或者UP事件的拦截状态要根据具体的情景

好了,到这里两种解决滑动冲突的方式就介绍完了,但要注意的是解决VIEwPager与ListVIEw滑动冲突并不是只能用外部拦截,同样可以使用内部拦截实现,第二个情景也是一样。解决方式并不是绝对的,我们要做的是选择最方便实现的方案。

 

 

 

总结

以上是内存溢出为你收集整理的Android View 滑动冲突解决方式以及原理全部内容,希望文章能够帮你解决Android View 滑动冲突解决方式以及原理所遇到的程序开发问题。

如果觉得内存溢出网站内容还不错,欢迎将内存溢出网站推荐给程序员好友。

欢迎分享,转载请注明来源:内存溢出

原文地址:https://54852.com/web/1056576.html

(0)
打赏 微信扫一扫微信扫一扫 支付宝扫一扫支付宝扫一扫
上一篇 2022-05-25
下一篇2022-05-25

发表评论

登录后才能评论

评论列表(0条)

    保存