自定義View事件篇進階篇(三)-CoordinatorLayout與Behavior

AndyJennifer發表於2019-08-02

前言

在上篇文章中,我們介紹了NestedScrolling(巢狀滑動)機制,介紹了子控制元件與父控制元件巢狀滑動的處理。現在我們來了解谷歌大大為我們提供的另一個控制元件的互動佈局CoordainatorLayout。CoordainatorLayout對於Android開發老司機來說肯定不會陌生,作為控制內部一個或多個的子控制元件協同互動的容器,開發者可以通過設定Behavior去控制多個控制元件的協同互動效果,測量尺寸、佈局位置及觸控響應。作為谷歌推出的明星元件,分析CoordainatorLayout的文章已是數不勝數。而分析整個CoordainatorLayout原理的相關資料在網上很少,因此本文會把重點放在分析其內部原理上。

通過閱讀該文,你能瞭解如下知識點:

  • CoordainatorLayout中Behavior中的基礎使用
  • CoordainatorLayout中多個控制元件協同互動的原理
  • CoordainatorLayout中Behavior的例項化過程
  • Behavior實現巢狀滑動的原理與過程
  • Behavior自定義佈局的時機與過程
  • Behavior自定義測量的時機與過程

該部落格中涉及到的示例,在NestedScrollingDemo專案中都有實現,大家可以按需自取。

CoordainatorLayout簡介

熟悉CoordinatorLayout的小夥伴,肯定知道CoordinatorLayout主要實現以下四個功能:

  • 處理子控制元件的依賴下的互動
  • 處理子控制元件的巢狀滑動
  • 處理子控制元件的測量與佈局
  • 處理子控制元件的事件攔截與響應。

而上述四個功能,都依託於CoordainatorLayout中提供的一個叫做Behavior的“外掛”。Behavior內部也提供了相應方法來對應這四個不同的功能,具體如下所示:

Behavior方法設定.jpg

在下面的文章中不會介紹Behavior巢狀滑動相關方法的作用,如果需要了解這些方法的作用,建議參看自定義View事件篇進階篇(二)-自定義NestedScrolling實戰文章下的方法介紹。

那現在我們就一起來看看,谷歌是怎麼圍繞Behavior對上述四個功能進行設計的把。

子控制元件依賴下的互動設計

對於子控制元件的依賴互動,谷歌是這樣設計的:

依賴下的互動.jpg

當CoordainatorLayout中子控制元件(childView1)的位置、大小等發生改變的時候,那麼在CoordainatorLayout內部會通知所有依賴childView1的控制元件,並呼叫對應宣告的Behavior,告知其依賴的childView1發生改變。那麼如何判斷依賴,接受到通知後如何處理。這些都交由Behavior來處理。

子控制元件的巢狀滑動的設計

對於子控制元件的巢狀滑動,谷歌是這樣設計的:

巢狀滑動設計.jpg

CoordinatorLayout實現了NestedScrollingParent2介面。那麼當事件(scroll或fling)產生後,內部實現了NestedScrollingChild介面的子控制元件會將事件分發給CoordinatorLayout,CoordinatorLayout又會將事件傳遞給所有的Behavior。接著在Behavior中實現子控制元件的巢狀滑動。那麼再結合上文提到的Behavior中巢狀滑動的相關方法,我們可以得到如下流程:

巢狀滑動整體流程.jpg

觀察谷歌的設計,我們可以發現,相對於NestedScrolling機制(參與角色只有子控制元件和父控制元件),CoordainatorLayout中的互動角色更為豐富,在CoordainatorLayout下的子控制元件可以與多個兄弟控制元件進行互動

子控制元件的測量、佈局、事件的設計

看了谷歌對子控制元件的巢狀滑動設計,我們再來看看子控制元件的測量、佈局、事件的設計:

佈局與測量及事件的設計.jpg

因為CoordainatorLayout主要負責的是子控制元件之間的互動,內部控制元件的測量與佈局,就簡單的類似FrameLayout處理方式就好了。在特殊的情況下,如子控制元件需要處理寬高和佈局的時候,那麼交由Behavior內部的onMeasureChildonLayoutChild方法來進行處理。同理對於事件的攔截與處理,如果子控制元件需要攔截並消耗事件,那麼交由給Behavior內部的onInterceptTouchEventonTouchEvent方法進行處理。

可能有的小夥伴會想,為什麼會將這四種功能對於的方法將這些功能都交由Behavior實現。其實原因非常簡單,因為將所有功能都對應在Behavior中,那麼對於子控制元件來說,這種外掛化的方式就非常解耦了,我們的子控制元件無需將效果寫死在自身中,我們只需要對應不同的Behavior,就可以實現不同的效果了。如下所示:

控制元件對應多個Behavior.jpg

CoordainatorLayout下的多個子控制元件的依賴互動

瞭解了CoordainatorLayout中四種功能的設計後,現在我們通過一個例子來講解CoordainatorLayout下多個子控制元件的互動。在講解具體的例子之前,我們先回顧一下Behavior中對子控制元件依賴互動提供的方法。如下所示:

public boolean layoutDependsOn(CoordinatorLayout parent, V child, View dependency) { return false; }
public boolean onDependentViewChanged(CoordinatorLayout parent, V child, View dependency) {return false; }
public void onDependentViewRemoved(CoordinatorLayout parent, V child, View dependency) {}
複製程式碼

layoutDependsOn方法介紹:

確定一個控制元件(childView1)依賴另外一個控制元件(childView2)的時候,是通過layoutDependsOn這個方法。其中child是依賴物件(childView1),而dependency是被依賴物件(childView2),該方法的返回值是判斷是否依賴對應view。如果返回true。那麼表示依賴。反之不依賴。一般情況下,在我們自定義Behavior時,我們需要重寫該方法。當layoutDependsOn方法返回true時,後面的onDependentViewChangedonDependentViewRemoved方法才會呼叫。

onDependentViewChanged方法介紹:

當一個控制元件(childView1)所依賴的另一個控制元件(childView2)位置、大小發生改變的時候,該方法會呼叫。其中該方法的返回值,是由childView1來決定的,如果childView1在接受到childView2的改變通知後,childView1的位置或大小發生改變,那麼就返回true,反之返回false。

onDependentViewRemoved方法介紹:

當一個控制元件(childView1)所依賴的另一個控制元件(childView2)被刪除的時候,該方法會呼叫。

Demo展示

下面我們就看一種簡單的例子,來講解在使用CoordainatorLayout下各個兄弟控制元件之間的依賴產生的互動效果。

效果展示.gif

在上述Demo中,我們建立了一個隨手勢滑動的DependedView,並設定了另外兩個依賴DependedView的TextView的Behavior,BrotherChameleonBehavior(變色小弟)與BrotherFollowBehavior(跟隨小弟)。具體程式碼如下所示:

public class DependedView extends View {

    private float mLastX;
    private float mLastY;
    private final int mDragSlop;//最小的滑動距離


    public DependedView(Context context) {
        this(context, null);
    }

    public DependedView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public DependedView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mDragSlop = ViewConfiguration.get(context).getScaledTouchSlop();
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int action = event.getAction();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                mLastX = event.getX();
                mLastY = event.getY();
                break;

            case MotionEvent.ACTION_MOVE:
                int dx = (int) (event.getX() - mLastX);
                int dy = (int) (event.getY() - mLastY);
                if (Math.abs(dx) > mDragSlop || Math.abs(dy) > mDragSlop) {
                    ViewCompat.offsetTopAndBottom(this, dy);
                    ViewCompat.offsetLeftAndRight(this, dx);
                }
                mLastX = event.getX();
                mLastY = event.getY();
                break;

            default:
                break;

        }
        return true;
    }
}
複製程式碼

DependedView邏輯非常簡單,就是重寫了onTouchEvent,監聽滑動,並設定DependedView的位置。我們繼續檢視另外兩個TextView的Behavior。

BrotherChameleonBehavior(變色小弟)程式碼如下所示:

在CoordainatorLayout中要實現子控制元件的依賴互動,我們需要繼承CoordinatorLayout.Behavior。實現layoutDependsOn、onDependentViewChanged、onDependentViewRemoved這三個方法,因為我們例子中不設計關於依賴控制元件的刪除,故沒有重寫onDependentViewRemoved方法。

public class BrotherChameleonBehavior extends CoordinatorLayout.Behavior<View> {

    private ArgbEvaluator mArgbEvaluator = new ArgbEvaluator();

    public BrotherChameleonBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
        return dependency instanceof DependedView;
    }

    @Override
    public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
        int color = (int) mArgbEvaluator.evaluate(dependency.getY() / parent.getHeight(), Color.WHITE, Color.BLACK);
        child.setBackgroundColor(color);
        return false;
    }
}
複製程式碼

BrotherFollowBehavior(跟隨小弟)程式碼如下所示:

public class BrotherFollowBehavior extends CoordinatorLayout.Behavior<View> {

    public BrotherFollowBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
        return dependency instanceof DependedView;//判斷依賴的是否是DependedView
    }

    @Override
    public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
        //如果DependedView的位置、大小改變,跟隨小弟始終在DependedView下面
        child.setY(dependency.getBottom() + 50);
        child.setX(dependency.getX());
        return true;
    }
}
複製程式碼

比較重要的佈局檔案怎麼能忘了吶,對應的佈局如下:

<?xml version="1.0" encoding="utf-8”?>
<android.support.design.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android”
    xmlns:app="http://schemas.android.com/apk/res-autoandroid:layout_width=“match_parent”
    android:layout_height=“match_parent”>


    <com.jennifer.andy.nestedscrollingdemo.view.DependedView
        android:layout_width=“80dp”
        android:layout_height=“40dp”
        android:layout_gravity=“center”
        android:background=“#f00”
        android:gravity=“center”
        android:textColor=“#fff”
        android:textSize="18sp”/>

    <TextView
        android:layout_width=“wrap_content”
        android:layout_height=“wrap_content”
        android:text=“跟隨兄弟”
        app:layout_behavior=".ui.cdl.behavior.BrotherFollowBehavior”/>

    <TextView
        android:layout_width=“wrap_content”
        android:layout_height=“wrap_content”
        android:text=“變色兄弟”
        app:layout_behavior=".ui.cdl.behavior.BrotherChameleonBehavior”/>

</android.support.design.widget.CoordinatorLayout>
複製程式碼

原理講解

大家肯定會很好奇,為什麼簡簡單單的設定了兩個Behavior,DependedView位置發生改變的時候就能通知依賴的兩個TextView呢?這要從DependedView的onTouchEvent方法說起。在onTouchEvent方法中,我們根據手勢修改了DependedView的位置,我們都知道當子控制元件位置、大小發生改變的時候,會導致父控制元件重繪。也就是會呼叫onDraw方法。而CoordainatorLayout在onAttachedToWindow中使用了ViewTreeObserver,並設定了繪製前監聽器OnPreDrawListener。如下所示:

  @Override
    public void onAttachedToWindow() {
        super.onAttachedToWindow();
        resetTouchBehaviors(false);
        if (mNeedsPreDrawListener) {
            if (mOnPreDrawListener == null) {
                mOnPreDrawListener = new OnPreDrawListener();
            }
            final ViewTreeObserver vto = getViewTreeObserver();
            vto.addOnPreDrawListener(mOnPreDrawListener);
        }
       //省略部分程式碼:
    }
複製程式碼

熟悉ViewTreeObserver的小夥伴一定清楚,該類主要是監測整個View樹的變化(這裡的變化指View樹的狀態變化,或者內部的View可見性變化等),我們繼續跟蹤OnPreDrawListener,檢視CoordainatorLayou在繪製前做了什麼。

  class OnPreDrawListener implements ViewTreeObserver.OnPreDrawListener {
        @Override
        public boolean onPreDraw() {
            onChildViewsChanged(EVENT_PRE_DRAW);
            return true;
        }
    }
複製程式碼

我們發現其內部呼叫了onChildViewsChanged(EVENT_PRE_DRAW);方法。我們繼續檢視該方法。

  final void onChildViewsChanged(@DispatchChangeEvent final int type) {
        final int layoutDirection = ViewCompat.getLayoutDirection(this);
        final int childCount = mDependencySortedChildren.size();
        final Rect inset = acquireTempRect();
        final Rect drawRect = acquireTempRect();
        final Rect lastDrawRect = acquireTempRect();

        //獲取內部的所有的子控制元件
        for (int i = 0; i < childCount; i++) {

            final View child = mDependencySortedChildren.get(i);
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();

            //省略部分程式碼…

            //再次獲取內部的所有的子控制元件
            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();

                //呼叫當前子控制元件的Behavior的layoutDependsOn方法判斷是否依賴
                if (b != null && b.layoutDependsOn(this, checkChild, child)) {
                    //省略部分程式碼….
                    final boolean handled;
                    switch (type) {
                        case EVENT_VIEW_REMOVED:
                            b.onDependentViewRemoved(this, checkChild, child);
                            handled = true;
                            break;
                        default:
                            // 如果依賴,那麼就會走當前子控制元件Behavior中的onDependentViewChanged方法。
                            handled = b.onDependentViewChanged(this, checkChild, child);
                            break;
                    }

                }
            }
        }
    //省略部分程式碼…
    }
複製程式碼

觀察程式碼,我們發現程式中使用了一個名為mDependencySortedChildren的集合,通過遍歷該集合,我們可以獲取集合中控制元件的LayoutParam,得到LayoutParam後,我們可以繼續獲取相應的Behavior。並呼叫其layoutDependsOn方法找到所依賴的控制元件,如果找到了當前控制元件所依賴的另一控制元件,那麼就呼叫Behavior中的onDependentViewChanged方法。

看到這裡,多個控制元件依賴互動的原理已經非常清楚了,在CoordainatorLayout下,控制元件A發生位置、大小改變時,會導致CoordainatorLayout重繪。而CoordainatorLayout又設定了繪製前的監聽。在該監聽中,會遍歷mDependencySortedChildren集合,找到依賴A控制元件的其他控制元件。並通知其他控制元件A控制元件發生了改變。當其他控制元件收到該通知後。就可以做自己想做的效果啦。

關於mDependencySortedChildren中儲存的到底是什麼資料還沒有介紹,現在我們就來看看這個集合中是儲存了什麼東西。檢視原始碼,我們發現mDependencySortedChildren中的元素是在onMeasure方法中的prepareChildren()中進行新增的,

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        prepareChildren();
        //省略部分程式碼…
    }
複製程式碼

我們繼續跟蹤prepareChildren()方法。程式碼如下所示:

  private void prepareChildren() {
        mDependencySortedChildren.clear();
        mChildDag.clear();
        //遍歷內部所有孩子
        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);

            // 再次迭代獲取子類控制元件,找到依賴控制元件並新增到"(mchildDag)圖”中
            for (int j = 0; j < count; j++) {
                if (j == i) {
                    continue;
                }
                final View other = getChildAt(j);
                if (lp.dependsOn(this, view, other)) {
                    if (!mChildDag.contains(other)) {
                        //將節點新增到圖中
                        mChildDag.addNode(other);
                    }
                    // 新增邊(依賴的view)
                    mChildDag.addEdge(other, view);
                }
            }
        }

        mDependencySortedChildren.addAll(mChildDag.getSortedList());
        //省略部分程式碼
    }
複製程式碼

在prepareChildren方法中,會遍歷內部所有的子控制元件,並將子控制元件新增到mChildDag集合中,mChildDag的資料結構一種叫圖的資料結構。通過這種資料結構,我們可以快速的找到具有依賴關係控制元件。當將子控制元件的依賴關係處理完畢後。方法最後會將mChildDag集合中全部的資料新增到mDependencySortedChildren集合中去,這樣我們的mDependencySortedChildren就有相應資料啦。

Behavior的例項化

現在我們來講解下一個知識點,在上述文章中,我們描述了CoordainatorLayout中子控制元件的依賴互動原理,以及Behavior依賴相關方法的呼叫時機,我們並沒有講解Behavior是何時被例項化的。下面我們就來看看Behavior是如何被例項化的。

檢視oordainatorLayout原始碼,我們發現在CoordainatorLayout中自定義了佈局引數LayoutParams。並且在LayoutParms類中宣告瞭Behavior成員變數。如下所示:

 public static class LayoutParams extends MarginLayoutParams {
        Behavior mBehavior;
 }
複製程式碼

CoordainatorLayout還重寫了generateLayoutParams方法。

   @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new LayoutParams(getContext(), attrs);
    }
複製程式碼

熟悉自定義View的小夥伴一定熟悉generateLayoutParams方法。當我們自定義ViewGroup時,如果希望我們的子控制元件需要一些特殊的佈局引數或一些特殊的屬性時,我們一般會自定義LayoutParams。比如Relativelayout中LayoutParms中包含LEFT_OF(對應xml佈局中的toLeftOf),RIGHT_OF(對應xml佈局中的toRightOf)屬性。當程式解析xml的時,會根據子控制元件宣告的屬性,生成對應父控制元件下的LayoutParam,通過該LayoutParam,我們就能獲取我們想要的引數啦。而子控制元件Layoutparam的生成,必然會走到父控制元件的LayoutParams的建構函式。檢視CoordainatorLayout下LayoutParams的建構函式:

LayoutParams(Context context, AttributeSet attrs) {
            super(context, attrs);

            //省略部分程式碼….

            //判斷是否宣告瞭Behavior
            mBehaviorResolved = a.hasValue(
                    R.styleable.CoordinatorLayout_Layout_layout_behavior);
            if (mBehaviorResolved) {
                mBehavior = parseBehavior(context, attrs, a.getString(
                        R.styleable.CoordinatorLayout_Layout_layout_behavior));
            }
            a.recycle();

            if (mBehavior != null) {
                // If we have a Behavior, dispatch that it has been attached
                mBehavior.onAttachedToLayoutParams(this);
            }
        }
複製程式碼

觀察程式碼,我們可以發現,子控制元件的佈局引數例項化時,會通過AttributeSet(xml中宣告的標籤)來判斷是否宣告瞭layout_behavior,如果宣告瞭,就呼叫parseBehavior方法來例項化Behavior物件。具體程式碼如下所示:

  static Behavior parseBehavior(Context context, AttributeSet attrs, String name) {
        if (TextUtils.isEmpty(name)) {
            return null;
        }

        final String fullName;
        if (name.startsWith(".")) {
            // Relative to the app package. Prepend the app package name.
            fullName = context.getPackageName() + name;
        } else if (name.indexOf('.') >= 0) {
            // Fully qualified package name.
            fullName = name;
        } else {
            // Assume stock behavior in this package (if we have one)
            fullName = !TextUtils.isEmpty(WIDGET_PACKAGE_NAME)
                    ? (WIDGET_PACKAGE_NAME + '.' + name)
                    : 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>) context.getClassLoader()
                        .loadClass(fullName);
                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);
        }
    }
複製程式碼

parseBehavior方法其實很簡單,就是根據相應的Behavior全限定名稱,通過反射呼叫其建構函式(自定義Behavior的時候,一定要寫建構函式),並例項化其物件。當然例項化Behavior的方法不止一種,Google還為我們提供了註解的方法設定Behavior。例如AppBarLayout中的設定:

@CoordinatorLayout.DefaultBehavior(AppBarLayout.Behavior.class)
public class AppBarLayout extends LinearLayout {}
複製程式碼

當然使用註解的方式,其原理也是通過反射呼叫相應Behavior建構函式,並例項化物件。只是需要通過合適的時間解析註解罷了,因為篇幅的限制,這裡不再講解註解例項化Behavior的原理與時機了,有興趣的小夥伴可以自行研究。

Behavior實現巢狀滑動的原理與過程

在上文CoordinatorLayout簡介中,我們簡單介紹了CoordinatorLayout巢狀滑動事件的傳遞過程與Behavior巢狀滑動的相關方法,現在我們就來了解巢狀滑動從CoordinatorLayout到Behavior的整個傳遞流程。如下所示:

巢狀滑動流程圖.jpg

單從上圖,來理解整個傳遞過程比較困難。我們需要抽絲剝繭,逐個擊破。下面我們就一步步來分析吧。

CoordainatorLayout的事件傳遞過程

Behavior的巢狀滑動其實都是圍繞CoordainatorLayout的的onInterceptTouchEventonTouchEvent方法展開的。那我們先從onInterceptTouchEvent方法講起,具體程式碼如下所示:

public boolean onInterceptTouchEvent(MotionEvent ev) {
        final int action = ev.getActionMasked();
        //省略部分程式碼…
        final boolean intercepted = performIntercept(ev, TYPE_ON_INTERCEPT);
        //省略部分程式碼…
        return intercepted;
    }
複製程式碼

在CoordainatorLayout的的onInterceptTouchEvent方法中,內部其實是呼叫了performIntercept來處理是否攔截事件,我們繼續檢視performIntercept方法。具體程式碼如下所示:

    private boolean performIntercept(MotionEvent ev, final int type) {
        boolean intercepted = false;
        boolean newBlock = false;

        MotionEvent cancelEvent = null;

        final int action = ev.getActionMasked();
        //獲取內部的控制元件集合,並按照z軸進行排序
        final List<View> topmostChildList = mTempList1;
        getTopSortedChildren(topmostChildList);

        // Let topmost child views inspect first
        //獲取所有子view
        final int childCount = topmostChildList.size();
        for (int i = 0; i < childCount; i++) {
            final View child = topmostChildList.get(i);

            //獲取子類的Behavior
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            final Behavior b = lp.getBehavior();

            if ((intercepted || newBlock) && action != MotionEvent.ACTION_DOWN) {
                if (b != null) {
                    //省略部分程式碼….
                    switch (type) {
                        case TYPE_ON_INTERCEPT:
                            //呼叫攔截方法
                            b.onInterceptTouchEvent(this, child, cancelEvent);
                            break;
                        case TYPE_ON_TOUCH:
                            b.onTouchEvent(this, child, cancelEvent);
                            break;
                    }
                }
                continue;
            }

            if (!intercepted && b != null) {
                switch (type) {
                    case TYPE_ON_INTERCEPT:
                       //呼叫behavior的onInterceptTouchEvent,如果攔截就攔截
                        intercepted = b.onInterceptTouchEvent(this, child, ev);
                        break;
                    case TYPE_ON_TOUCH:
                        intercepted = b.onTouchEvent(this, child, ev);
                        break;
                }
                //注意這裡,比較重要找到第一個behavior物件,並賦值
                if (intercepted) {
                    mBehaviorTouchView = child;
                }
            }
            //省略部分程式碼….
        }
        //省略部分程式碼….
        return intercepted;//是否攔截與CoordinatorLayout中子view的behavior有關
    }
複製程式碼

整個方法程式碼的邏輯並不是很難,主要分為兩個步驟:

  • 獲取內部的控制元件集合(topmostChildList),並按照z軸進行排序
  • 迴圈遍歷topmostChildList,獲取控制元件的Behavior,並呼叫Behavior的onInterceptTouchEvent方法判斷是否攔截事件,如果攔截事件,則事件又會交給CoordinatorLayout的onTouchEvent方法處理。

這裡我們先不考慮Behavior攔截事件,一般情況下,Behavior的onInterceptTouchEvent方法基本都是返回false。特殊情況下Behavior事件攔截處理,大家可以在理解本文章所有的知識點後,結合官方提供的BottomSheetBehaviorSwipeDismissBehavior等進行深入的研究,這裡因為篇幅的限制就不再深入的探討了。

那麼假設現在所有的子控制元件中的Behavior.onInterceptTouchEvent返回為false,那麼CoordinatorLayout就不會攔截事件,根據事件傳遞機制,事件就傳遞到了子控制元件中去了。如果我們的子控制元件實現是了NestedScrollingChild介面(如RecyclerView或NestedScrollView),並且在onTouchEvent方法呼叫了相關巢狀滑動API,那麼再根據巢狀滑動機制,會呼叫實現了NestedScrollingParent2介面的父控制元件的相應方法。又因為CoordinatorLayout實現了NestedScrollingParent2介面。那麼就又回到了我們最開始的介紹的巢狀滑動機制了。

這裡的理解非常重要!!!!!非常重要!!!!非常重要!!!如果沒有理解,建議多讀幾遍。

既然最終會呼叫CoordinatorLayout的巢狀滑動方法。那我們來介紹CoordinatorLayout下比較有代表性的巢狀滑動方法,那麼先來看onStartNestedScroll方法。具體程式碼如下:

    public boolean onStartNestedScroll(View child, View target, int axes, int type) {
        boolean handled = false;

        final int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View view = getChildAt(i);
            if (view.getVisibility() == View.GONE) {
                //如果當前控制元件隱藏,則不傳遞
                continue;
            }
            final LayoutParams lp = (LayoutParams) view.getLayoutParams();
            final Behavior viewBehavior = lp.getBehavior();
            if (viewBehavior != null) {
                //判斷Behavior是否接受巢狀滑動事件
                final boolean accepted = viewBehavior.onStartNestedScroll(this, view, child,
                        target, axes, type);
                handled |= accepted;
                //設定當前子控制元件接受接受巢狀滑動
                lp.setNestedScrollAccepted(type, accepted);
            } else {
                lp.setNestedScrollAccepted(type, false);
            }
        }
        return handled;
    }
複製程式碼

在該方法中,我們會發現會獲取所有的內部的控制元件,並呼叫對應Behavior的onStartNestedScroll方法,需要注意的是,如果當前Behavior接受巢狀滑動事件(accepted = true),那麼就會呼叫lp.setNestedScrollAccepted(type, accepted),這段程式碼非常重要,會影響Behavior後續的巢狀方法的執行。我們接著看CoordinatorLayout下的onNestedScrollAccepted方法。程式碼如下所示:

    @Override
    public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes, int type) {
        mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes, type);
        mNestedScrollingTarget = target;

        final int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View view = getChildAt(i);
            final LayoutParams lp = (LayoutParams) view.getLayoutParams();
            if (!lp.isNestedScrollAccepted(type)) {
                continue;
            }

            final Behavior viewBehavior = lp.getBehavior();
            if (viewBehavior != null) {
                viewBehavior.onNestedScrollAccepted(this, view, child, target,
                        nestedScrollAxes, type);
            }
        }
    }
複製程式碼

同樣在onNestedScrollAccepted方法中,也會呼叫所有控制元件的Behavior的onNestedScrollAccepted方法,需要注意的是,在該方法中增加了if (!lp.isNestedScrollAccepted(type))的判斷,也就是說只有Behavior的onStartNestedScroll方法返回true的時候,該方法才會執行。接下來繼續檢視onNestedScroll方法。具體程式碼如下所示:

    @Override
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed) {
        onNestedScroll(target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,
                ViewCompat.TYPE_TOUCH);
    }

    @Override
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed, int type) {
        final int childCount = getChildCount();
        boolean accepted = false;

        for (int i = 0; i < childCount; i++) {
            final View view = getChildAt(i);
            if (view.getVisibility() == GONE) {
                // If the child is GONE, skip…
                continue;
            }

            final LayoutParams lp = (LayoutParams) view.getLayoutParams();
            if (!lp.isNestedScrollAccepted(type)) {
                continue;
            }

            final Behavior viewBehavior = lp.getBehavior();
            if (viewBehavior != null) {
                viewBehavior.onNestedScroll(this, view, target, dxConsumed, dyConsumed,
                        dxUnconsumed, dyUnconsumed, type);
                accepted = true;
            }
        }

        if (accepted) {
            onChildViewsChanged(EVENT_NESTED_SCROLL);
        }
    }

複製程式碼

同樣的,在onNestedScroll方法中,也會判斷當前控制元件對應Behavior是否接受巢狀滑動事件,如果接受就呼叫對應方法。在程式碼的最後一行,我們會發現又呼叫了onChildViewsChanged(EVENT_NESTED_SCROLL)。該行程式碼在CoordinatorLayout下多出巢狀滑動方法中都會呼叫,我們先看onNestedPreScroll方法。然後再來介紹onChildViewsChanged(EVENT_NESTED_SCROLL)方法呼叫下的邏輯處理。onNestedPreScroll程式碼如下所示:

    @Override
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
        onNestedPreScroll(target, dx, dy, consumed, ViewCompat.TYPE_TOUCH);
    }

    @Override
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed, int  type) {
        int xConsumed = 0;
        int yConsumed = 0;
        boolean accepted = false;

        final int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View view = getChildAt(i);
            if (view.getVisibility() == GONE) {
                // If the child is GONE, skip…
                continue;
            }

            final LayoutParams lp = (LayoutParams) view.getLayoutParams();
            if (!lp.isNestedScrollAccepted(type)) {
                continue;
            }

            final Behavior viewBehavior = lp.getBehavior();
            if (viewBehavior != null) {
                mTempIntPair[0] = mTempIntPair[1] = 0;
                viewBehavior.onNestedPreScroll(this, view, target, dx, dy, mTempIntPair, type);

                xConsumed = dx > 0 ? Math.max(xConsumed, mTempIntPair[0])
                        : Math.min(xConsumed, mTempIntPair[0]);
                yConsumed = dy > 0 ? Math.max(yConsumed, mTempIntPair[1])
                        : Math.min(yConsumed, mTempIntPair[1]);

                accepted = true;
            }
        }

        consumed[0] = xConsumed;
        consumed[1] = yConsumed;

        if (accepted) {
            //這裡也呼叫了onChildViewsChanged方法
            onChildViewsChanged(EVENT_NESTED_SCROLL);
        }
    }
複製程式碼

同樣的在該方法中,也是呼叫子控制元件的Behavior對應的方法,並最後呼叫了onChildViewsChanged(EVENT_NESTED_SCROLL)。該方法與其他方法的最大的不同就是,用int[] mTempIntPair = new int[2]記錄了控制元件在X軸與Y軸的距離,比較並獲取內部子控制元件中最大的消耗距離後,最後將最大的消耗距離,通過int[]consumed陣列在傳回NestedScrollingChild。

在CoordinatorLayout下的比較重要的巢狀滑動方法基本上講解完畢了。餘下的onNestedPreFlingonNestedFling方法都大同小異,這裡就不再講解了,現在講解一下當onChildViewsChanged(EVENT_NESTED_SCROLL)方法呼叫下的邏輯處理。程式碼如下所示:

   final void onChildViewsChanged(@DispatchChangeEvent final int type) {
        final int layoutDirection = ViewCompat.getLayoutDirection(this);
        final int childCount = mDependencySortedChildren.size();
        // 省略部分程式碼…
        for (int i = 0; i < childCount; i++) {
            // 省略部分程式碼…
            for (int j = i + 1; j < childCount; j++) {
                final View checkChild = mDependencySortedChildren.get(j);

                //獲取對應控制元件的Behavior
                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()) {
                        //檢查當前控制元件的巢狀滑動的標誌位,如果為true,表示已經巢狀滑動過了,那麼就跳過
                        checkLp.resetChangedAfterNestedScroll();
                        continue;
                    }

                    final boolean handled;
                    //這裡判斷所依賴的物件是否移除或改變
                    switch (type) {
                        case EVENT_VIEW_REMOVED://移除
                            //當型別為EVENT_VIEW_REMOVED時,表示該控制元件移除,我們要通知依賴該控制元件的其他控制元件,該控制元件已經被移除了
                            b.onDependentViewRemoved(this, checkChild, child);
                            handled = true;
                            break;
                        default:
                            //預設情況下,通知通知依賴該控制元件的其他控制元件,該控制元件發生了改變
                            handled = b.onDependentViewChanged(this, checkChild, child);
                            break;
                    }

                    if (type == EVENT_NESTED_SCROLL) {
                        // If this is from a nested scroll, set the flag so that we may skip
                        // any resulting onPreDraw dispatch (if needed)
                        //如果當前是巢狀滑動,那麼就需要設定該標誌位為true,方便跳過OnPreDraw方法
                        checkLp.setChangedAfterNestedScroll(handled);
                    }
                }
            }
        }
        //省略部分程式碼
    }
複製程式碼

整個方法分為一下幾個步驟:

  • 獲取控制元件的Behavior,呼叫其layoutDependsOn方法判斷是否依賴,找到依賴該控制元件的其他控制元件。
  • 隨後呼叫控制元件的LayoutParams的getChangedAfterNestedScroll()方法,檢查當前控制元件的巢狀滑動的標誌位,如果為true,表示已經巢狀滑動過了,那麼就跳過。如果該標誌位為false,那程式繼續往下走。
  • 如果找到依賴控制元件其巢狀滑動標誌位也為false,那麼接下來會呼叫依賴控制元件的Behavior的onDependentViewChanged方法,通知其他控制元件依賴的控制元件位置、大小發生了改變。
  • 通知完畢後,如果其他的控制元件位置、大小發生了改變,那麼需要在onDependentViewChanged方法中返回為true(handlet=true),如果type==EVENT_NESTED_SCROLL那麼需要呼叫ChangedAfterNestedScroll,設定當前控制元件已經巢狀滑動的標誌位為true

整個流程並不是很複雜,但是我向下大家會有一個疑問,就是為什麼type==EVENT_NESTED_SCROLL時,需要設定控制元件的巢狀滑動標誌位呢?為什麼當該標誌位為true的時候,就需要跳過迴圈呢?其實這兩個問題並不難,我們看下圖:

邏輯理解.jpg

根據上圖,我們來回顧一下整個機制的巢狀滑動過程。

  • 當CoordinatorLayout中子控制元件的Behvior預設不攔截事件,且內部有NestedScrollingChild控制元件的時候。最終會呼叫到某個控制元件的Behavior的巢狀相關方法,這裡以A控制元件為例。
  • 在A控制元件部分相關巢狀方法中,會呼叫onChildViewsChanged(EVENT_NESTED_SCROLL)。在該方法中又會通知其他依賴A控制元件的其他控制元件。並呼叫onDependentViewChanged方法(上圖中,藍色與紅色部分)。
  • 因為A控制元件在執行部分巢狀滑動方法後,會導致父控制元件重繪,所以又會回到本文最初講解的onPreDraw方法,在該方法中,又會呼叫onChildViewsChanged(EVENT_PRE_DRAW)(上圖中黃色部分)。

根據當前整體流程,我們可以推斷出,如果不通過設定控制元件的巢狀滑動標誌位的話,那麼其他依賴A控制元件的Behavior就會呼叫兩次onDependentViewChanged,如果說其他控制元件都在該方法中發生了位置、或大小的改變。那麼整個過程就會出現問題!!!!!。所以說我們需要一個標誌位來區分繪製與巢狀滑動。

當然這個巢狀滑動的標誌位,是與Behavior的onDependentViewChanged方法的返回值有關,所以在平時的開發中,我們一定要注意。如果我們當我們對目標控制元件的位置、大小造成了改變之後,我們一定要將該方法的返回值返回為true

Behavior的佈局

還有最後兩個知識點了,大家加油啊~~~

我們都知道CoordinatorLayout中被谷歌稱為超級FrameLayout,其中的原因不僅因為其佈局方式與測量方式與FrameLayout非常相似以外,最主要的原因是CoordinatorLayout可以將滑動事件、佈局、測量交給子控制元件中的Behavior。現在我們就來看看CoordinatorLayout下的佈局實現。檢視其onLayout方法。

 @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        final int layoutDirection = ViewCompat.getLayoutDirection(this);
        final int childCount = mDependencySortedChildren.size();
        for (int i = 0; i < childCount; i++) {
            final View child = mDependencySortedChildren.get(i);
            if (child.getVisibility() == GONE) {
                // If the child is GONE, skip…
                continue;
            }

            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            final Behavior behavior = lp.getBehavior();
            //獲取子控制元件的Behavior方法,並呼叫其onLayoutChild方法判斷子控制元件是否需要自己佈局
            if (behavior == null || !behavior.onLayoutChild(this, child, layoutDirection)) {
                onLayoutChild(child, layoutDirection);
            }
        }
    }
複製程式碼

從程式碼中我們可以看出,在對子 View 進行遍歷的時候,CoordinatorLayout有主動向子控制元件的Behavior傳遞佈局的要求,如果Behavior呼叫onLayoutChild了方法自主佈局了子控制元件,則以它的結果為準,否則將呼叫onLayoutChild方法親自佈局。這裡就不對CoordinatorLayout下的onLayoutChild方法進行過多的描述了,大家知道這個方法類似於FrameLayout的佈局就行了。

Behavior的佈局時機

其實肯定會有小夥伴會疑惑,什麼樣的情況下,我們需要設定自主佈局呢?(也就是behavior.onLayoutChild()方法返回true)。在上文中我們說過了CoordinatorLayout佈局方式是類似於FrameLayout的。在FrameLayout的佈局中是隻支援Gravity來設定佈局的。如果我們需要自主的擺放控制元件中的位置,那麼我們就需要重寫Behavior的onLayoutChild方法。並設定該方法返回結果為true。

Behavior的測量

最後一個知識點了!!!!!Behavior的測量。依然還是通過CoordinatorLayout傳遞過來的。我們檢視CoordinatorLayout的onMeasure方法。程式碼如下所示:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //省略部分程式碼….
        final int childCount = mDependencySortedChildren.size();
        for (int i = 0; i < childCount; i++) {
            final View child = mDependencySortedChildren.get(i);
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            int childWidthMeasureSpec = widthMeasureSpec;
            int childHeightMeasureSpec = heightMeasureSpec;

            final Behavior b = lp.getBehavior();
            //呼叫Behavior的測量方法。
            if (b == null || !b.onMeasureChild(this, child, childWidthMeasureSpec, keylineWidthUsed,
                    childHeightMeasureSpec, 0)) {
                onMeasureChild(child, childWidthMeasureSpec, keylineWidthUsed,
                        childHeightMeasureSpec, 0);
            }

            widthUsed = Math.max(widthUsed, widthPadding + child.getMeasuredWidth() +
                    lp.leftMargin + lp.rightMargin);

            heightUsed = Math.max(heightUsed, heightPadding + child.getMeasuredHeight() +
                    lp.topMargin + lp.bottomMargin);
            childState = View.combineMeasuredStates(childState, child.getMeasuredState());
        }

        final int width = View.resolveSizeAndState(widthUsed, widthMeasureSpec,
                childState & View.MEASURED_STATE_MASK);
        final int height = View.resolveSizeAndState(heightUsed, heightMeasureSpec,
                childState << View.MEASURED_HEIGHT_STATE_SHIFT);
        setMeasuredDimension(width, height);
    }
複製程式碼

上面的程式碼中,我還是省略了一些不重要的程式碼。觀察上述程式碼,我們發現該方法與CoordinatorLayout的佈局邏輯非常相似,也是對子控制元件進行遍歷,並調那個用子控制元件的Behavior的onMeasureChild方法,判斷是否自主測量,如果為true,那麼則以子控制元件的測量為準。當子控制元件測量完畢後。會通過widthUsedheightUsed 這兩個變數來儲存CoordinatorLayout中子控制元件最大的尺寸。這兩個變數的值,最終將會影響CoordinatorLayout的寬高。

Behavior的測量時機

還是相似的問題,在什麼樣的情況下,我們需要重寫BehavioronMeasureChild方法來自主測量控制元件呢?當你的控制元件需要重新設定位置的時候你要考慮是否需要重寫該方法。什麼意思呢?看下圖所示:

空白區域.jpg

在上圖中我們定義了兩個控制元件A與B,我們假設這兩個控制元件處於這三個條件下:

  • A、B控制元件都在CoordinatorLayout下,且A、B控制元件位置關係為控制元件A在B控制元件的下方。
  • A控制元件的高度為match_parent或者wrap_content
  • A、B控制元件的巢狀滑動關係為:B控制元件先處理巢狀滑動事件,當控制元件B向上滑動至隱藏後,控制元件A才能開始滑動。

那麼根據上述條件,在滾動的過程中,我們會發現一個問題,就是當我們的控制元件A逐漸滑動到頂部時,我們會發現螢幕下方會出現一個空白區域,那原因是什麼呢?其實很簡單,當控制元件A高度為match_parent`或者`wrap_content時,根據View的測量規則,控制元件A實際的高度就是整個控制元件剩餘的高度(螢幕高度-控制元件B的高度),所以當控制元件B滾出螢幕後,那麼就會出現一段空白。

那麼為了使控制元件A在滑動過程中始終填充整個螢幕,我們需要在CoordinatorLayout測量該控制元件的高度之前,讓控制元件自主的去測量高度,那麼這個時候,Behavior的onMeasureChild方法就派上用場了。我們可以重寫該方法並設定當前控制元件A的高度為整個螢幕的高度。當然如何通過Behavior的onMeasureChild重新設定控制元件的高度是我們後續文章將講解的知識,大家如果有興趣的話,可以關注後續文章。

最後

看到這裡的小夥伴真的非常值得鼓勵。點贊!!!!!關於CoordinatorLayout的整個下的Behavior確實理解起來需要花費不少的時間。我本人從理解到寫這篇部落格零零散散也花費了兩週多的時間。雖然說這塊知識點比較偏門。但是還是希望能幫助到有需要的小夥伴們。能有幸幫助到大家,我也非常開心了。

相關文章