上一篇深入解析AndroidDesign包——NestedScroll 已經(jīng)說過了,在AndroidDesign包中主要有兩個核心概念:一是NestedScroll,另一個就是Behavior。
相比于NestedScroll這個概念來說,Behavior分析起來會難很多,因為它幾乎遍布了AndroidDesign包的每一個控件,種類繁多;另外Behavior提供了二十多個空方法給使用者來重寫,主要分為四類:
1.與Touch事件相關(guān)的方法
2.與NestedScroll相關(guān)的方法
3.與控件依賴相關(guān)的方法(依賴這個概念可能接觸的不多,就是如果A依賴B,那么當(dāng)B變化時會通知A跟著變化)
4.其他方法,如測量和布局等
由此可見,Behavior的使用是非常靈活的,所以功能也是非常的強(qiáng)大。但是,對于越靈活的東西,就越難將它講清除。它有一百種用法,總不能我就舉出一百個例子來進(jìn)行說明,因此本文只能起到一個拋磚引玉的作用,要真正融會貫通還得靠各位自己去揣摩。
從CoordinatorLayout入手
好好的干嘛扯到CoordinatorLayout呢?
如果你這么問那你就外行了,因為如果沒有CoordinatorLayout,光有Behavior是啥用都沒有滴。
CoordinatorLayout就是一個容器,主要功能就是為它里面的控件傳遞命令,更準(zhǔn)確的說就是使用Behavior來讓子控件們相互調(diào)用。
CoordinatorLayout有自己的LayoutParams類
public static class LayoutParams extends ViewGroup.MarginLayoutParams {
/**
* A {@link Behavior} that the child view should obey.
*/
Behavior mBehavior;
它的布局參數(shù)類定義的第一個屬性就是Behavior,而且還有l(wèi)ayout_behavior屬性供布局文件使用,可在布局文件中為CoordinatorLayout內(nèi)部的控件設(shè)置behavior對象。
另外,所有的Behavior的祖宗都是CoordinatorLayout.Behavior,這是一個靜態(tài)-內(nèi)部-虛擬類,頭銜有點長~ 我們抓住這個靜態(tài)內(nèi)部類就算是抓到Behavior的精髓了。
除了以上兩點,最重要的一層關(guān)系是:所有Behavior的方法都是在CoordinatorLayout中調(diào)用的,比如來了個NestedScroll事件,那么CoordinatorLayout會調(diào)用自己的onNestedScroll()方法,然后在方法內(nèi)部,就會調(diào)用childView的behavior對應(yīng)的onNestedScroll()方法了。
具體過程,我們來詳細(xì)分析。
如何處理Touch相關(guān)事件
找到CoordinatorLayout中的Behavior類,可以發(fā)現(xiàn)該類中定義了如下兩個方法:
public boolean onInterceptTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev) {
return false;
}
public boolean onTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev) {
return false;
}
很顯然,這是攔截觸摸事件和處理觸摸事件的方法。
我們看看這兩個方法是如何被CoordinatorLayout調(diào)用的。
先看onInterceptTouchEvent
根據(jù)View的事件體系可知,對事件是否攔截的處理在onInterceptTouchEvent()方法中,于是找到CoordinatorLayout的這個方法:
public boolean onInterceptTouchEvent(MotionEvent ev) {
MotionEvent cancelEvent = null;
final int action = MotionEventCompat.getActionMasked(ev);
// Make sure we reset in case we had missed a previous important event.
if (action == MotionEvent.ACTION_DOWN) {
resetTouchBehaviors();
}
//這里才是處理事件攔截的代碼
final boolean intercepted = performIntercept(ev, TYPE_ON_INTERCEPT);
if (cancelEvent != null) {
cancelEvent.recycle();
}
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
resetTouchBehaviors();
}
return intercepted;
}
從上面代碼可知,真正的處理邏輯在performIntercept()方法中,注意它的第二個參數(shù)TYPE_ON_INTERCEPT。然后再看performIntercept方法:
private boolean performIntercept(MotionEvent ev, final int type) {
...
//遍歷所有顯示出來了的childView
for (int i = 0; i < childCount; i++) {
final View child = topmostChildList.get(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final Behavior b = lp.getBehavior();
if ((intercepted || newBlock) && action != MotionEvent.ACTION_DOWN) {
// 如果事件已經(jīng)被攔截,那么向其他childView發(fā)送cancelEvent
//...省略代碼...
continue;
}
if (!intercepted && b != null) {
switch (type) {
//如果事件未攔截,且childView設(shè)置了behavior,則進(jìn)行攔截判斷
case TYPE_ON_INTERCEPT:
intercepted = b.onInterceptTouchEvent(this, child, ev);
break;
case TYPE_ON_TOUCH:
intercepted = b.onTouchEvent(this, child, ev);
break;
}
if (intercepted) {
mBehaviorTouchView = child;
}
}
...
}
topmostChildList.clear();
return intercepted;
}
對代碼進(jìn)行簡化之后,邏輯就很明顯了。先從childView中取出LayoutParams對象,然后從LayoutParams對象中取出Behavior對象,如果performIntercept()方法第二個參數(shù)傳進(jìn)來的是TYPE_ON_INTERCEPT,則調(diào)用behavior.onInterceptTouchEvent()方法判斷是否攔截事件。換句話說就是,是否攔截事件跟CoordinatorLayout本身沒有一毛錢關(guān)系。
再看onTouchEvent
直接看CoordinatorLayout中的onTouchEvent()方法源碼:
public boolean onTouchEvent(MotionEvent ev) {
boolean handled = false;
boolean cancelSuper = false;
MotionEvent cancelEvent = null;
final int action = MotionEventCompat.getActionMasked(ev);
//先判斷mBehaviorTouchView是否為null,如果不為null,則不會執(zhí)行后面的performIntercept()
//如果等于null,則調(diào)用performIntercept方法,該方法如果返回true會對mBehaviorTouchView賦值
if (mBehaviorTouchView != null
|| (cancelSuper = performIntercept(ev, TYPE_ON_TOUCH))) {
final LayoutParams lp = (LayoutParams) mBehaviorTouchView.getLayoutParams();
final Behavior b = lp.getBehavior();
if (b != null) {
handled = b.onTouchEvent(this, mBehaviorTouchView, ev);
}
}
// 經(jīng)過上面的兩重判斷之后,如果mBehavior還是null,則說明childView不消費(fèi)touch事件
// 那么該touch事件交給CoordinatorLayout的parent去處理
if (mBehaviorTouchView == null) {
handled |= super.onTouchEvent(ev);
}
...
return handled;
}
上面的代碼加了注釋之后,應(yīng)該不需要多說什么了。
還是那句話,CoordinatorLayout本身也不會消費(fèi)touch事件。
如何處理NestedScroll相關(guān)事件
先看Behavior中跟NestedScroll相關(guān)的方法
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout,
V child, View directTargetChild, View target, int nestedScrollAxes) {
return false;
}
public void onNestedScrollAccepted(CoordinatorLayout coordinatorLayout, V child,
View directTargetChild, View target, int nestedScrollAxes) {
// Do nothing
}
public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target) {
// Do nothing
}
public void onNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target,
int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
// Do nothing
}
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, V child, View target,
int dx, int dy, int[] consumed) {
// Do nothing
}
public boolean onNestedFling(CoordinatorLayout coordinatorLayout, V child, View target,
float velocityX, float velocityY, boolean consumed) {
return false;
}
public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, V child, View target,
float velocityX, float velocityY) {
return false;
}
大家不要被這么多方法給嚇到了,如果你有閱讀這篇文章深入解析Android Design包——NestedScroll 就能夠發(fā)現(xiàn),這些方法都是NestedScrollingParent接口中定義的方法。并且CoordinatorLayout本身是實現(xiàn)了NestedScrollingParent接口的,那么CoordinatorLayout會如何調(diào)用Behavior的這些方法呢? 肯定是一一對應(yīng)的來調(diào)用。
我想Google這么設(shè)計的目的應(yīng)該是為了解耦,只要給控件提供一個Behavior就可以擁有NestedScrollingParent的功能,這樣一來控件本身就與NestedScrollingParent完全無關(guān)了。
由于方法比較多,這里就不一一展示調(diào)用過程了,挑onNestedScroll方法來說一下吧。
先看CoordinatorLayout中的onNestedScroll方法:
@Override
public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed) {
final int childCount = getChildCount();
boolean accepted = false;
for (int i = 0; i < childCount; i++) {
final View view = getChildAt(i);
if (view.getVisibility() == GONE) {
continue;
}
final LayoutParams lp = (LayoutParams) view.getLayoutParams();
if (!lp.isNestedScrollAccepted()) {
continue;
}
//獲取childView的behavior,并調(diào)用behavior的onNestedScroll方法
final Behavior viewBehavior = lp.getBehavior();
if (viewBehavior != null) {
viewBehavior.onNestedScroll(this, view, target, dxConsumed, dyConsumed,
dxUnconsumed, dyUnconsumed);
accepted = true;
}
}
if (accepted) {
onChildViewsChanged(EVENT_NESTED_SCROLL);
}
}
代碼邏輯非常清晰,就是直接把nestedScroll事件通過behavior傳遞給childView去處理。
但是,我們注意到最后一段代碼,調(diào)用了一個onChildViewsChanged()方法。
這個方法具體邏輯我們在下一小結(jié)分析,它主要是處理那些依賴控件的。之所以在此處加一句,是為了那些跟滑動控件存在依賴關(guān)系的其他控件,也可以做出響應(yīng)。
如何處理依賴相關(guān)事件
接下來,我們來看看Behavior中依賴相關(guān)的方法
//判斷child和dependency是否存在依賴關(guān)系
public boolean layoutDependsOn(CoordinatorLayout parent, V child, View dependency) {
return false;
}
//dependency發(fā)生改變時,回調(diào)此方法
public boolean onDependentViewChanged(CoordinatorLayout parent, V child, View dependency) {
return false;
}
//dependency被移除時,回調(diào)此方法
public void onDependentViewRemoved(CoordinatorLayout parent, V child, View dependency) {
}
要完成上面三個方法的使命,需要滿足兩點:
1.需要對CoordinatorLayout所有childView進(jìn)行兩兩判斷,看它們是否存在依賴關(guān)系。
2.當(dāng)一個childView發(fā)生布局改變時,CoordinatorLayout需要回調(diào)通知與其有依賴關(guān)系的其他childView。
判斷依賴關(guān)系
一個View在Android系統(tǒng)中的顯示都是:onMeasure, onLayout, onDraw
所以先看onMeasure:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
prepareChildren();
...
}
沒想到第一句就看到重點了,prepareChildren就是為了整理CoordinatorLayout內(nèi)部的childView,自然也會將childView之間的依賴關(guān)系確定好,來看代碼:
private void prepareChildren() {
mDependencySortedChildren.clear(); //List<View>
mChildDag.clear(); //圖結(jié)構(gòu) --- 無回路有向圖
//遍歷所有childView
for (int i = 0, count = getChildCount(); i < count; i++) {
final View view = getChildAt(i);
final LayoutParams lp = getResolvedLayoutParams(view);
lp.findAnchorView(this, view);
mChildDag.addNode(view);
//再次遍歷,需要雙重遍歷才能將childView兩兩判斷dependency
//將判斷的結(jié)果保存在有向圖mChildDag中
for (int j = 0; j < count; j++) {
if (j == i) {
continue;
}
final View other = getChildAt(j);
final LayoutParams otherLp = getResolvedLayoutParams(other);
//這個dependsOn方法就是判斷依賴關(guān)系的,內(nèi)部會調(diào)用Behavior.layoutDependsOn()方法
if (otherLp.dependsOn(this, other, view)) {
if (!mChildDag.contains(other)) {
mChildDag.addNode(other);
}
mChildDag.addEdge(view, other);
}
}
}
//將圖中的數(shù)據(jù)排序,并保存在List中
mDependencySortedChildren.addAll(mChildDag.getSortedList());
//將List倒序設(shè)置,讓被依賴的childView排在前面,依賴于它的排在后面
Collections.reverse(mDependencySortedChildren);
}
這里涉及到一種比較復(fù)雜的數(shù)據(jù)結(jié)構(gòu)——無回路有向圖,篇幅有限就不在這里多說,我們只要知道會調(diào)用layoutDependsOn方法來判斷依賴關(guān)系,然后將數(shù)據(jù)最后保存在mDependencySortedChildren這個List中。
這個mDependencySortedChildren列表中保存的都是childView,不過是按照特定的順序進(jìn)行了排序:
如果childView被其他view依賴的次數(shù)最多,則排在最前面,以此類推。
至于依賴關(guān)系并沒有保存,到時候要用到時,再次調(diào)用layoutDependsOn方法來判斷,寫到這里我好像明白了為什么要將依賴次數(shù)多的放列表前面了。
childView發(fā)生布局改變
OK,依賴關(guān)系確定了,那就看看當(dāng)childView發(fā)生改變時,如何讓依賴的view跟著改變。
其實在NestedScroll相關(guān)方法中,最后都會調(diào)用一句代碼
if (accepted) {
onChildViewsChanged(EVENT_NESTED_SCROLL);
}
上面也有提到,這個方法就是處理依賴控件的變化的。在分析它之前,還有必要看看其他地方有沒有調(diào)用此方法。
然后,就看到了在onAttachToWindow()方法中,為CoordinatorLayout設(shè)置了OnPreDrawListener 回調(diào),也就是說在執(zhí)行onDraw之前,回執(zhí)行onPreDraw方法中的代碼。
我們先來看OnPreDrawListener:
class OnPreDrawListener implements ViewTreeObserver.OnPreDrawListener {
@Override
public boolean onPreDraw() {
onChildViewsChanged(EVENT_PRE_DRAW);
return true;
}
}
onPreDraw()的代碼很簡單,就是執(zhí)行onChildViewsChanged()方法,也就是說每次onDraw都會調(diào)用這個方法來處理依賴控件。
接下來重點看onChildViewsChanged()方法了。
final void onChildViewsChanged(@DispatchChangeEvent final int type) {
final int childCount = mDependencySortedChildren.size();
for (int i = 0; i < childCount; i++) {
final View child = mDependencySortedChildren.get(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
// ... 省略了對anchor和inset相關(guān)的代碼 ...
//可以說,上面的代碼都是為了提升效率,下面的才是真正的處理邏輯
for (int j = i + 1; j < childCount; j++) {
final View checkChild = mDependencySortedChildren.get(j);
final LayoutParams checkLp = (LayoutParams) checkChild.getLayoutParams();
//獲取對應(yīng)的Behavior對象
final Behavior b = checkLp.getBehavior();
//判斷依賴關(guān)系
if (b != null && b.layoutDependsOn(this, checkChild, child)) {
if (type == EVENT_PRE_DRAW && checkLp.getChangedAfterNestedScroll()) {
//當(dāng)NestedScroll 和 Draw都會觸發(fā)這個方法,
//這里二者只能有一個繼續(xù)往下執(zhí)行
checkLp.resetChangedAfterNestedScroll();
continue;
}
final boolean handled;
switch (type) {
case EVENT_VIEW_REMOVED:
// 調(diào)用Behavior對應(yīng)的方法,通知依賴這個child的其他view
//這里的child的被依賴,checkChild是依賴于它
b.onDependentViewRemoved(this, checkChild, child);
handled = true;
break;
default:
// 除了刪除事件,其他的都調(diào)用下面的方法
handled = b.onDependentViewChanged(this, checkChild, child);
break;
}
if (type == EVENT_NESTED_SCROLL) {
//這里跟上面的checkLp.getChangedAfterNestedScroll()對應(yīng)
checkLp.setChangedAfterNestedScroll(handled);
}
}
}
}
...
}
同樣的,為了突出處理邏輯,把一些代碼省略掉了。
代碼不長,希望大家根據(jù)我的注釋讀一下代碼,自然就明白了。
其實,onChildViewsChanged這個方法也只是在調(diào)用Behavior的相關(guān)方法而已,也就是說如果childA依賴于childB,那么當(dāng)childB發(fā)生布局變化時,childB的Behavior就會把這個變化同時作用到childA身上。
結(jié)語
說到這里,一句話總結(jié)CoordinatorLayout本身啥事兒也不干,全讓底下的childView去干了。
而Behavior之所以強(qiáng)大,是因為在我們不修改View的情況下,可以對View的行為進(jìn)行修改和控制。
本來是要舉例說明一下Behavior的具體用法的,但是沒想到一寫一寫已經(jīng)這么長了,太長的文章不利于閱讀,也不利于學(xué)習(xí)吸收,就只能把舉例說明移到下一篇去寫了。
這里大概說一下會分析一個什么樣的示例吧。
布局層級大概就是下面這個樣子
<CoordinatorLayout>
<AppBarLayout>
<CollapsingToolbarLayout />
</AppBarLayout>
<NestedScrollView />
</CoordinatorLayout>
這個例子涉及到四個控件,之所以選它來說明Behavior的用法是因為,這個例子中Android都為我們提供了相應(yīng)的Behavior,分析起來也更有權(quán)威性。
如果大家感興趣,就敬請期待吧~