深入解析Android Design包——Behavior


已經說過了,在AndroidDesign包中主要有兩個核心概念:一是NestedScroll,另一個就是Behavior。
相比於NestedScroll這個概念來說,Behavior分析起來會難很多,因為它幾乎遍布了AndroidDesign包的每一個控件,種類繁多;另外Behavior提供了二十多個空方法給使用者來重寫,主要分為四類:
1.與Touch事件相關的方法
2.與NestedScroll相關的方法
3.與控件依賴相關的方法(依賴這個概念可能接觸的不多,就是如果A依賴B,那么當B變化時會通知A跟着變化)
4.其他方法,如測量和布局等
由此可見,Behavior的使用是非常靈活的,所以功能也是非常的強大。但是,對於越靈活的東西,就越難將它講清除。它有一百種用法,總不能我就舉出一百個例子來進行說明,因此本文只能起到一個拋磚引玉的作用,要真正融會貫通還得靠各位自己去揣摩。

從CoordinatorLayout入手

好好的干嘛扯到CoordinatorLayout呢?
如果你這么問那你就外行了,因為如果沒有CoordinatorLayout,光有Behavior是啥用都沒有滴。
CoordinatorLayout就是一個容器,主要功能就是為它里面的控件傳遞命令,更准確的說就是使用Behavior來讓子控件們相互調用。
CoordinatorLayout有自己的LayoutParams類

public static class LayoutParams extends ViewGroup.MarginLayoutParams {
/**
* A {@link Behavior} that the child view should obey.
*/
Behavior mBehavior;
1
2
3
4
5
1
2
3
4
5
它的布局參數類定義的第一個屬性就是Behavior,而且還有layout_behavior屬性供布局文件使用,可在布局文件中為CoordinatorLayout內部的控件設置behavior對象。
另外,所有的Behavior的祖宗都是CoordinatorLayout.Behavior,這是一個靜態-內部-虛擬類,頭銜有點長~ 我們抓住這個靜態內部類就算是抓到Behavior的精髓了。
除了以上兩點,最重要的一層關系是:所有Behavior的方法都是在CoordinatorLayout中調用的,比如來了個NestedScroll事件,那么CoordinatorLayout會調用自己的onNestedScroll()方法,然后在方法內部,就會調用childView的behavior對應的onNestedScroll()方法了。
具體過程,我們來詳細分析。

如何處理Touch相關事件

找到CoordinatorLayout中的Behavior類,可以發現該類中定義了如下兩個方法:

public boolean onInterceptTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev) {
return false;
}

public boolean onTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev) {
return false;
很顯然,這是攔截觸摸事件和處理觸摸事件的方法。
我們看看這兩個方法是如何被CoordinatorLayout調用的。

先看onInterceptTouchEvent

根據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();
}
從上面代碼可知,真正的處理邏輯在performIntercept()方法中,注意它的第二個參數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 http://www.wmyl15.com/) child.getLayoutParams();
final Behavior b = lp.getBehavior();

if ((intercepted || newBlock) && action != MotionEvent.ACTION_DOWN) {
// 如果事件已經被攔截,那么向其他childView發送cancelEvent
//...省略代碼...
continue;
}

if (!intercepted && b != null) {
switch (type) {
//如果事件未攔截,且childView設置了behavior,則進行攔截判斷
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();

對代碼進行簡化之后,邏輯就很明顯了。先從childView中取出LayoutParams對象,然后從LayoutParams對象中取出Behavior對象,如果performIntercept()方法第二個參數傳進來的是TYPE_ON_INTERCEPT,則調用behavior.onInterceptTouchEvent()方法判斷是否攔截事件。換句話說就是,是否攔截事件跟CoordinatorLayout本身沒有一毛錢關系。

再看onTouchEvent

直接看CoordinatorLayout中的onTouchEvent(www.wmyl11.com)方法源碼:

public boolean onTouchEvent(MotionEvent ev) {
boolean handled = false;
boolean cancelSuper = false;
MotionEvent cancelEvent = null;

final int action = MotionEventCompat.getActionMasked(ev);

//先判斷mBehaviorTouchView是否為null,如果不為null,則不會執行后面的performIntercept()
//如果等於null,則調用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);
}
}

// 經過上面的兩重判斷之后,如果mBehavior還是null,則說明childView不消費touch事件
// 那么該touch事件交給CoordinatorLayout的parent去處理
if (mBehaviorTouchView == null) {
handled |= super.onTouchEvent(ev);
}
...
上面的代碼加了注釋之后,應該不需要多說什么了。
還是那句話,CoordinatorLayout本身也不會消費touch事件。

如何處理NestedScroll相關事件

先看Behavior中跟NestedScroll相關的方法

public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout,
V child, View directTargetChild, View target, int nestedScrollAxes) {
return false;
}

public void onNestedScrollAccepted(www.yule1369.net 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(www.lieqibiji.com CoordinatorLayout coordinatorLayout, V child, View target,
float velocityX, float velocityY) {
return false;
大家不要被這么多方法給嚇到了,如果你有閱讀這篇文章深入解析Android Design包——NestedScroll 就能夠發現,這些方法都是NestedScrollingParent接口中定義的方法。並且CoordinatorLayout本身是實現了NestedScrollingParent接口的,那么CoordinatorLayout會如何調用Behavior的這些方法呢? 肯定是一一對應的來調用。
我想Google這么設計的目的應該是為了解耦,只要給控件提供一個Behavior就可以擁有NestedScrollingParent的功能,這樣一來控件本身就與NestedScrollingParent完全無關了。
由於方法比較多,這里就不一一展示調用過程了,挑onNestedScroll方法來說一下吧。
先看CoordinatorLayout中的onNestedScroll方法:

@Override
public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed www.xyseo.net) {
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,並調用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去處理。
但是,我們注意到最后一段代碼,調用了一個onChildViewsChanged()方法。
這個方法具體邏輯我們在下一小結分析,它主要是處理那些依賴控件的。之所以在此處加一句,是為了那些跟滑動控件存在依賴關系的其他控件,也可以做出響應。

如何處理依賴相關事件

接下來,我們來看看Behavior中依賴相關的方法

//判斷child和dependency是否存在依賴關系
public boolean layoutDependsOn(C www.wmyl88.com/ oordinatorLayout parent, V child, View dependency) {
return false;
}

//dependency發生改變時,回調此方法
public boolean onDependentViewChanged(CoordinatorLayout parent, V child, View dependency) {
return false;
}

//dependency被移除時,回調此方法
public void onDependentViewRemoved(CoordinatorLayout parent, V child, View dependency) {

要完成上面三個方法的使命,需要滿足兩點:
1.需要對CoordinatorLayout所有childView進行兩兩判斷,看它們是否存在依賴關系。
2.當一個childView發生布局改變時,CoordinatorLayout需要回調通知與其有依賴關系的其他childView。

判斷依賴關系

一個View在Android系統中的顯示都是:onMeasure, onLayout, onDraw
所以先看onMeasure:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
prepareChildren();

沒想到第一句就看到重點了,prepareChildren就是為了整理CoordinatorLayout內部的childView,自然也會將childView之間的依賴關系確定好,來看代碼:

private void prepareChildren() {
mDependencySortedChildren.clear(); //List<View>
mChildDag.clear(); //圖結構 --- 無回路有向圖
//遍歷所有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
//將判斷的結果保存在有向圖mChildDag中
for (int j = 0; j < count; j++) {
if (j == i) {
continue;
}
final View other = getChildAt(j);
final LayoutParams otherLp = getResolvedLayoutParams(other);
//這個dependsOn方法就是判斷依賴關系的,內部會調用Behavior.layoutDependsOn()方法
if (otherLp.dependsOn(this, other, view)) {
if (!mChildDag.contains(other)) {
mChildDag.addNode(other);
}
mChildDag.addEdge(view, other);
}
}
}
//將圖中的數據排序,並保存在List中
mDependencySortedChildren.addAll(mChildDag.getSortedList());
//將List倒序設置,讓被依賴的childView排在前面,依賴於它的排在后面
Collections.reverse(mDependencySortedChildren);
這里涉及到一種比較復雜的數據結構——無回路有向圖,篇幅有限就不在這里多說,我們只要知道會調用layoutDependsOn方法來判斷依賴關系,然后將數據最后保存在mDependencySortedChildren這個List中。
這個mDependencySortedChildren列表中保存的都是childView,不過是按照特定的順序進行了排序:
如果childView被其他view依賴的次數最多,則排在最前面,以此類推。
至於依賴關系並沒有保存,到時候要用到時,再次調用layoutDependsOn方法來判斷,寫到這里我好像明白了為什么要將依賴次數多的放列表前面了。

childView發生布局改變

OK,依賴關系確定了,那就看看當childView發生改變時,如何讓依賴的view跟着改變。
其實在NestedScroll相關方法中,最后都會調用一句代碼

if (accepted) {
onChildViewsChanged(EVENT_NESTED_SCROLL);
上面也有提到,這個方法就是處理依賴控件的變化的。在分析它之前,還有必要看看其他地方有沒有調用此方法。
然后,就看到了在onAttachToWindow()方法中,為CoordinatorLayout設置了OnPreDrawListener 回調,也就是說在執行onDraw之前,回執行onPreDraw方法中的代碼。
我們先來看OnPreDrawListener:

class OnPreDrawListener implements ViewTreeObserver.OnPreDrawListener {
@Override
public boolean onPreDraw() {
onChildViewsChanged(EVENT_PRE_DRAW);
return true;

onPreDraw()的代碼很簡單,就是執行onChildViewsChanged()方法,也就是說每次onDraw都會調用這個方法來處理依賴控件。

接下來重點看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相關的代碼 ...
//可以說,上面的代碼都是為了提升效率,下面的才是真正的處理邏輯

for (int j = i + 1; j < childCount; j++) {
final View checkChild = mDependencySortedChildren.get(j);
final LayoutParams checkLp = (LayoutParams) checkChild.getLayoutParams();
//獲取對應的Behavior對象
final Behavior b = checkLp.getBehavior();

//判斷依賴關系
if (b != null && b.layoutDependsOn(this, checkChild, child)) {
if (type == EVENT_PRE_DRAW && checkLp.getChangedAfterNestedScroll()) {
//當NestedScroll 和 Draw都會觸發這個方法,
//這里二者只能有一個繼續往下執行
checkLp.resetChangedAfterNestedScroll();
continue;
}

final boolean handled;
switch (type) {
case EVENT_VIEW_REMOVED:
// 調用Behavior對應的方法,通知依賴這個child的其他view
//這里的child的被依賴,checkChild是依賴於它
b.onDependentViewRemoved(this, checkChild, child);
handled = true;
break;
default:
// 除了刪除事件,其他的都調用下面的方法
handled = b.onDependentViewChanged(this, checkChild, child);
break;
}

if (type == EVENT_NESTED_SCROLL) {
//這里跟上面的checkLp.getChangedAfterNestedScroll()對應
checkLp.setChangedAfterNestedScroll(handled);

同樣的,為了突出處理邏輯,把一些代碼省略掉了。
代碼不長,希望大家根據我的注釋讀一下代碼,自然就明白了。
其實,onChildViewsChanged這個方法也只是在調用Behavior的相關方法而已,也就是說如果childA依賴於childB,那么當childB發生布局變化時,childB的Behavior就會把這個變化同時作用到childA身上。

結語

說到這里,一句話總結CoordinatorLayout本身啥事兒也不干,全讓底下的childView去干了。
而Behavior之所以強大,是因為在我們不修改View的情況下,可以對View的行為進行修改和控制。

本來是要舉例說明一下Behavior的具體用法的,但是沒想到一寫一寫已經這么長了,太長的文章不利於閱讀,也不利於學習吸收,就只能把舉例說明移到下一篇去寫了。
這里大概說一下會分析一個什么樣的示例吧。
布局層級大概就是下面這個樣子

<CoordinatorLayout>

<AppBarLayout>
<CollapsingToolbarLayout />
</AppBarLayout>

<NestedScrollView />

</CoordinatorLayout>


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM