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

前言

在上篇文章中,咱們介紹了NestedScrolling(嵌套滑動)機制,介紹了子控件與父控件嵌套滑動的處理。如今咱們來了解谷歌大大爲咱們提供的另外一個控件的交互佈局CoordainatorLayout。CoordainatorLayout對於Android開發老司機來講確定不會陌生,做爲控制內部一個或多個的子控件協同交互的容器,開發者能夠經過設置Behavior去控制多個控件的協同交互效果,測量尺寸、佈局位置及觸摸響應。做爲谷歌推出的明星組件,分析CoordainatorLayout的文章已經是數不勝數。而分析整個CoordainatorLayout原理的相關資料在網上不多,所以本文會把重點放在分析其內部原理上。java

經過閱讀該文,你能瞭解以下知識點:android

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

該博客中涉及到的示例,在NestedScrollingDemo項目中都有實現,你們能夠按需自取。git

CoordainatorLayout簡介

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

  • 處理子控件的依賴下的交互
  • 處理子控件的嵌套滑動
  • 處理子控件的測量與佈局
  • 處理子控件的事件攔截與響應。

而上述四個功能,都依託於CoordainatorLayout中提供的一個叫作Behavior的「插件」。Behavior內部也提供了相應方法來對應這四個不一樣的功能,具體以下所示:數組

Behavior方法設置.jpg

在下面的文章中不會介紹Behavior嵌套滑動相關方法的做用,若是須要了解這些方法的做用,建議參看自定義View事件篇進階篇(二)-自定義NestedScrolling實戰文章下的方法介紹。markdown

那如今咱們就一塊兒來看看,谷歌是怎麼圍繞Behavior對上述四個功能進行設計的把。數據結構

子控件依賴下的交互設計

對於子控件的依賴交互,谷歌是這樣設計的:app

依賴下的交互.jpg

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

子控件的嵌套滑動的設計

對於子控件的嵌套滑動,谷歌是這樣設計的:函數

嵌套滑動設計.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是如何被實例化的。

查看CoordainatorLayout源碼,咱們發如今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確實理解起來須要花費很多的時間。我本人從理解到寫這篇博客零零散散也花費了兩週多的時間。雖說這塊知識點比較偏門。可是仍是但願能幫助到有須要的小夥伴們。能有幸幫助到你們,我也很是開心了。

相關文章
相關標籤/搜索