View的滑动冲突深入理解

96
uit_Igor
2016.05.05 12:44* 字数 1614

由于这部分的内容涉及的底层知识较多,没有读过源码小伙伴相对比较难以理解。许多小伙伴遇到滑动冲突的时候只是去知其然而不是去知其所以然。其实大家没必要害怕去接触复杂的内容,其实他们也都是由多个细节方面的特点堆积形成的。就此部分的内容谈一谈我自己的看法,期待各位的不吝赐教。

滑动冲突的产生

那么滑动冲突时如何产生的呢?界面中只要在内外俩层同时可以滑动的时候就会产生滑动冲突,导致内外俩层只有一层可以滑动。

场景一:外部和内部俩层滑动方向不一致


内外层的滑动效果可以通过俩种方式来实现,ViewPager + Fragment和ScrollView + Fragment。对于前者来说系统在内部已经解决了滑动冲突,而后者需要手动解决。这就需要小伙伴们了解一些基础的事件分发机制的知识。

场景二:外部和内部俩层滑动方向一致


实现方式同场景一,不同的是因为内外俩层的滑动方向一致,也就是说当手指滑动的时候,系统无法确定用户是想让那一层滑动。进而会导致要么只有一层滑动,要么内外俩层都可以滑动但是比较卡顿。

场景三:主要是针对场景一和二的嵌套


就是针对场景一和场景二的嵌套。其实也没有看起来这么复杂,可以理解为几个冲突的叠加。简单来说就是将其拆分成多个场景二或者场景一来处理。

滑动冲突的解决思路

对于场景一来说,滑动类型(水平,垂直)可以根据滑动路径与水平方向的夹角,或者是水平方向和垂直方向的距离差(dy - dx),或者是水平和垂直方向上的速度差,然后根据滑动是水平滑动还是竖直滑动进一步取决于谁来拦截当前事件。对于场景二,场景三来说比较特殊,无法根据场景一的思路来解决。一般是可以在业务找到一个突破点进行相应的处理。这里将不再赘述。


滑动冲突的解决方式

针对上述的场景一般有俩种方式去解决。分别是外部拦截内部拦截

外部拦截(父容器优先)就是由父容器首先决定是否消耗事件,然后才会传递给子元素。相比内部拦截更简单,也符合View的事件分发机制,是解决滑动冲突的优先选择。通过重写父容器的onInterceptTouchEvent方法实现。针对不同的滑动冲突,只需要修改父容器需要消耗此事件的条件即可,外部拦截的逻辑框架如下:

public boolean onInterceptTouchEvent(MotionEvent event) {

boolean intercepted = false;

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

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

switch(event.getAction()) {

case MotionEvent.ACTION_DOWN: {

intercepted = false;

//如果为true的话子元素将接收不到任何事件

if(!scroller.isFinished()) {

scroller.abortAnimation();

intercepted = true;

}

break;

}

caseMotionEvent.ACTION_MOVE: {

int tempX = x-lastXIntercept;

int tempY = y-lastYIntercept;

//是否是水平滑动

if(Math.abs(tempX) >Math.abs(tempY)) {

intercepted = true;

}else{

intercepted = false;

}

break;

}

case MotionEvent.ACTION_UP: {

intercepted = false;//一般不去拦截UP事件

break;

}

default:

break;

}

lastX = x;

lastY = y;

lastXIntercept = x;

lastYIntercept = y;

returnintercepted;

}

内部拦截(子元素优先)就是父容器默认不去拦截任何事件,所有的事件都传递给子元素,如果子元素需要消耗该事件就直接消耗,否则会通过重写子元素的dispatchTouchEvent方法将事件传递给父容器处理。针对不同的滑动策略只需修改对应的条件即可,内部拦截的逻辑框架如下:

public boolean dispatchTouchEvent(MotionEvent event){

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

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

switch(event.getAction()) {

case MotionEvent.ACTION_DOWN: {

horizontalScrollView.requestDisallowInterceptTouchEvent(true);

break;

}

case MotionEvent.ACTION_MOVE: {

int tempX = x - lastX;

int tempY = y - lastY;

if(Math.abs(tempX) > Math.abs(tempY)) {

horizontalScrollView.requestDisallowInterceptTouchEvent(false);

}

break;

}

case MotionEvent.ACTION_UP: {

break;

}

default:

break;

}

lastX = x;

lastY = y;

return super.dispatchTouchEvent(event);

}

“MotionEvent.ACTION_DOWN: {// 必须返回false,否则后续事件无法传递到子元素”------why?

这个问题涉及到事件分发机制的一些知识。我尽量通俗的去分析,方便大家伙儿的理解。你有没有想过,当你单击一个按钮的时候系统是如何确定被单击的组件的呢?是这样的,事件一旦被触发的话最先传递给当前的Activity,由它的dispatchTouchEvent()来完成事件的分发。具体工作由它内部的Window将事件传递给Decor View(顶级父容器),再由顶级父容器传递给子元素来实现的。对于有使用过标签优化界面的同学一定听说过最外层的那个神秘的FrameLayout。是的,它就是Decor View

对于单个View来说,由于它没有子元素无法再向下传递,所以只能自己决定是否消耗事件。如果设置了OnTouchListener()的话,onTouch()就会被调用否则onTouchEvent()被调用。如果同时提供的话onTouch()会将onTouchEvent()屏蔽掉。如果该组件决定处理事件,则会终止Down事件的分发,并将接下来的所有事件都交给该组件直接进行处理(当然前提是事件可以传递给它)。在执行UP事件的时候,如果在onTouchEvent()中设置onClickListener的话onClick()也会被触发。

对于ViewGroup来说,如果不拦截当前事件的话,就会由子组件继续进行事件的分发。当所有组件都没有消耗事件的时候也就是说所有的onTouchEvent()方法中都返回false,然后就会触发Activity的onTouchEvent()事件来消耗事件。否则,之后全部的事件都由ViewGroup来处理,不会传递给子元素。

日记本
Web note ad 1