在上篇文章中,我們簡單介紹了一下Behavior,今天對他的原理做進(jìn)一步分析。主要介紹behavior如何自定義,behavior的構(gòu)造,onPreDraw,fab為何隨snackbar變化的相關(guān)的知識。
自定義behavior
先看個例子,上篇文章主要是重點分析了下,為什么snackbar出現(xiàn)和消失的時候,fab會做出相應(yīng)變化,那我們能否修改這種變化呢?
比如我想要snackbar出現(xiàn)的時候,fab往上移動100,snackbar消失的時候fab再往上移動100,能否實現(xiàn)呢?
當(dāng)然可以,代碼也很簡單,自定義一個MyBehavior,注意必須加入一個MyBehavior的構(gòu)造器,帶有Context和AttributeSet參數(shù),原因后文會說。
public class MyBehavior extends CoordinatorLayout.Behavior<View> {
//此構(gòu)造函數(shù)必須加入
public MyBehavior(Context context, AttributeSet attrs) {
super(context,attrs);
}
//child就是綁定此behavior的view,dependency是發(fā)送變化的view
@Override
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
return dependency instanceof Snackbar.SnackbarLayout;
}
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
//此處child 就是fab,dependency是被依賴的view
if (dependency instanceof Snackbar.SnackbarLayout) {
//SnackbarLayout 變化了,A該如何變化在這里寫
child.setTranslationY(child.getTranslationY() - 100);
return true;
}
return false;
}
@Override
public void onDependentViewRemoved(CoordinatorLayout parent, View child, View dependency) {
if (dependency instanceof Snackbar.SnackbarLayout) {
//SnackbarLayout 變化了,fab該如何變化在這里寫
child.setTranslationY(child.getTranslationY() - 100);
}
}
}
然后在xml內(nèi)配置behavior,其實就是加入了一行代碼 app:layout_behavior="com.fish.a2.MyBehavior"
<android.support.design.widget.FloatingActionButton
android:id="@+id/fab"
app:layout_behavior="com.fish.a2.MyBehavior"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="@dimen/fab_margin"
android:src="@android:drawable/ic_dialog_email" />
看效果

所以說自定義Behavior也是很簡單的事情
原理分析
behavior到底是什么
從上邊的代碼看來,behavior像是view的一個屬性,其實他是view的LayoutParam的一個屬性,就像寬高一樣。當(dāng)然不是任何一個view的LayoutParam都有這個屬性的,只有LayoutParam為android.support.design.widget.CoordinatorLayout.LayoutParams才有這個屬性,說白了,就是只有CoordinatorLayout的子view的LayoutParam可以設(shè)置behavior。我們看看CoordinatorLayout.LayoutParams的代碼,可以發(fā)現(xiàn)Behavior變量的確在里面。
public static class LayoutParams extends ViewGroup.MarginLayoutParams {
Behavior mBehavior;
boolean mBehaviorResolved = false;
...
final Rect mLastChildRect = new Rect();
}
再來看看app:layout_behavior="com.fish.a2.MyBehavior"這行代碼是怎么導(dǎo)致LayoutParams內(nèi)的mBehavior被賦值的
我們知道infate的時候,會根據(jù)xml去構(gòu)造LayoutParams,所以我們看CoordinatorLayout.LayoutParams的構(gòu)造函數(shù)
LayoutParams(Context context, AttributeSet attrs) {
super(context, attrs);
final TypedArray a = context.obtainStyledAttributes(attrs,
R.styleable.CoordinatorLayout_LayoutParams);
...
mBehaviorResolved = a.hasValue(
R.styleable.CoordinatorLayout_LayoutParams_layout_behavior);
if (mBehaviorResolved) {
mBehavior = parseBehavior(context, attrs, a.getString(
R.styleable.CoordinatorLayout_LayoutParams_layout_behavior));
}
a.recycle();
}
當(dāng)我們構(gòu)造fab的LayoutParams時,走到L7,查一下是否存在layout_behavior值,如果存在,那就parseBehavior并且賦值給mBehavior。parseBehavior就是把字符串com.fish.a2.MyBehavior變成一個對象,用反射的方法。主要代碼如下所示
// 這里是指定的Behavior構(gòu)造器的參數(shù)類型
static final Class<?>[] CONSTRUCTOR_PARAMS = new Class<?>[] {
Context.class,
AttributeSet.class
};
...
static Behavior parseBehavior(Context context, AttributeSet attrs, String name) {
...
try {
Map<String, Constructor<Behavior>> constructors = sConstructors.get();
if (constructors == null) {
constructors = new HashMap<>();
sConstructors.set(constructors);
}
Constructor<Behavior> c = constructors.get(fullName);
if (c == null) {
final Class<Behavior> clazz = (Class<Behavior>) Class.forName(fullName, true,
context.getClassLoader());
//獲取特定參數(shù)的構(gòu)造器
c = clazz.getConstructor(CONSTRUCTOR_PARAMS);
c.setAccessible(true);
constructors.put(fullName, c);
}
return c.newInstance(context, attrs);
} catch (Exception e) {
throw new RuntimeException("Could not inflate Behavior subclass " + fullName, e);
}
}
主要L23,這里是用特定參數(shù)的構(gòu)造器來c.newInstance的,CONSTRUCTOR_PARAMS就是一個Context,一個AttributeSet,為什么我們開頭的時候說自定義Behavior必須帶一個這種類型的構(gòu)造器的,現(xiàn)在應(yīng)該有答案了。
那么現(xiàn)在問題來了,在上一篇文章中,我們根本就沒有設(shè)置layout_behavior,那這個LayoutParams里面的mBehavior是在哪里復(fù)制的呢?
我們再來看看, FloatingActionButton有個注解,CoordinatorLayout.DefaultBehavior(FloatingActionButton.Behavior.class),在這里指定了默認(rèn)的Behavior
@CoordinatorLayout.DefaultBehavior(FloatingActionButton.Behavior.class)
public class FloatingActionButton extends VisibilityAwareImageButton
在CoordinatorLayout的onMeasure的時候會調(diào)用prepareChildren,進(jìn)而調(diào)用getResolvedLayoutParams,在getResolvedLayoutParams里會把注解里的默認(rèn)Behavior賦值給mBehavior,主要代碼如下
LayoutParams getResolvedLayoutParams(View child) {
final LayoutParams result = (LayoutParams) child.getLayoutParams();
//如果xml內(nèi)寫了behavior,此時result.mBehaviorResolved就為true,不會進(jìn)去
if (!result.mBehaviorResolved) {
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().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;
}
所以到了現(xiàn)在,我們知道設(shè)置一個view的behavior有2種方式,xml內(nèi)指定,或者注解里指定,xml優(yōu)先級高。xml內(nèi)指定的話,是在inflate的時候?qū)Behavior賦值的,在注解里指定的話,是在onMeasure內(nèi)賦值的,稍有不同。
behavior如何發(fā)揮作用
前面說了如何給view配置behavior,那配了behavior又有什么用呢?為何behavior能夠監(jiān)測到另一個view的變化情況,這都是CoordinatorLayout的功勞。
onMeasure
我們再來看看onMeasure的代碼,主要看prepareChildren和ensurePreDrawListener
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
prepareChildren();
ensurePreDrawListener();
。。。
先看prepareChildren
private void prepareChildren() {
//清空mDependencySortedChildren
mDependencySortedChildren.clear();
for (int i = 0, count = getChildCount(); i < count; i++) {
final View child = getChildAt(i);
final LayoutParams lp = getResolvedLayoutParams(child);
lp.findAnchorView(this, child);
//加入child
mDependencySortedChildren.add(child);
}
// We need to use a selection sort here to make sure that every item is compared
// against each other
//排序
selectionSort(mDependencySortedChildren, mLayoutDependencyComparator);
}
prepareChildren內(nèi)做了什么,主要是搞出來一個mDependencySortedChildren,根據(jù)依賴關(guān)系對child進(jìn)行排序。首先L3把mDependencySortedChildren clear,然后遍歷子view,全部加入到mDependencySortedChildren內(nèi),最后對mDependencySortedChildren進(jìn)行排序。注意每次measure都會調(diào)用prepareChildren來搞出一個mDependencySortedChildren。
我們在看看排序的代碼(用的冒泡),看mLayoutDependencyComparator就行了,看下邊代碼可以知道,被依賴的view放前面,比如我們fab依賴于snackbar,那么snackbar必然放在fab的前邊。這么排序有什么用?其實是提高一點效率,后文會說的。
final Comparator<View> mLayoutDependencyComparator = new Comparator<View>() {
@Override
public int compare(View lhs, View rhs) {
if (lhs == rhs) {
return 0;
} else if (((LayoutParams) lhs.getLayoutParams()).dependsOn(
CoordinatorLayout.this, lhs, rhs)) {
return 1;
} else if (((LayoutParams) rhs.getLayoutParams()).dependsOn(
CoordinatorLayout.this, rhs, lhs)) {
return -1;
} else {
return 0;
}
}
};
再看ensurePreDrawListener
在prepareChildren確定mDependencySortedChildren之后,會執(zhí)行ensurePreDrawListener,在這里寫判斷下CoordinatorLayout的子view是否存在依賴關(guān)系,如果存在的話就hasDependencies為true,后邊會加入PreDrawListener。
void ensurePreDrawListener() {
//判斷是否存在依賴關(guān)系
boolean hasDependencies = false;
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
if (hasDependencies(child)) {
hasDependencies = true;
break;
}
}
if (hasDependencies != mNeedsPreDrawListener) {
if (hasDependencies) {
//加入PreDrawListener
addPreDrawListener();
} else {
removePreDrawListener();
}
}
}
PreDrawListener是什么?看下邊代碼,簡單,就是在onPreDraw的時候調(diào)用dispatchOnDependentViewChanged。
void addPreDrawListener() {
if (mIsAttachedToWindow) {
// Add the listener
if (mOnPreDrawListener == null) {
mOnPreDrawListener = new OnPreDrawListener();
}
final ViewTreeObserver vto = getViewTreeObserver();
vto.addOnPreDrawListener(mOnPreDrawListener);
}
// Record that we need the listener regardless of whether or not we're attached.
// We'll add the real listener when we become attached.
mNeedsPreDrawListener = true;
}
class OnPreDrawListener implements ViewTreeObserver.OnPreDrawListener {
@Override
public boolean onPreDraw() {
dispatchOnDependentViewChanged(false);
return true;
}
}
onPreDraw這個回調(diào)和onGlobalLayout類似的,他們的對象是ViewTreeObserver,而不是某個view。在即將繪制的時候,會調(diào)用mTreeObserver.dispatchOnPreDraw(),然后分發(fā)到各個OnPreDrawListener,在回調(diào)onPreDraw的。簡單的說,就是在重繪之前,會調(diào)用onPreDraw。我們在onPreDraw里面調(diào)用了dispatchOnDependentViewChanged,這個函數(shù)是CoordinatorLayout非常重要的函數(shù)。Behavior的主要行為都是寫在這里面的。我們先總結(jié)下ensurePreDrawListener做了什么,判斷子view是否有依賴行為,如果有的話注冊一個onPreDraw監(jiān)聽
dispatchOnDependentViewChanged
這里傳進(jìn)來的fromNestedScroll為false,遍歷mDependencySortedChildren,查一下每個view的rect是否發(fā)生了變化,如果發(fā)生了變化(假設(shè)變化的view為A),就遍歷后邊的view,判斷后邊view是否依賴于A(L33),如果依賴就做出相應(yīng)變化(L36)。看到L33和L36,終于舒了口氣,和Behavior里的行為扯上了關(guān)系。
void dispatchOnDependentViewChanged(final boolean fromNestedScroll) {
final int layoutDirection = ViewCompat.getLayoutDirection(this);
final int childCount = mDependencySortedChildren.size();
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
for (int j = 0; j < i; j++) {
final View checkChild = mDependencySortedChildren.get(j);
if (lp.mAnchorDirectChild == checkChild) {
offsetChildToAnchor(child, layoutDirection);
}
}
// Did it change? if not continue
final Rect oldRect = mTempRect1;
final Rect newRect = mTempRect2;
getLastChildRect(child, oldRect);
getChildRect(child, true, newRect);
if (oldRect.equals(newRect)) {
continue;
}
recordLastChildRect(child, newRect);
// 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();
//這里調(diào)用了behavior的layoutDependsOn
if (b != null && b.layoutDependsOn(this, checkChild, child)) {
。。。
//這里調(diào)用了behavior的onDependentViewChanged
final boolean handled = b.onDependentViewChanged(this, checkChild, child);
...
}
}
}
}
這里再說幾點,怎么知道哪些view發(fā)生了變化,代碼如下,就是看oldRect和 newRect 是否一致,getChildRect就是獲取view的當(dāng)前rect,而getLastChildRect是獲取view的舊的rect,這個比較奇怪,居然知道舊的rect。
final Rect oldRect = mTempRect1;
final Rect newRect = mTempRect2;
getLastChildRect(child, oldRect);
getChildRect(child, true, newRect);
if (oldRect.equals(newRect)) {
continue;
}
recordLastChildRect(child, newRect);
看看getLastChildRect的代碼,原來CoordinatorLayout的LayoutParams里面存儲了mLastChildRect??瓷线叺腖8可以知道,會記錄newRect到LayoutParams里。
void getLastChildRect(View child, Rect out) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
out.set(lp.getLastChildRect());
}
還有個問題,比如我們知道子view A發(fā)生了變化,可能有B依賴于A,C依賴于A,怎么去找B,C呢,看上邊L28,只要遍歷A后邊的代碼就可以了,為什么?看看前文的mDependencySortedChildren的排序規(guī)則就知道了,B,C絕對是在A的后邊??梢允∪フ仪懊娴膙iew,這就是mDependencySortedChildren排序的作用。
再后邊代碼就是,先判斷下b.layoutDependsOn是否返回true,然后執(zhí)行b.onDependentViewChanged
if (b != null && b.layoutDependsOn(this, checkChild, child)) {
。。。
//這里調(diào)用了behavior的onDependentViewChanged
final boolean handled = b.onDependentViewChanged(this, checkChild, child);
...
}
}
好了,behavior的原理基本分析完了。有點繞,但不復(fù)雜。
我個人認(rèn)為,這個實現(xiàn)過程還可以優(yōu)化一下,比如上文第一個for循環(huán),是遍歷了所有的子view,實際上只要遍歷被依賴的子view 就好了。而第二個for循環(huán),是遍歷了child(rect變化的view)之后的所有子view,其實也沒這個必要,因為依賴關(guān)系是早就定好的,可以建一個數(shù)組存儲哪些view依賴了child,這樣只要遍歷這個數(shù)組就可以了。
如果是我來寫,我會給每個view設(shè)計一個依賴者數(shù)組,比如Aview的依賴者數(shù)組內(nèi)有B,C,就代表B依賴于A,C依賴于A。 那第一個for循環(huán)遍歷依賴者數(shù)組非空的view即可,而第二個for循環(huán)遍歷依賴者數(shù)組就好。
還有一點,view的rect發(fā)生變化肯定在onLayout之后就知道了,如果在onLayout里把發(fā)生變化的view記錄下來,那么第一個for循環(huán)就可以更簡單了,也沒必要在LayoutParam里面設(shè)計一個mLastChildRect了。
以上是我的個人想法,如有不對,歡迎指正,可能代碼google認(rèn)為反正CoordinatorLayout的子view很小,所以沒必要搞那么復(fù)雜。
onDependentViewRemoved
我們開篇自定布局的時候還寫了onDependentViewRemoved,那這個onDependentViewRemoved是在哪里被調(diào)用的呢?
ViewGroup內(nèi)有個mOnHierarchyChangeListener,view結(jié)構(gòu)發(fā)生變化時會觸發(fā)OnHierarchyChangeListener回調(diào)。
protected OnHierarchyChangeListener mOnHierarchyChangeListener;
public interface OnHierarchyChangeListener {
/**
* Called when a new child is added to a parent view.
*
* @param parent the view in which a child was added
* @param child the new child view added in the hierarchy
*/
void onChildViewAdded(View parent, View child);
/**
* Called when a child is removed from a parent view.
*
* @param parent the view from which the child was removed
* @param child the child removed from the hierarchy
*/
void onChildViewRemoved(View parent, View child);
}
再看CoordinatorLayout內(nèi)自己定義了一個HierarchyChangeListener,在onChildViewRemoved的時候會調(diào)用dispatchDependentViewRemoved,這個HierarchyChangeListener在構(gòu)造函數(shù)內(nèi)set。所以有view被remove調(diào)的時候回回調(diào)到
dispatchDependentViewRemoved。
private class HierarchyChangeListener implements OnHierarchyChangeListener {
@Override
public void onChildViewAdded(View parent, View child) {
if (mOnHierarchyChangeListener != null) {
mOnHierarchyChangeListener.onChildViewAdded(parent, child);
}
}
@Override
public void onChildViewRemoved(View parent, View child) {
dispatchDependentViewRemoved(child);
if (mOnHierarchyChangeListener != null) {
mOnHierarchyChangeListener.onChildViewRemoved(parent, child);
}
}
}
dispatchDependentViewRemoved的代碼也很簡單,會根據(jù)需要觸發(fā)onDependentViewRemoved
void dispatchDependentViewRemoved(View view) {
final int childCount = mDependencySortedChildren.size();
boolean viewSeen = false;
for (int i = 0; i < childCount; i++) {
final View child = mDependencySortedChildren.get(i);
if (child == view) {
// We've seen our view, which means that any Views after this could be dependent
viewSeen = true;
continue;
}
if (viewSeen) {
CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams)
child.getLayoutParams();
CoordinatorLayout.Behavior b = lp.getBehavior();
if (b != null && lp.dependsOn(this, child, view)) {
b.onDependentViewRemoved(this, child, view);
}
}
}
}
上述代碼都是為了監(jiān)聽某個view被remove而加的,那為什么增加一個view的時候沒這么麻煩,刪除一個view就這么麻煩呢。因為增加了一個view,那這個view,必然在mDependencySortedChildren內(nèi),而刪除了一個view,這個view在mDependencySortedChildren就找不到了,所以加了這一堆代碼
泛型類Behavior
要知道Behavior其實是個泛型類
public static abstract class Behavior<V extends View>
所以自定義Behavior可以這么寫,這樣更優(yōu)雅,免去了強轉(zhuǎn)
public static class Behavior extends CoordinatorLayout.Behavior<FloatingActionButton> {
// We only support the FAB <> Snackbar shift movement on Honeycomb and above. This is
// because we can use view translation properties which greatly simplifies the code.
private static final boolean SNACKBAR_BEHAVIOR_ENABLED = Build.VERSION.SDK_INT >= 11;
private ValueAnimatorCompat mFabTranslationYAnimator;
private float mFabTranslationY;
private Rect mTmpRect;
@Override
public boolean layoutDependsOn(CoordinatorLayout parent,
FloatingActionButton child, View dependency) {
// We're dependent on all SnackbarLayouts (if enabled)
return SNACKBAR_BEHAVIOR_ENABLED && dependency instanceof Snackbar.SnackbarLayout;
}
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, FloatingActionButton child,
View dependency) {
if (dependency instanceof Snackbar.SnackbarLayout) {
updateFabTranslationForSnackbar(parent, child, dependency);
} else if (dependency instanceof AppBarLayout) {
// If we're depending on an AppBarLayout we will show/hide it automatically
// if the FAB is anchored to the AppBarLayout
updateFabVisibility(parent, (AppBarLayout) dependency, child);
}
return false;
}
@Override
public void onDependentViewRemoved(CoordinatorLayout parent, FloatingActionButton child,
View dependency) {
if (dependency instanceof Snackbar.SnackbarLayout) {
updateFabTranslationForSnackbar(parent, child, dependency);
}
}
總結(jié)
1、view的behavior有2種方式,xml內(nèi)指定,或者注解里指定,xml優(yōu)先級高。xml內(nèi)指定的話,是在inflate的時候?qū)Behavior賦值的,在注解里指定的話,是在onMeasure內(nèi)賦值的,稍有不同。
2、behavior能夠檢測到view的尺寸變化以及view被remove
3、CoordinatorLayout內(nèi)的mDependencySortedChildren里,被依賴的view放前面,比如我們fab依賴于snackbar,那么snackbar必然放在fab的前邊。