[Android]CoordinatorLayout簡介(三)手寫一個CoordinatorLayout怎么樣廓译?

參考資料

CoordinatorLayout簡介(一)CoordinatorLayout的簡單使用
CoordinatorLayout簡介(二)幾種系統(tǒng)默認Behavior的使用
CoordinatorLayout簡介(三)手寫一個CoordinatorLayout怎么樣?

前言

這是CoordinatorLayout系列的第三篇文章,本來按計劃是準備解析源碼的,但是粗略規(guī)劃了一下,發(fā)現(xiàn)竟然無從下口袖扛,根本原因還是CoordinatorLayout體系過于紛繁復雜,其中包含了嵌套滑動框架的實現(xiàn)十籍、事件傳遞和攔截蛆封、坐標系變換、View繪制流程等等妓雾,以至于不知從何說起娶吞。

源碼中因為穩(wěn)定性和兼容性的需要,以及各種效果的事件械姻,包含了過多非主流程的代碼妒蛇,這給我們閱讀源碼也帶來了一定的困難机断,在閱讀的過程中經(jīng)常感覺亂花漸欲迷人眼,為邏輯所困绣夺,越陷越深吏奸,無奈放棄

本文將手動實現(xiàn)一個CoordinatorLayout+AppBarLayout效果的組件,盡量刪掉源碼中各種分支邏輯和變換邏輯陶耍,著重于CoordinatorLayout主流程奋蔚,實現(xiàn)方式盡可能還原原生的CoordinatorLayout+AppBarLayout,主要是讓我們可以更加容易理解CoordinatorLayout的工作過程

正文

效果圖:

NestedParentView[00_00_02--00_00_09].gif

實現(xiàn)

先看下工程結(jié)構(gòu):


image.png

和原生CoordinatorLayout的對應關(guān)系:

自定義 原生
NestedParentView CoordinatorLayout
NestedChildView CoordinatorLayout 中定義的滑動組件
HeaderView AppBarLayout
HeaderBehavior AppBarLayout$Behavor
ScrollBehavior AppBarLayout$ScrollingViewBehavior

xml中的布局:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <com.dafasoft.custombehavior.view.NestedParentView
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <com.dafasoft.custombehavior.view.HeaderView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            app:behavior="@string/header_behavior">
            <ImageView
                android:layout_width="match_parent"
                android:layout_height="230dp"
                android:scaleType="fitXY"
                app:headerScrollFlag="1"
                android:src="@drawable/yellow_zero"/>

            <TextView
                android:layout_width="match_parent"
                android:layout_height="30dp"
                android:background="@color/chip_background_invalid"
                android:layout_alignParentBottom="true"
                android:gravity="center"
                android:textColor="@color/white"
                android:text="頁面標題欄"/>
        </com.dafasoft.custombehavior.view.HeaderView>

        <com.dafasoft.custombehavior.view.NestedChildView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical"
            app:behavior="@string/scroll_behavior">
           <ScrollView
               android:layout_width="match_parent"
               android:layout_height="match_parent">
               <LinearLayout
                   android:layout_width="match_parent"
                   android:layout_height="match_parent"
                   android:orientation="vertical">
                   <View
                       android:layout_width="match_parent"
                       android:layout_height="350dp"
                       android:background="@color/black"/>

                   <View
                       android:layout_width="match_parent"
                       android:layout_height="350dp"
                       android:background="@color/purple_200"/>

                   <View
                       android:layout_width="match_parent"
                       android:layout_height="350dp"
                       android:background="@color/teal_200"/>
               </LinearLayout>
           </ScrollView>
        </com.dafasoft.custombehavior.view.NestedChildView>
    </com.dafasoft.custombehavior.view.NestedParentView>
</RelativeLayout>

其中自定義屬性behaviorheaderScrollFlag分別對應CoordinatorLayout組件中的layout_behaviorlayout_scrollFlags,這需要我們在attrs.xml中聲明:

 <declare-styleable name="NestedParentView">
        <attr name="behavior" format="string" />
    </declare-styleable>

    <declare-styleable name="HeaderView">
        <attr name="headerScrollFlag" format="integer" />
    </declare-styleable>

behavior對應的兩個String:

    <string name="scroll_behavior">com.dafasoft.custombehavior.behavior.ScrollBehavior</string>
    <string name="header_behavior">com.dafasoft.custombehavior.behavior.HeaderBehavior</string>

NestedChildView和HeaderView均是NestedParentView的子View,它們的LayoutParams屬性是在NestedParentView中進行解析的,解析方法:

public LayoutParams(Context c, AttributeSet attrs) {
            super(c, attrs);
            TypedArray array = c.obtainStyledAttributes(attrs, R.styleable.NestedParentView);
            behavior = parseBehavior(c, attrs, array.getString(R.styleable.NestedParentView_behavior));
            array.recycle();
        }

        static Behavior parseBehavior(Context context, AttributeSet attrs, String name) {
        if (TextUtils.isEmpty(name)) {
            return null;
        }
        try {
            // 獲取設置中behavior的值烈钞,通過反射初始化其實例
            final Class<Behavior> clazz = (Class<Behavior>) Class.forName(name, true,
                    context.getClassLoader());
            Constructor<Behavior> c = clazz.getConstructor(CONSTRUCTOR_PARAMS);
            c.setAccessible(true);
            return c.newInstance(context, attrs);
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        return null;
    }

NestedParentView$LayoutParams的初始化在LayoutInflate的過程中泊碑,這一部分屬于XML解析的范疇,這里不多講

NestedParentView$LayoutParams的總體設計:

public static class LayoutParams extends ViewGroup.MarginLayoutParams {

        private NestedParentView.Behavior behavior; // 對應View的Behavior
        private boolean mDidAcceptNestedScrollTouch; // 對應View接收嵌套滑動的觸摸事件
        public int gravity = Gravity.NO_GRAVITY;

        public LayoutParams(Context c, AttributeSet attrs) {
            super(c, attrs);
            TypedArray array = c.obtainStyledAttributes(attrs, R.styleable.NestedParentView);
            behavior = parseBehavior(c, attrs, array.getString(R.styleable.NestedParentView_behavior));
            array.recycle();
        }

        public LayoutParams(int width, int height) {
            super(width, height);
        }

        public LayoutParams(MarginLayoutParams source) {
            super(source);
        }

        public LayoutParams(ViewGroup.LayoutParams source) {
            super(source);
        }

        public NestedParentView.Behavior getBehavior() {
            return behavior;
        }

        public void setBehavior(NestedParentView.Behavior behavior) {
            this.behavior = behavior;
        }

        void setNestedScrollAccepted(int type, boolean accept) {
            switch (type) {
                case ViewCompat.TYPE_TOUCH:
                    mDidAcceptNestedScrollTouch = accept;
                    break;
            }
        }

        boolean isNestedScrollAccepted(int type) {
            switch (type) {
                case ViewCompat.TYPE_TOUCH:
                    return mDidAcceptNestedScrollTouch;
            }
            return false;
        }
    }

    static Behavior parseBehavior(Context context, AttributeSet attrs, String name) {
        if (TextUtils.isEmpty(name)) {
            return null;
        }
        try {
            // 獲取設置中behavior的值毯欣,通過反射初始化其實例
            final Class<Behavior> clazz = (Class<Behavior>) Class.forName(name, true,
                    context.getClassLoader());
            Constructor<Behavior> c = clazz.getConstructor(CONSTRUCTOR_PARAMS);
            c.setAccessible(true);
            return c.newInstance(context, attrs);
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        return null;
    }

NestedParentView及其子View的布局初始化

這里會涉及到一些View繪制的知識馒过,還不太熟悉的同學可以趁這個機會復習一下

直接看代碼:
NestedParentView#onMeasure:

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        final int paddingLeft = getPaddingLeft();
        final int paddingTop = getPaddingTop();
        final int paddingRight = getPaddingRight();
        final int paddingBottom = getPaddingBottom();

        final int widthPadding = paddingLeft + paddingRight;
        final int heightPadding = paddingTop + paddingBottom;
        int widthUsed = getSuggestedMinimumWidth();
        int heightUsed = getSuggestedMinimumHeight();
        int childState = 0;


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

            final LayoutParams lp = (LayoutParams) child.getLayoutParams();

            int keylineWidthUsed = 0;

            int childWidthMeasureSpec = widthMeasureSpec;
            int childHeightMeasureSpec = heightMeasureSpec;


            final Behavior b = lp.getBehavior();
            // 如果child的Behavior不為null且onMeasureChild的工作交給Behavior完成,則NestedParentView不處理子View的measure,否則交給系統(tǒng)處理
            if (b == null || !b.onMeasureChild(this, child, childWidthMeasureSpec, keylineWidthUsed,
                    childHeightMeasureSpec, 0)) {
                onMeasureChild(child, childWidthMeasureSpec, keylineWidthUsed,
                        childHeightMeasureSpec, 0);
            }
            // NestedParentView繼承于ViewGroup酗钞,它所占用的寬高就是最大的子View占的寬或高
            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);
    }

NestedParentView#onLayout:
onLayout方法和onMeasure的邏輯類似腹忽,都是看behavior要不要處理,behavior不處理交給View作默認處理

protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        final int layoutDirection = ViewCompat.getLayoutDirection(this);
        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View child = getChildAt(i);
            if (child.getVisibility() == GONE) {
                continue;
            }

            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            final NestedParentView.Behavior behavior = lp.getBehavior();

            if (behavior == null || !behavior.onLayoutChild(this, child, layoutDirection)) {
                onLayoutChild(child, layoutDirection);
            }
        }
    }

接著看下兩個Behavior中onMeasure 和 onLayoutChild的實現(xiàn)

首先看HeaderBehavior:

@Override
    public boolean onMeasureChild(NestedParentView parent, HeaderView child,
                                  int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec,
                                  int heightUsed) {
        final NestedParentView.LayoutParams lp =
                (NestedParentView.LayoutParams) child.getLayoutParams();
        if (lp.height == NestedParentView.LayoutParams.WRAP_CONTENT) {
            // 如果View的高度被設置為WRAP_CONTENT,NestedParentView默認會束縛這個View在其本身所占區(qū)域內(nèi)砚作,因為HeaderView是可以滑動的窘奏,
            // 因此需要設置MesaureSpce為UNSPECIFIED從而允許其超過其父布局的高度
            parent.onMeasureChild(child, parentWidthMeasureSpec, widthUsed,
                    View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), heightUsed);
            return true;
        }

        // Let the parent handle it as normal
        return super.onMeasureChild(parent, child, parentWidthMeasureSpec, widthUsed,
                parentHeightMeasureSpec, heightUsed);
    }

    @Override
    public boolean onLayoutChild(NestedParentView parent, HeaderView child, int layoutDirection) {
        // First let lay the child out
        layoutChild(parent, child, layoutDirection);

        // 初始化ViewOffsetHelper這個很重要
        if (mViewOffsetHelper == null) {
            mViewOffsetHelper = new ViewOffsetHelper(child);
        }
        // ViewOffsetHelper處理View的layout
        mViewOffsetHelper.onViewLayout();

        // 設置View的邊界
        if (mTempTopBottomOffset != 0) {
            mViewOffsetHelper.setTopAndBottomOffset(mTempTopBottomOffset);
            mTempTopBottomOffset = 0;
        }
        if (mTempLeftRightOffset != 0) {
            mViewOffsetHelper.setLeftAndRightOffset(mTempLeftRightOffset);
            mTempLeftRightOffset = 0;
        }

        return true;
    }

    protected void layoutChild(NestedParentView parent, HeaderView child, int layoutDirection) {
        // Let the parent lay it out by default
        parent.onLayoutChild(child, layoutDirection);
    }

在HeaderBehavior的方法中,有一個非常重要的任務就是ViewOffsetHelper的初始化及其對View的Layout過程的處理葫录,ViewOffsetHelper這個類就是后面我們處理嵌套滑動最重要的一個類着裹,它主要負責NestedParentView的子View的坐標變化,通過坐標變化實現(xiàn)嵌套滑動的效果

再來看ScrollBehavior的實現(xiàn):

@Override
    public boolean onMeasureChild(NestedParentView parent, View child,
                                  int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec,
                                  int heightUsed) {
        final int childLpHeight = child.getLayoutParams().height;
        if (childLpHeight == ViewGroup.LayoutParams.MATCH_PARENT
                || childLpHeight == ViewGroup.LayoutParams.WRAP_CONTENT) {

            // 尋找headerView
            View header = null;
            int count = parent.getChildCount();
            for (int i = 0; i < count; i++) {
                if (parent.getChildAt(i) instanceof HeaderView) {
                    header = parent.getChildAt(i);
                }
            }

            if (header != null) {
                if (ViewCompat.getFitsSystemWindows(header)
                        && !ViewCompat.getFitsSystemWindows(child)) {
                    ViewCompat.setFitsSystemWindows(child, true);

                    if (ViewCompat.getFitsSystemWindows(child)) {
                        child.requestLayout();
                        return true;
                    }
                }

                int availableHeight = View.MeasureSpec.getSize(parentHeightMeasureSpec);
                if (availableHeight == 0) {
                    availableHeight = parent.getHeight();
                }

                // 計算ScrollView的可繪制高度压昼,其可繪制高度為父布局的可繪制高度 - header的不可滑動區(qū)域
                final int height = availableHeight - header.getMeasuredHeight() + getScrollRange(header);
                final int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(height,
                        childLpHeight == ViewGroup.LayoutParams.MATCH_PARENT
                                ? View.MeasureSpec.EXACTLY
                                : View.MeasureSpec.AT_MOST);

                parent.onMeasureChild(child, parentWidthMeasureSpec,
                        widthUsed, heightMeasureSpec, heightUsed);

                return true;
            }
        }
        return false;
    }

    @Override
    public boolean onLayoutChild(@NonNull NestedParentView parent, @NonNull View child, int layoutDirection) {
        View headerView = null;
        int count = parent.getChildCount();
        for (int i = 0; i < count; i++) {
            if (parent.getChildAt(i) instanceof HeaderView) {
                headerView = parent.getChildAt(i);
            }
        }
        if (headerView != null) {
            final NestedParentView.LayoutParams lp =
                    (NestedParentView.LayoutParams) child.getLayoutParams();
            final Rect available = mTempRect1;

            // 獲取設置了ScrollBehavior屬性的View的可布局區(qū)域求冷,將其置于HeaderView的下方
            available.set(parent.getPaddingLeft() + lp.leftMargin,
                    headerView.getBottom() + lp.topMargin,
                    parent.getWidth() - parent.getPaddingRight() - lp.rightMargin,
                    parent.getHeight() + headerView.getBottom()
                            - parent.getPaddingBottom() - lp.bottomMargin);

            final Rect out = mTempRect2;
            GravityCompat.apply(resolveGravity(lp.gravity), child.getMeasuredWidth(),
                    child.getMeasuredHeight(), available, out, layoutDirection);


            child.layout(out.left, out.top, out.right, out.bottom);
        }
        return true;
    }

ScrollBehavior的onMeasureChild和onLayoutChild的主要工作是計算設置了ScrollBehavior的View(這里簡稱ScrollableView)可繪制高度和其擺放位置

ScrollableView的可繪制高度計算方式為 NestedParentView的高度 - HeaderView的不可滑動區(qū)域,這樣做的結(jié)果很明顯 就是當HeaderView滑動到需要懸浮處理時窍霞,ScrollableView正好可以全部顯示出來

onLayoutChild負責ScrollableView的擺放,實現(xiàn)方法就是通過尋找HeaderView,將HeaderView的底邊設為ScrollableView的頂邊拯坟,再結(jié)合onMeasureChild后確定的高度但金,即可確定ScrollableView的繪制Rect

通過上面對NestedParentView和Behavior的拆分,我們應該能理解為什么我們自定義實現(xiàn)一些CoordinatorLayout的炫酷效果時要自定義Behavior了郁季,也正因為Behavior如此強大的功能冷溃,CoordinatorLayout才會變?yōu)閷V胃鞣N花里胡哨的利器

聯(lián)動效果的實現(xiàn):

我們只是將NestedParentView和它的子View擺放好肯定是遠遠不夠的,關(guān)鍵要讓它們聯(lián)動起來梦裂,

原生CoordinatorLayout使用的是NestedParent 和 NestedChild組件

具體的實現(xiàn)可以看NestedScrollingChild2, NestedScrollingChild3似枕、NestedScrollingParent2, NestedScrollingParent3這四個接口文件中方法的定義,總之年柠,通過一些操作繼承于NestedScrollingChild2, NestedScrollingChild3的View是可以和繼承于NestedScrollingParent2, NestedScrollingParent3的View進行聯(lián)動的

現(xiàn)在將NestedChildView繼承于NestedScrollingChild2, NestedScrollingChild3凿歼,看下onTouchEvent:

NestedChildView#onTouchEvent

@Override
    public boolean onTouchEvent(MotionEvent event) {

        if (event.getAction() == MotionEvent.ACTION_DOWN) {
            mNestedOffsets[0] = mNestedOffsets[1] = 0;
        }

        final MotionEvent vtev = MotionEvent.obtain(event);
        vtev.offsetLocation(mNestedOffsets[0], mNestedOffsets[1]);

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mLastTouchX = (int) event.getX();
                mLastTouchY = (int) event.getY();
                int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
                // 設置滑動為垂直滑動
                nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
                // 調(diào)用NestedScrollingChild2#startNestedScroll
                startNestedScroll(nestedScrollAxis, TYPE_TOUCH);
                break;

            case MotionEvent.ACTION_MOVE:

                final int x = (int) (event.getX());
                final int y = (int) (event.getY());
                int dy = mLastTouchY - y;
                // 滑動布局的修復值
                mReusableIntPair[0] = 0;
                mReusableIntPair[1] = 0;

                // 將嵌套滑動事件分發(fā)出去
                if (dispatchNestedPreScroll(0, dy, mReusableIntPair, mScrollOffset, TYPE_TOUCH)) {
                    dy -= mReusableIntPair[1];
                    // 嵌套滑動的總距離
                    mNestedOffsets[0] += mScrollOffset[0];
                    mNestedOffsets[1] += mScrollOffset[1];
                    // 禁止父布局攔截事件
                    getParent().requestDisallowInterceptTouchEvent(true);
                }
                // 設置最后的接觸坐標為 實際坐標 - 嵌套滑動的距離
                mLastTouchX = x - mScrollOffset[0];
                mLastTouchY = y - mScrollOffset[1];
                Log.d("zyl", String.format("mLastTouchY = %d     y = %d   mNestedOffsetsY = %d    mScrollOffsetY = %d  dy = %d  mReusableIntPair = %d", mLastTouchY, y, mNestedOffsets[1], mScrollOffset[1], dy, mReusableIntPair[1]));
                if (dy != 0) {
                    // NestedPreScroll 結(jié)束,開始本View的滑動,這里用ScrollView的滑動來模擬
                    ((ScrollView)getChildAt(0)).scrollBy(0, dy);
                }
                break;

            case MotionEvent.ACTION_UP:
                break;

            default:
                break;
        }
        return true;
    }

這里主要的工作答憔,在ACTION_DOWN的時候味赃,傳遞startNestedScroll事件至父布局,這個事件主要做兩件事情:

  1. 父布局根據(jù)該View確定嵌套滑動事件的子View
  2. 尋找接受嵌套滑動的其他View(在本案例中為HeaderView)

接著看ACTION_MOVE:
這里的工作主要有幾個
1.將dispatchNestedPreScroll傳遞給父布局
2.根據(jù)父布局對View坐標系的變化虐拓,修改mLastTouchX和mLastTouchY
3.計算總的嵌套滑動距離
4.處理本View的滑動

看下實現(xiàn):
dispatchNestedPreScroll方法:

    @Override
    public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed, @Nullable int[] offsetInWindow, int type) {
        return getScrollingChildHelper().dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow,
                type);
    }

其中NestedScrollingChildHelper是在View初始化的時候進行的初始化心俗,這是系統(tǒng)給我們提供的工具類,主要負責nestedScrollingChild 和nestedScrollingParent的通信
NestedScrollingChildHelper#dispatchNestedPreScroll

public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
            @Nullable int[] offsetInWindow, @NestedScrollType int type) {
        if (isNestedScrollingEnabled()) {
            final ViewParent parent = getNestedScrollingParentForType(type);
            if (parent == null) {
                return false;
            }

            if (dx != 0 || dy != 0) {
                int startX = 0;
                int startY = 0;
                if (offsetInWindow != null) {
                    mView.getLocationInWindow(offsetInWindow);
                    startX = offsetInWindow[0];
                    startY = offsetInWindow[1];
                }

                if (consumed == null) {
                    consumed = getTempNestedScrollConsumed();
                }
                consumed[0] = 0;
                consumed[1] = 0;
                ViewParentCompat.onNestedPreScroll(parent, mView, dx, dy, consumed, type);

                if (offsetInWindow != null) {
                    mView.getLocationInWindow(offsetInWindow);
                    offsetInWindow[0] -= startX;
                    offsetInWindow[1] -= startY;
                }
                return consumed[0] != 0 || consumed[1] != 0;
            } else if (offsetInWindow != null) {
                offsetInWindow[0] = 0;
                offsetInWindow[1] = 0;
            }
        }
        return false;
    }

在這里通知父布局(即NestedParentView)執(zhí)行onNestedPreScroll蓉驹,根據(jù)執(zhí)行結(jié)果對坐標系進行轉(zhuǎn)換

看下NestedParentView#onNestedPreScroll

@Override
    public void onNestedPreScroll(@NonNull 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) {
                mBehaviorConsumed[0] = 0;
                mBehaviorConsumed[1] = 0;
                viewBehavior.onNestedPreScroll(this, view, target, dx, dy, mBehaviorConsumed, type);

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

                accepted = true;
            }
        }

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

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

這里又交給了Behavior#onNestedPreScroll執(zhí)行城榛,其中ScrollBehvior沒做處理,HeaderBehavior的實現(xiàn):

    @Override
    public void onNestedPreScroll(@NonNull NestedParentView parent, @NonNull HeaderView child, @NonNull View target, int dx, int dy, int[] consumed, int type) {
        if (dy != 0) {
            int min;
            int max;
            min = -child.getTotalScrollRange();
            max = 0;
            if (min != max) {
                consumed[1] = scroll(parent, child, dy, min, max);
            }
        }
    }

在這里執(zhí)行的對NestedParentView整體的滾動态兴,實現(xiàn)方式是更改其子View的top和Bottom:

int setHeaderTopBottomOffset(NestedParentView parent, HeaderView header, int newOffset,
                                 int minOffset, int maxOffset) {
        final int curOffset = getTopAndBottomOffset();
        int consumed = 0;

        if (minOffset != 0 && curOffset >= minOffset && curOffset <= maxOffset) {
            // If we have some scrolling range, and we're currently within the min and max
            // offsets, calculate a new offset
            newOffset = MathUtils.clamp(newOffset, minOffset, maxOffset);

            if (curOffset != newOffset) {
                setTopAndBottomOffset(newOffset);
                // Update how much dy we have consumed
                consumed = curOffset - newOffset;
            }
        }

        return consumed;
    }

    int getTopBottomOffsetForScrollingSibling() {
        return getTopAndBottomOffset();
    }

    public boolean setTopAndBottomOffset(int offset) {
        if (mViewOffsetHelper != null) {
            return mViewOffsetHelper.setTopAndBottomOffset(offset);
        } else {
            mTempTopBottomOffset = offset;
        }
        return false;
    }

上面的過程是HeaderView的滑動狠持,但是只有HeaderView滑動是不行的,NestedChildView華東也要跟上诗茎,回到NestedParentView#onNestedPreScroll工坊,這個方法的最后一行就是處理NestedParentView中其他子View的滑動的:

final void onChildViewsChanged(final int type) {
        final int childCount = getChildCount();
        final Rect inset = acquireTempRect();
        final Rect drawRect = acquireTempRect();
        final Rect lastDrawRect = acquireTempRect();

        for (int i = 0; i < childCount; i++) {
            final View child = getChildAt(i);
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            if (child.getVisibility() == View.GONE) {
                continue;
            }
            getChildRect(child, true, drawRect);

            for (int j = i + 1; j < childCount; j++) {
                final View checkChild = getChildAt(j);
                final LayoutParams checkLp = (LayoutParams) checkChild.getLayoutParams();
                final Behavior b = checkLp.getBehavior();

                // 如果checkChild和child滑動互相依賴
                if (b != null && b.layoutDependsOn(this, checkChild, child)) {
                    final boolean handled;
                    handled = b.onDependentViewChanged(this, checkChild, child);
                }
            }
        }

        releaseTempRect(inset);
        releaseTempRect(drawRect);
        releaseTempRect(lastDrawRect);
    }

ScrollBehavior#onDependentViewChanged:

@Override
    public boolean onDependentViewChanged(
            @NonNull NestedParentView parent, @NonNull View child, @NonNull View dependency) {
        offsetChildAsNeeded(child, dependency);
        return false;
    }

    private void offsetChildAsNeeded(View child, View dependency) {
        // 將View移動至dependency的下方
        final NestedParentView.Behavior behavior =
                ((NestedParentView.LayoutParams) dependency.getLayoutParams()).getBehavior();
        if (behavior instanceof HeaderBehavior) {
            final HeaderBehavior ablBehavior = (HeaderBehavior) behavior;
            ViewCompat.offsetTopAndBottom(child, (dependency.getBottom() - child.getTop()));
        }
    }

通過以上步驟,基本實現(xiàn)了一個乞丐版的CoordinatorLayout

相信照著做一遍敢订,會對CoordinatorLayout的理解加深很多

這里還有很多碎片代碼的欠缺王污,全部代碼可以參考文末的DEMO鏈接

計劃接下來的幾篇文章繼續(xù)分析CoordinatorLayout的源碼

代碼地址:

各種DEMO

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市楚午,隨后出現(xiàn)的幾起案子昭齐,更是在濱河造成了極大的恐慌,老刑警劉巖矾柜,帶你破解...
    沈念sama閱讀 222,378評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件阱驾,死亡現(xiàn)場離奇詭異,居然都是意外死亡怪蔑,警方通過查閱死者的電腦和手機里覆,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,970評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來缆瓣,“玉大人喧枷,你說我怎么就攤上這事」耄” “怎么了隧甚?”我有些...
    開封第一講書人閱讀 168,983評論 0 362
  • 文/不壞的土叔 我叫張陵,是天一觀的道長渡冻。 經(jīng)常有香客問我戚扳,道長,這世上最難降的妖魔是什么族吻? 我笑而不...
    開封第一講書人閱讀 59,938評論 1 299
  • 正文 為了忘掉前任帽借,我火速辦了婚禮珠增,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘宜雀。我一直安慰自己切平,他們只是感情好,可當我...
    茶點故事閱讀 68,955評論 6 398
  • 文/花漫 我一把揭開白布辐董。 她就那樣靜靜地躺著悴品,像睡著了一般。 火紅的嫁衣襯著肌膚如雪简烘。 梳的紋絲不亂的頭發(fā)上苔严,一...
    開封第一講書人閱讀 52,549評論 1 312
  • 那天,我揣著相機與錄音孤澎,去河邊找鬼届氢。 笑死,一個胖子當著我的面吹牛覆旭,可吹牛的內(nèi)容都是我干的退子。 我是一名探鬼主播,決...
    沈念sama閱讀 41,063評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼型将,長吁一口氣:“原來是場噩夢啊……” “哼寂祥!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起七兜,我...
    開封第一講書人閱讀 39,991評論 0 277
  • 序言:老撾萬榮一對情侶失蹤丸凭,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后腕铸,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體惜犀,經(jīng)...
    沈念sama閱讀 46,522評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,604評論 3 342
  • 正文 我和宋清朗相戀三年狠裹,在試婚紗的時候發(fā)現(xiàn)自己被綠了虽界。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,742評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡涛菠,死狀恐怖浓恳,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情碗暗,我是刑警寧澤,帶...
    沈念sama閱讀 36,413評論 5 351
  • 正文 年R本政府宣布梢夯,位于F島的核電站言疗,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏颂砸。R本人自食惡果不足惜噪奄,卻給世界環(huán)境...
    茶點故事閱讀 42,094評論 3 335
  • 文/蒙蒙 一死姚、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧勤篮,春花似錦都毒、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,572評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至金抡,卻和暖如春瀑焦,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背梗肝。 一陣腳步聲響...
    開封第一講書人閱讀 33,671評論 1 274
  • 我被黑心中介騙來泰國打工榛瓮, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人巫击。 一個月前我還...
    沈念sama閱讀 49,159評論 3 378
  • 正文 我出身青樓禀晓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親坝锰。 傳聞我的和親對象是個殘疾皇子粹懒,可洞房花燭夜當晚...
    茶點故事閱讀 45,747評論 2 361