系列文章:
CoordinatorLayout系列(一):Behavior
CoordinatorLayout系列(二)AppBarLayout
CoordinatorLayout系列(三)AppBarLayout之layout_scrollFlags
CoordinatorLayout系列(四)CollapsingToolbarLayout
CoordinatorLayout系列(五)例子
CoordinatorLayout是google在Material Design上提出的一個佈局樣式,這個佈局的主要功能就是實現一個view跟隨另一個view變化,具體變化邏輯,由Behavior來實現,一個好的跟隨效果可以展現出很酷的動畫,因此這個系列就從Behavior入手,來剖析CoordinatorLayout的細節。
一、使用方法
首先擺出效果:
功能就是實現一個球跟隨另一個球移動,兩個球的中點就是整個界面的中點。
如果沒有CoordinatorLayout的話,那上面的效果也是很好實現的,只要在一個球的onTouchEventListener中,按邏輯移動另一個球就行,但是這樣耦合度比較高,當多個view聯動時,代碼看起來相對要亂,不好處理。CoordinatorLayout就是專門爲這麼一種情況設計的佈局,充分的解耦了各個聯動view之間的邏輯。
佈局:
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- 主動view,也叫dependency -->
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/dependency"
android:layout_width="80dp"
android:layout_height="80dp"
android:layout_marginTop="200dp"
android:src="@drawable/ic_launcher_background" />
<!-- 被動view,也叫child -->
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/child"
android:layout_width="80dp"
android:layout_height="80dp"
android:src="@drawable/ic_launcher_background"
app:layout_behavior=".Behavior.MyBehavior" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
這裏有兩個角色,主動view,也叫dependency和被動view,也叫child,child依賴dependency,dependency發生了改變,child也要隨之發生改變。注意到上面的佈局child有一個屬性:layout_behavior。
layout_behavior是指定一個類,這個類裏面定義了變化的行爲,也就是怎麼變化的。
Behavior行爲定義如下:
public class MyBehavior extends CoordinatorLayout.Behavior<CircleImageView> {
public MyBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
//判斷child是否依賴dependency
@Override
public boolean layoutDependsOn(CoordinatorLayout parent, CircleImageView child, View dependency) {
if (child.getId() == R.id.child) {
return dependency.getId() == R.id.dependency;
}
return false;
}
//當dependency做出變化時,就調用這個方法
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, CircleImageView child, View dependency) {
//根據dependency的位置,btn
int totalWidth = ((View) dependency.getParent()).getWidth();
int totalHeight = ((View) dependency.getParent()).getHeight();
int top = dependency.getTop();
int left = dependency.getLeft();
int x = totalWidth - left - child.getWidth();
int y = totalHeight - top - child.getHeight();
//根據dependency的位置,設置child位置
setPosition(child, x, y);
return true;
}
private void setPosition(View v, int x, int y) {
v.layout(x, y, x + v.getWidth(), y + v.getHeight());
}
}
demo地址:https://github.com/whoami-I/CoordinatorLayoutExample.git
二、源碼分析
下面從源碼的分析角度來看,CoordinatorLayout是怎麼實現denpendency變化時,child也能相應的變化。
打開CoordinatorLayout源碼,從onMeasure方法開始,第一行代碼就映入眼簾;
prepareChildren();
這個方法是非常重要的,主要做了這麼幾項工作:
1、遍歷子view,確保behavior存在
2、確定依賴關係,使用有向無環圖保存這種關係
3、使用DFS算法,將圖轉化成一條鏈,保證鏈的首節點無依賴,並且依賴關係只存在於靠近尾端的節點依賴於靠近頭部的節點,也就是後面的依賴前面的,下面有圖。
Behavior這個屬性其實被定義在了LayoutParams中,解析Behavior的主要工作被放在了getResolvedLayoutParams這個方法:
LayoutParams getResolvedLayoutParams(View child) {
final LayoutParams result = (LayoutParams) child.getLayoutParams();
if (!result.mBehaviorResolved) {
if (child instanceof AttachedBehavior) {
Behavior attachedBehavior = ((AttachedBehavior) child).getBehavior();
if (attachedBehavior == null) {
Log.e(TAG, "Attached behavior class is null");
}
result.setBehavior(attachedBehavior);
result.mBehaviorResolved = true;
} else {
// The deprecated path that looks up the attached behavior based on annotation
Class<?> childClass = child.getClass();
DefaultBehavior defaultBehavior = null;
while (childClass != null
&& (defaultBehavior = childClass.getAnnotation(DefaultBehavior.class))
== null) {
childClass = childClass.getSuperclass();
}
if (defaultBehavior != null) {
try {
result.setBehavior(
defaultBehavior.value().getDeclaredConstructor().newInstance());
} catch (Exception e) {
Log.e(TAG, "Default behavior class " + defaultBehavior.value().getName()
+ " could not be instantiated. Did you forget"
+ " a default constructor?", e);
}
}
result.mBehaviorResolved = true;
}
}
return result;
}
mBehaviorResolved這個變量是在LayoutParams初始化時設置的,如果child節點設置了app:layout_behavior屬性,則爲true,否則爲false,在這裏我們已經設置了這個屬性,當然,如果沒有設置的話,還可以使用註解的方式來初始化Behavior,這個註解爲DefaultBehavior,或者是當前view繼承了AttachedBehavior這個類,其實AppBarLayout就是繼承了AttachedBehavior,所以結合AppBarLayout結合可以做出很好的效果。
找到Behavior之後,下一步就是尋找dependency了,這個工作放在了findAnchorView這個方法:
View findAnchorView(CoordinatorLayout parent, View forChild) {
if (mAnchorId == View.NO_ID) {
mAnchorView = mAnchorDirectChild = null;
return null;
}
if (mAnchorView == null || !verifyAnchorView(forChild, parent)) {
resolveAnchorView(forChild, parent);
}
return mAnchorView;
}
首先看當前view是否有依賴,如果沒有,則直接返回,如果有的話,就通過findViewById找到這個dependency。
好了,解析完behavior和dependency,下一步就是將這個依賴關係用一個圖結構保存到mChildDag裏面。
俗話說一圖勝千言,下面用三張圖來說明上面的工作:
先假設一個依賴關係:
然後將這個依賴關係轉化成有向圖:
最後轉化成鏈表:
鏈表頭爲D,沒有依賴關係。
有了Behavior和依賴關係圖,所有準備工作都做好了,到了調用behavior的時機了,CoordinatorLayout註冊了這麼一個監聽:
class OnPreDrawListener implements ViewTreeObserver.OnPreDrawListener {
@Override
public boolean onPreDraw() {
onChildViewsChanged(EVENT_PRE_DRAW);
return true;
}
}
此監聽在layout和draw之間進行調用,裏面代碼比較多,只需要抓住裏面的重點:
//對CoordinatorLayout的直接子view遍歷
for (int i = 0; i < childCount; i++) {
final View child = mDependencySortedChildren.get(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
........
// Check child views before for anchor,如果有anchor view,根據anchorview調整位置
for (int j = 0; j < i; j++) {
final View checkChild = mDependencySortedChildren.get(j);
if (lp.mAnchorDirectChild == checkChild) {
offsetChildToAnchor(child, layoutDirection);
}
}
.........
// Update any behavior-dependent views for the change
for (int j = i + 1; j < childCount; j++) {
final View checkChild = mDependencySortedChildren.get(j);
final LayoutParams checkLp = (LayoutParams) checkChild.getLayoutParams();
final Behavior b = checkLp.getBehavior();
if (b != null && b.layoutDependsOn(this, checkChild, child)) {
if (type == EVENT_PRE_DRAW && checkLp.getChangedAfterNestedScroll()) {
// If this is from a pre-draw and we have already been changed
// from a nested scroll, skip the dispatch and reset the flag
checkLp.resetChangedAfterNestedScroll();
continue;
}
final boolean handled;
switch (type) {
case EVENT_VIEW_REMOVED:
// EVENT_VIEW_REMOVED means that we need to dispatch
// onDependentViewRemoved() instead
b.onDependentViewRemoved(this, checkChild, child);
handled = true;
break;
default:
// Otherwise we dispatch onDependentViewChanged()
handled = b.onDependentViewChanged(this, checkChild, child);
break;
}
}
}
}
只需要注意上面有三個for循環,循環的大致意思就是對於mDependencySortedChildren裏面的第i個view(注意mDependencySortedChildren上面已經說了,是隻有靠近尾端的節點依賴靠近頭部的節點),首先調整自身的位置,對0~i-1個元素循環,如果第i個節點依賴其中的某一個的話,調用offsetChildToAnchor,其實這裏面就調用到了behavior.onDependentViewChanged方法。然後遍歷i+1 ~ totoalSize這之間的節點,如果這中間有依賴於第i個節點的view的話,那麼就要調整這個view的位置,並且調用behavior.onDependentViewChanged方法
到此,關於behavior的介紹完畢,之後的文章還會結合AppBarLayout、CollapseLayout一起實現聯動效果、CoordinatorLayout事件分發和滑動處理相關的細節。
下一篇:CoordinatorLayout系列(二)AppBarLayout