前言
如題谊路,現(xiàn)在有一種behavior的使用場(chǎng)景:NestedScrollView下面包裹橫向的RecyclerView析二,behavior的滾動(dòng)回調(diào)方法不執(zhí)行霉涨。詳細(xì)可見(jiàn)demo, 建議最好clone下來(lái)自己試一試茂嗓,因?yàn)槟憧傆幸惶鞎?huì)用到behavior钥勋!
看看問(wèn)題
-
先來(lái)看看demo的布局層級(jí)
CoordinatorLayout包含兩個(gè)子View: Viewpager和View(注入behavior關(guān)聯(lián)滾動(dòng)的view) -
再看看viewpager_item
里面是一層NestedScrollView炬转,里面包含幾個(gè)子Linear, Linear里面包裹橫向的RecyclerView
- 最終層級(jí)圖
這個(gè)層級(jí)還是簡(jiǎn)化后的demo的,實(shí)際開(kāi)發(fā)中我們遇到的情況比這個(gè)更加復(fù)雜算灸,但是就算層級(jí)再多再?gòu)?fù)雜扼劈,只要符合behavior的使用規(guī)則,那么一切皆可以實(shí)現(xiàn)菲驴。
- 再看看behavior
public class MyBehavior extends CoordinatorLayout.Behavior<View> {
private boolean isHide = false;
public MyBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View directTargetChild, @NonNull View target, int axes, int type) {
return true;
}
@Override
public void onNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) {
Log.e("test", "onNS");
if(dyConsumed >0 ) {
if (!isHide) {
child.offsetTopAndBottom(child.getHeight());
isHide = true;
}
}else {
if(isHide){
child.offsetTopAndBottom(-child.getHeight());
isHide = false;
}
}
}
}
也超級(jí)簡(jiǎn)單就是判斷一下滾動(dòng)方向荐吵,然后顯示和隱藏bottomView而已。
但是
我們這樣簡(jiǎn)單的代碼卻有著問(wèn)題,我們實(shí)際運(yùn)行發(fā)現(xiàn)先煎,貌似滾動(dòng)的關(guān)聯(lián)“不太靈敏”贼涩,打log發(fā)現(xiàn),有時(shí)候onNestedScroll方法不會(huì)調(diào)用薯蝎。這是為什么呢遥倦?
問(wèn)題
于是提出兩個(gè)問(wèn)題:
1、為什么onNestedScroll方法不會(huì)調(diào)用占锯?
2袒哥、為什么讓RecyclerView設(shè)置setNestedScrollingEnable(false)就能夠正常使用?
另外后面會(huì)進(jìn)行更深層次的源碼分析消略,附加幾個(gè)問(wèn)題:
1堡称、對(duì)于如果onIntercept返回true攔截了,交給onTouchEvent去處理艺演,具體體現(xiàn)在何處却紧?
2、判斷子View是否能夠接收事件從哪里體現(xiàn)钞艇?
3啄寡、另外一個(gè)比較重要的方法dispatchTransformedTouchEvent干什么用的?
4哩照、viewGroup和view的dispatch返回false挺物,會(huì)直接回溯到parent的onTouchEvent,這個(gè)又在哪里體現(xiàn)飘弧?
5识藤、viewGroup重寫(xiě)了dispatch但是沒(méi)有調(diào)用super, 那么它在哪里調(diào)用自己的onTouch的呢?
正題
首先我們解決第一個(gè)問(wèn)題次伶,“為什么onNestedScroll沒(méi)有調(diào)用痴昧?”
這需要大家對(duì)behavior有一定的了解,我們都知道coordinatorLayout和behavior聯(lián)合使用可以實(shí)現(xiàn)許多花哨的效果冠王,很牛逼赶撰。
behavior的工作原理就是:
1、coordinatorLayout下面的所有子view(包含子孫view),實(shí)現(xiàn)了滾動(dòng)接口(包括NestedScrollingChild柱彻、NestedScrollingParent等等)的view, 如果有滑動(dòng)事件的消耗豪娜,就會(huì)一層一層向上傳遞,直到coordinatorLayout
2哟楷、然后coordinatorLayout再對(duì)注入了behavior的子View傳遞滾動(dòng)回調(diào)事件瘤载,這樣,behavior就能拿到滾動(dòng)的值卖擅,進(jìn)而進(jìn)行對(duì)View的一些關(guān)聯(lián)滾動(dòng)操作
如果用最通俗的例子來(lái)講就是:
父親是CoordinatorLayout鸣奔,它有兩個(gè)兒子墨技,一個(gè)是NestedScrollView,一個(gè)是BottomView挎狸,behavior綁在BottomView身上(父親比較偏愛(ài)他)扣汪。NestedScrollView發(fā)年終獎(jiǎng)了(滾動(dòng)了),發(fā)了紅包給父親(通知給了父親)伟叛,然后父親又把錢(qián)分給了喜愛(ài)的兒子BottomView(父親又通知了BottomView)
貼點(diǎn)重要代碼
recycler->linear->nestedScroll->coordinator:
為什么onNestedScroll沒(méi)有回調(diào)呢?
PS: 這里的源碼是對(duì)應(yīng)26的统刮,support是26.1
通過(guò)在代碼里面打斷點(diǎn)發(fā)現(xiàn):
RecyclerView中自己消費(fèi)了consumedY,uncomsumed = y - consumeY = 0 账千,然后
NestedScrollView中拿到的dyUnConsumed為0侥蒙,調(diào)用dispatchNestedScroll方法也就傳入0
這樣的話(huà),NestedScrollingChildHelper中if分支進(jìn)不去匀奏,就沒(méi)法向上層傳遞消費(fèi)的y值(相當(dāng)于它并沒(méi)有滾動(dòng)),ViewParentCompat.onNestedScroll沒(méi)法調(diào)用,所以沒(méi)能傳遞到頂層的CoordinatorLayout副签,自然behavior里面也不會(huì)收到回調(diào)了奶是。
從源碼上來(lái)看是這樣的,如果從宏觀上來(lái)講聚磺,其實(shí)就是RecyclerView和NestedScrollView的事件處理有沖突坯台,RecyclerView消費(fèi)了事件,從而NestedScrollView沒(méi)能把自己消費(fèi)的事件往上傳遞瘫寝。
按道理蜒蕾,我們都知道,如果豎向的RecyclerView和NestedScrollView或者ScrollView聯(lián)合使用的話(huà)(雖然焕阿,這樣聯(lián)合使用沒(méi)有意義咪啡,也不建議這樣做),會(huì)出現(xiàn)事件沖突暮屡。但是撤摸,橫向的RecyclerView和NestedScrollView一起使用,在事件處理上面是沒(méi)有問(wèn)題的褒纲,沒(méi)有沖突准夷,但是,在使用到behavior外厂,希望nestedScrollView能夠把自己滾動(dòng)消費(fèi)的事件往上傳遞的時(shí)候就會(huì)出問(wèn)題了冕象。
(我們都希望behavior的使用是在沒(méi)有嵌套滾動(dòng)沖突的情況下,兄弟滾動(dòng)汁蝶,然后父親知道渐扮,父親通知另外一個(gè)兄弟做出相應(yīng)的行為论悴,而如果是子孫滾動(dòng),往上傳給父親墓律,這期間出了問(wèn)題膀估,就沒(méi)法正常工作了)
接著第二個(gè)問(wèn)題,“為什么讓RecyclerView設(shè)置setNestedScrollingEnable(false)就能夠正常使用耻讽?”
看看效果
第二個(gè)問(wèn)題就需要大家對(duì)于事件分發(fā)機(jī)制有一定的了解察纯,這里就大致貼張圖。
另外针肥,貼幾個(gè)認(rèn)為比較不錯(cuò)的鏈接:
1饼记、圖解 Android 事件分發(fā)機(jī)制
2、Android事件分發(fā)機(jī)制詳解:史上最全面慰枕、最易懂
3具则、Android6.0源碼解讀之View點(diǎn)擊事件分發(fā)機(jī)制
4、Android 事件分發(fā)機(jī)制-試著讀懂每一行源碼-View
5具帮、ScrollView與頭+RecycleView嵌套沖突源碼分析
我們看看setNestedScrollingEnabled
// RecyclerView
@Override
public void setNestedScrollingEnabled(boolean enabled) {
getScrollingChildHelper().setNestedScrollingEnabled(enabled);
}
調(diào)用了輔助類(lèi)
// NestedScrollingChildHelper
public void setNestedScrollingEnabled(boolean enabled) {
if (mIsNestedScrollingEnabled) {
ViewCompat.stopNestedScroll(mView);
}
mIsNestedScrollingEnabled = enabled;
}
輔助類(lèi)設(shè)置mIsNestedScrollingEnabled為false博肋,并且調(diào)用了 ViewCompat.stopNestedScroll(mView);傳入了自己
// NestedScrollingChildHelper
public boolean isNestedScrollingEnabled() {
return mIsNestedScrollingEnabled;
}
這樣isNestedScrollingEnabled返回false了,以后behavior的回調(diào)方法里面的if(isNestedScrollingEnabled())就進(jìn)不去了
接著:
//ViewCompat
public static void stopNestedScroll(@NonNull View view) {
IMPL.stopNestedScroll(view);
}
這里蜂厅,ViewCompat就是一個(gè)兼容類(lèi)匪凡,兼容各個(gè)版本api的使用,因?yàn)橛幸恍┬掳姹镜腶pi掘猿,實(shí)現(xiàn)的是NestedScrollingParent2等方法病游。
//ViewCompat
public void stopNestedScroll(View view) {
if (view instanceof NestedScrollingChild) {
((NestedScrollingChild) view).stopNestedScroll();
}
}
這里是相當(dāng)于調(diào)用view的stopNestedScroll,也就是RecyclerView的术奖。
//RecyclerView
@Override
public void stopNestedScroll() {
getScrollingChildHelper().stopNestedScroll();
}
//NestedScrollingChildHelper
public void stopNestedScroll() {
stopNestedScroll(TYPE_TOUCH);
}
//NestedScrollingChildHelper
public void stopNestedScroll(@NestedScrollType int type) {
ViewParent parent = getNestedScrollingParentForType(type);
if (parent != null) {
ViewParentCompat.onStopNestedScroll(parent, mView, type);
setNestedScrollingParentForType(type, null);
}
}
這里就比較重要了礁遵,這里通過(guò)getNestedScrollingParenForType獲得了parent,然后調(diào)用了ViewParentCompat.onStopNestedScroll(parent, mView, type);
//viewParentCompat
public static void onStopNestedScroll(ViewParent parent, View target, int type) {
if (parent instanceof NestedScrollingParent2) {
// First try the NestedScrollingParent2 API
((NestedScrollingParent2) parent).onStopNestedScroll(target, type);
} else if (type == ViewCompat.TYPE_TOUCH) {
// Else if the type is the default (touch), try the NestedScrollingParent API
IMPL.onStopNestedScroll(parent, target);
}
}
這個(gè)方法會(huì)又調(diào)用IMPL.onStopNestedScroll(parent, target);這樣類(lèi)似的方法其實(shí)就是把事件一層一層往上傳采记,當(dāng)然佣耐,其他onPreNestedScroll、onNestedScroll這些也都是這樣的唧龄。
//NestesScrollView
@Override
public void onStopNestedScroll(View target) {
mParentHelper.onStopNestedScroll(target);
stopNestedScroll();
}
我們又看NestedScrollView里面的onStopNestedScroll
stopNestedScroll();
是繼續(xù)往上調(diào)用傳遞
mParentHelper.onStopNestedScroll(target);
就比較關(guān)鍵了
//NestedScrollingParentHelper
public void onStopNestedScroll(@NonNull View target) {
onStopNestedScroll(target, ViewCompat.TYPE_TOUCH);
}
public void onStopNestedScroll(@NonNull View target, @NestedScrollType int type) {
mNestedScrollAxes = 0;
}
這個(gè)就關(guān)鍵了兼砖,mNestedScrollAxes = 0
return mNestedScrollAxes;
}
這個(gè)方法返回0了,看看它在哪被調(diào)用
//NestedScrollVIew#onIntercept#move
final int yDiff = Math.abs(y - mLastMotionY);
if (yDiff > mTouchSlop
&& (getNestedScrollAxes() & ViewCompat.SCROLL_AXIS_VERTICAL) == 0) {
mIsBeingDragged = true;
mLastMotionY = y;
initVelocityTrackerIfNotExists();
mVelocityTracker.addMovement(ev);
mNestedYOffset = 0;
final ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
}
break;
在move的時(shí)候既棺,它返回為0讽挟,那么走入分支的話(huà),mIsBeingDragged =true
onInterceptTouchEvent就返回true, 就會(huì)攔截了丸冕。
這就說(shuō)明耽梅,在move的時(shí)候,nestedScrollView就完全攔截了事件胖烛,里面的子孫view(包括橫向的RecyclerView就不會(huì)有事件了眼姐,更不用談什么它自己消費(fèi)掉了consumeY诅迷,NestedScrollView自己全權(quán)處理了),這樣的話(huà)它自己的滾動(dòng)事件就能夠再往上一直傳遞到coordinatorLayout众旗,然后behavior也就肯定能夠執(zhí)行回到方法了罢杉!
啊,原來(lái)如此贡歧,恍然大悟滩租!
5個(gè)小問(wèn)題
前面兩個(gè)大問(wèn)題終于解決了,下面來(lái)搞清楚后面提的那5個(gè)小問(wèn)題利朵。
1律想、對(duì)于如果onIntercept返回true攔截了,交給onTouchEvent去處理绍弟,具體體現(xiàn)在何處蜘欲?
2、判斷子View是否能夠接收事件從哪里體現(xiàn)晌柬?
3、另外一個(gè)比較重要的方法dispatchTransformedTouchEvent干什么用的郭脂?
4年碘、viewGroup和view的dispatch返回false,會(huì)直接回溯到parent的onTouchEvent展鸡,這個(gè)又在哪里體現(xiàn)屿衅?
5、viewGroup重寫(xiě)了dispatch但是沒(méi)有調(diào)用super, 那么它在哪里調(diào)用自己的onTouch的呢莹弊?
這幾個(gè)問(wèn)題全是關(guān)于事件分發(fā)的涤久,大家可以把那幾個(gè)鏈接的文章都看了,如果還不能解決忍弛,那么再往下看响迂。
//ViewGroup#dispatchTouchEvent 代碼有省略
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean handled = false;
// Handle an initial down.
if (actionMasked == MotionEvent.ACTION_DOWN) {
##### 重點(diǎn)
// 這里mFirstTouchTarget置為null
cancelAndClearTouchTargets(ev);
resetTouchState();
}
// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
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 {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
##### 重要 if分支1
if (!canceled && !intercepted) {
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
// If there is a view that has accessibility focus we want it
// to get the event first and if not handled we will perform a
// normal dispatch. We may do a double iteration but this is
// safer given the timeframe.
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
##### 重點(diǎn)
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
resetCancelNextUpFlag(child);
##### 重點(diǎn)
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
// childIndex points into presorted list, find original index
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
##### 重點(diǎn)
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
// The accessibility focus didn't handle the event, so clear
// the flag and do a normal dispatch to all children.
ev.setTargetAccessibilityFocus(false);
}
if (preorderedList != null) preorderedList.clear();
}
}
// Dispatch to touch targets.
##### 重要 if分支2
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
// Dispatch to touch targets, excluding the new touch target if we already
// dispatched to it. Cancel touch targets if necessary.
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
##### 重點(diǎn)
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;
}
}
...
return handled;
}
//ViewGroup#dispatchTransformedTouchEvent 代碼有省略
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
final MotionEvent transformedEvent;
if (newPointerIdBits == oldPointerIdBits) {
if (child == null || child.hasIdentityMatrix()) {
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
return handled;
}
transformedEvent = MotionEvent.obtain(event);
} else {
transformedEvent = event.split(newPointerIdBits);
}
// Perform any necessary transformations and dispatch.
if (child == null) {
handled = super.dispatchTouchEvent(transformedEvent);
} else {
handled = child.dispatchTouchEvent(transformedEvent);
}
// Done.
transformedEvent.recycle();
return handled;
}
1 、對(duì)于如果onIntercept返回true攔截了细疚,交給onTouchEvent去處理蔗彤,具體體現(xiàn)在何處?
如果onIntercep返回true疯兼,那么interceped變量為true然遏,那么不會(huì)走入【重要 if分支1】(里面分發(fā)事件,設(shè)置mFirstTouchTarget等)吧彪,mFirstTouchTarget依舊為null, 于是走入【重要 if分支2】的dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS)
并且傳入child為null,
在dispatchTransformedTouchEvent中如果child為null,就會(huì)走super.dispatch, super就是view待侵,這樣的話(huà),就會(huì)走view的dispatch(view本身的dispatch會(huì)調(diào)onTouch)姨裸,就會(huì)走到onTouch去了
2秧倾、判斷子View是否能夠接收事件從哪里體現(xiàn)怨酝?
在viewgroup的dispatch中的走入if分支之后,里面有個(gè)判斷
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
有個(gè)方法canViewReceivePointerEvents中狂,里面主要是判斷是否Visible
isTransformedTouchPointInView主要是判斷事件的位置是否在子VIew的區(qū)域內(nèi)
如果不行凫碌,就continue,后續(xù)的事件分發(fā)就不進(jìn)行
3胃榕、另外一個(gè)比較重要的方法dispatchTransformedTouchEvent干什么用的盛险?
dispatchTransformedTouchEvent主要就是對(duì)于事件分發(fā)的處理,比如什么時(shí)候調(diào)用自己的super.dispatch勋又,什么時(shí)候調(diào)用child.disaptch分發(fā)給子View, 這個(gè)判斷方法的主要根據(jù)就是child是否為null, 而這個(gè)又跟mFirstTouchTarget有關(guān)聯(lián)
4苦掘、viewGroup和view的dispatch返回false,會(huì)直接回溯到parent的onTouchEvent楔壤,這個(gè)又在哪里體現(xiàn)鹤啡?
這個(gè)問(wèn)題也跟第1個(gè)問(wèn)題有點(diǎn)類(lèi)似,如果子View的dispatch返回false蹲嚣,那么dispatchTransformedTouchEvent的handled就會(huì)是false返回递瑰,然后【重要 if分支1】就走不進(jìn)去,addTouchTarget這個(gè)方法也不執(zhí)行(主要是給mFirstTarget賦值)
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
前面說(shuō)了隙畜,如果mFirstTarget為null, 【重要 if分支2】就會(huì)進(jìn)入dispatchTransformedTouchEvent的時(shí)候傳入為null的child, 這樣就會(huì)調(diào)用super.dispatch抖部,就是view的dispatch,然后就調(diào)用了onTouch咯
viewGroup重寫(xiě)了dispatch但是沒(méi)有調(diào)用super, 那么它在哪里調(diào)用自己的onTouch的呢议惰?
如果看到這慎颗,希望第5個(gè)問(wèn)題我已經(jīng)不用解釋了,因?yàn)榍懊?個(gè)問(wèn)題已經(jīng)把它囊括在內(nèi)了言询。
最后
為了解決這個(gè)問(wèn)題俯萎,最近一直在源碼的黑洞里遨游,打了N個(gè)斷點(diǎn)來(lái)回跳运杭,梳理邏輯夫啊。最后一句,最終想要深刻地理解事件分發(fā)機(jī)制县习、behavior機(jī)制這些玩意兒涮母,RTFSC。