前言
做程序开发,基础很重要。同样是拧螺丝人家拧出来的可以经久不坏,你拧出来的遇到点风浪就开始颤抖,可见基本功的重要性。此系列,专门收录一些看似基础,但是没那么简单的小细节,同时提供权威解决方案。喜欢的同志们点个赞就是对我最大的鼓励!先行谢过!
网上可能有一些其他文章,提供了解决方案,但是要么就是没有提供可运行demo
,要么就是demo不够纯粹
,让人探索起来受到其他代码因素的影响,无法专注于当前这个知识点(比如,我只是想了解Activity
的生命周期,你把生命周期探究的过程混入到一个很复杂的大杂烩Demo
中,让人一眼就没有了阅读Demo代码
的欲望),所以我觉得有必要做一个专题,用最纯粹
的方式展示一个坑
的解决方案.
正文
记得有一次要使用多个ScrollView
嵌套的时候,需要同时让两层ScrollView
的滑动都能生效。但是,当我直接套了两层ScrollView
之后,发现内层的滑动完全无效了。
研究一番之后发现解决方案其实非常简单。
效果
多层ScrollView嵌套.gif
不墨迹,直接给出源码工程github.
关键代码
android的事件分发滑动冲突的基础知识,这里不再赘述。
两种解决方案:
1,自定义外层ScrollView
的拦截行为. 重写onInterceptTouchEvent
,直接返回false
,外层不再拦截事件。
public class OutsideScrollView extends ScrollView {
public OutsideScrollView(Context context) {
this(context, null);
}
public OutsideScrollView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public OutsideScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return false;
}
}
2、自定义内层 ScrollView
的拦截行为,调用 getParent().requestDisallowInterceptTouchEvent(true);
不允许外层对它的事件进行拦截.
public class InsideScrollView extends ScrollView {
public InsideScrollView(Context context) {
this(context, null);
}
public InsideScrollView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public InsideScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
//如果我不允许外部拦截我呢?
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
getParent().requestDisallowInterceptTouchEvent(true);
return super.onInterceptTouchEvent(ev);
}
}
原理
先来解决 第一个疑问 不进行上面的处理时内部的ScrollView滑动不了呢?
解读一下
image.pngScrollView
的源码,发现,它重写了View
的onInterceptTouchEvent
和onTouchEvent
。
上图中,我们能在重写的onInterceptTouchEvent
方法中找到两处return true
(true则拦截或者消费,false则放行或不消费,整个事件分发机制都是这个套路,记住就行了
)。
第二处,调用的是父类,也就是FrameLayout的拦截返回值,一般都会返回false放行,不理会即可。
只看第一处,首先,指定拦截ACTION_MOVE事件,并且还有另一个条件。
mIsBeingDragged - 是否正在拖拽。看看这个值什么时候会变成true
,找到下面这个地方(其实还有另一处,在onTouchEvent中,但是现在还没到事件回传的时候,所以不用看)
image.png
让它变成true判定条件为:
if (yDiff > mTouchSlop && (getNestedScrollAxes() & SCROLL_AXIS_VERTICAL) == 0) {
yDiff > mTouchSlop
的意思是,Y轴上的滑动距离,要大于设备规定的最小滑动距离.
(getNestedScrollAxes() & SCROLL_AXIS_VERTICAL) == 0
的意思是,此视图组的嵌套滚动的当前轴是否是纵向(看了注释之后理解的,这里不能debug很蛋疼).getNestedScrollAxes()
的值应该是0
,因为搜索了全文,发现针对mNestedScrollAxes
值的变动,在类内部就只有赋值为0
的情况,而&
是与运算
,只要有0
,就可以断言整个都是0
了,所以==0
,成立。
两者都是true
,则进入if
。 进入之后:mIsBeingDragged = true;
便会执行。
当第一个move执行之后,mIsBeingDragged
已经是true
。当第二个move
来的时候,ScrollView
便会阻拦后面所有的move
。 这就是内层ScrollView
不能滑动的原因。
第二个疑问:为什么自定义外层 scrollView
,重写 onInterceptTouchEvent
直接 return false
之后,内层就能正常滑动呢,而且手指在内层滑动时,外层是不动的?
重写了
onInterceptTouchEvent
直接return false
,那原本scrollView
的onInterceptTouchEvent
过程则不会执行。现在,所有的事件直接透传,那么内层ScrollView
就可以收到事件,自然就有了滑动效果。但是,当手指在内层滑动时,外层不受影响。这是为何。
答案在ScrollView
的onTouchEvent
方法内(代码太长,我就不贴全部了)
@Override
public boolean onTouchEvent(MotionEvent ev) {
initVelocityTrackerIfNotExists();
MotionEvent vtev = MotionEvent.obtain(ev);
final int actionMasked = ev.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
mNestedYOffset = 0;
}
vtev.offsetLocation(0, mNestedYOffset);
switch (actionMasked) {
case MotionEvent.ACTION_DOWN: {
if (getChildCount() == 0) {
return false;
}
.... 省略N 行代码
break;
}
... 省略N 行代码
}
if (mVelocityTracker != null) {
mVelocityTracker.addMovement(vtev);
}
vtev.recycle();
return true;
}
很明确,
ScrollView
的onTouchEvent
,消费掉了除DOWN
之外的所有事件。所以外层ScrollView
收不到move,自然就没有任何反应。
第三个疑问:内层拦截 getParent().requestDisallowInterceptTouchEvent(true)
到底做了什么,让外层无法拦截事件?
先看
getParent
, 众所周知,View不是一个独立个体,它是一个树形结构,有一个parent
节点,也有N
个child
节点。这个getParent
实际上就是得到自己的父View
。
看看ViewGroup
的requestDisallowInterceptTouchEvent
:
@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
// We're already in this state, assume our ancestors are too
return;
}
if (disallowIntercept) {
mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
} else {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
// Pass it up to our parent
if (mParent != null) {
mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
}
}
可以看到入参:
image.pngdisallowIntercept
的值,改变了全局变量mGroupFlags
的值。并且,这个方法将disallowIntercept
的值向父View
传递。
全局变量mGroupFlags
什么时候用到呢?
进入ViewGroup
的dispatchTouchEvent
方法:
可以断定
,之前传入的disallowIntercept
入参值,一定可以影响到这里的局部变量boolean disallowIntercept
的值,并且如果之前传入true
,这里就会得到true
(你问我为什么会断定?因为这是在书上看到的。。。具体过程涉及到数字的位运算,贼复杂,在这里说不清楚,以后做专题的时候再讲吧
).
如果之前传入的是true,那么这里就会执行else 中的 intercepted = false; 也就是,不会执行这个
intercepted = onInterceptTouchEvent(ev);
明白了吧? 如果内层调用了requestDisallowInterceptTouchEvent(true)
,在父view
的dispatchTouchEvent
中,就不会执行onInterceptTouchEvent
.
值得一提的是,
requestDisallowInterceptTouchEvent(true)
方法内部,调用了mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
,让这个bool值会一直向上传递,也就是说,如果一个子view调用了这个方法,那么它的父,父的父。。。节点,都不会拦截它的事件。
结语
阅读源码是一个痛苦的过程,随时随地会发现自己的知识盲区。但是,不读源码,就不知道源码的深浅,就无法进阶成高级工(super)程(ma)师(nong),努力吧,骚年!
网友评论