
产生滑动冲突的场景主要有两种:
父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 滑动冲突解决方式以及原理所遇到的程序开发问题。
如果觉得内存溢出网站内容还不错,欢迎将内存溢出网站推荐给程序员好友。
欢迎分享,转载请注明来源:内存溢出
微信扫一扫
支付宝扫一扫
评论列表(0条)