CoordinatorLayout系列(一):Behavior

系列文章:
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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章