前言
做程序開發(fā),基礎(chǔ)很重要。同樣是擰螺絲人家擰出來的可以經(jīng)久不壞,你擰出來的遇到點風(fēng)浪就開始顫抖,可見基本功的重要性。此系列,專門收錄一些看似基礎(chǔ),但是沒那么簡單的小細節(jié),同時提供權(quán)威解決方案。喜歡的同志們點個贊就是對我最大的鼓勵!先行謝過!
網(wǎng)上可能有一些其他文章,提供了解決方案,但是要么就是沒有提供可運行demo,要么就是demo不夠純粹,讓人探索起來受到其他代碼因素的影響,無法專注于當前這個知識點(比如,我只是想了解Activity的生命周期,你把生命周期探究的過程混入到一個很復(fù)雜的大雜燴Demo中,讓人一眼就沒有了閱讀Demo代碼的欲望),所以我覺得有必要做一個專題,用最純粹的方式展示一個坑的解決方案.
正文
記得有一次要使用多個ScrollView嵌套的時候,需要同時讓兩層ScrollView的滑動都能生效。但是,當我直接套了兩層ScrollView之后,發(fā)現(xiàn)內(nèi)層的滑動完全無效了。
研究一番之后發(fā)現(xiàn)解決方案其實非常簡單。
效果
多層ScrollView嵌套.gif
不墨跡,直接給出源碼工程github.
關(guān)鍵代碼
android的事件分發(fā)滑動沖突的基礎(chǔ)知識,這里不再贅述。
兩種解決方案:
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、自定義內(nèi)層 ScrollView的攔截行為,調(diào)用 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);
}
}
原理
先來解決 第一個疑問 不進行上面的處理時內(nèi)部的ScrollView滑動不了呢?
解讀一下
ScrollView的源碼,發(fā)現(xiàn),它重寫了View的onInterceptTouchEvent和onTouchEvent。
image.png
上圖中,我們能在重寫的onInterceptTouchEvent方法中找到兩處return true(true則攔截或者消費,false則放行或不消費,整個事件分發(fā)機制都是這個套路,記住就行了)。
第二處,調(diào)用的是父類,也就是FrameLayout的攔截返回值,一般都會返回false放行,不理會即可。
只看第一處,首先,指定攔截ACTION_MOVE事件,并且還有另一個條件。
mIsBeingDragged - 是否正在拖拽。看看這個值什么時候會變成true,找到下面這個地方(其實還有另一處,在onTouchEvent中,但是現(xiàn)在還沒到事件回傳的時候,所以不用看)
image.png
讓它變成true判定條件為:
if (yDiff > mTouchSlop && (getNestedScrollAxes() & SCROLL_AXIS_VERTICAL) == 0) {
yDiff > mTouchSlop的意思是,Y軸上的滑動距離,要大于設(shè)備規(guī)定的最小滑動距離.
(getNestedScrollAxes() & SCROLL_AXIS_VERTICAL) == 0的意思是,此視圖組的嵌套滾動的當前軸是否是縱向(看了注釋之后理解的,這里不能debug很蛋疼).getNestedScrollAxes()的值應(yīng)該是0,因為搜索了全文,發(fā)現(xiàn)針對mNestedScrollAxes值的變動,在類內(nèi)部就只有賦值為0的情況,而&是與運算,只要有0,就可以斷言整個都是0了,所以==0,成立。
兩者都是true,則進入if。 進入之后:mIsBeingDragged = true;便會執(zhí)行。
當?shù)谝粋€move執(zhí)行之后,mIsBeingDragged已經(jīng)是true。當?shù)诙€move來的時候,ScrollView便會阻攔后面所有的move。 這就是內(nèi)層ScrollView不能滑動的原因。
第二個疑問:為什么自定義外層 scrollView,重寫 onInterceptTouchEvent 直接 return false之后,內(nèi)層就能正常滑動呢,而且手指在內(nèi)層滑動時,外層是不動的?
重寫了
onInterceptTouchEvent直接return false,那原本scrollView的onInterceptTouchEvent過程則不會執(zhí)行?,F(xiàn)在,所有的事件直接透傳,那么內(nèi)層ScrollView就可以收到事件,自然就有了滑動效果。但是,當手指在內(nèi)層滑動時,外層不受影響。這是為何。
答案在ScrollView的onTouchEvent方法內(nèi)(代碼太長,我就不貼全部了)
@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,自然就沒有任何反應(yīng)。
第三個疑問:內(nèi)層攔截 getParent().requestDisallowInterceptTouchEvent(true)到底做了什么,讓外層無法攔截事件?
先看
getParent, 眾所周知,View不是一個獨立個體,它是一個樹形結(jié)構(gòu),有一個parent節(jié)點,也有N個child節(jié)點。這個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);
}
}
可以看到入?yún)ⅲ?code>disallowIntercept的值,改變了全局變量
mGroupFlags的值。并且,這個方法將disallowIntercept的值向父View傳遞。
全局變量mGroupFlags什么時候用到呢?
進入ViewGroup的dispatchTouchEvent方法:
image.png
可以斷定,之前傳入的disallowIntercept入?yún)⒅?,一定可以影響到這里的局部變量boolean disallowIntercept的值,并且如果之前傳入true,這里就會得到true(你問我為什么會斷定?因為這是在書上看到的。。。具體過程涉及到數(shù)字的位運算,賊復(fù)雜,在這里說不清楚,以后做專題的時候再講吧).
如果之前傳入的是true,那么這里就會執(zhí)行else 中的 intercepted = false; 也就是,不會執(zhí)行這個
intercepted = onInterceptTouchEvent(ev);明白了吧? 如果內(nèi)層調(diào)用了requestDisallowInterceptTouchEvent(true),在父view的dispatchTouchEvent中,就不會執(zhí)行onInterceptTouchEvent.
值得一提的是,
requestDisallowInterceptTouchEvent(true)方法內(nèi)部,調(diào)用了mParent.requestDisallowInterceptTouchEvent(disallowIntercept);,讓這個bool值會一直向上傳遞,也就是說,如果一個子view調(diào)用了這個方法,那么它的父,父的父。。。節(jié)點,都不會攔截它的事件。
結(jié)語
閱讀源碼是一個痛苦的過程,隨時隨地會發(fā)現(xiàn)自己的知識盲區(qū)。但是,不讀源碼,就不知道源碼的深淺,就無法進階成高級工(super)程(ma)師(nong),努力吧,騷年!



