Android-CoordinatorLayout.……

[Android]CoordinatorLayout简介(三)手

2020-12-23  本文已影响0人  dafasoft

参考资料

CoordinatorLayout简介(一)CoordinatorLayout的简单使用
CoordinatorLayout简介(二)几种系统默认Behavior的使用
CoordinatorLayout简介(三)手写一个CoordinatorLayout怎么样?

前言

这是CoordinatorLayout系列的第三篇文章,本来按计划是准备解析源码的,但是粗略规划了一下,发现竟然无从下口,根本原因还是CoordinatorLayout体系过于纷繁复杂,其中包含了嵌套滑动框架的实现、事件传递和拦截、坐标系变换、View绘制流程等等,以至于不知从何说起。

源码中因为稳定性和兼容性的需要,以及各种效果的事件,包含了过多非主流程的代码,这给我们阅读源码也带来了一定的困难,在阅读的过程中经常感觉乱花渐欲迷人眼,为逻辑所困,越陷越深,无奈放弃

本文将手动实现一个CoordinatorLayout+AppBarLayout效果的组件,尽量删掉源码中各种分支逻辑和变换逻辑,着重于CoordinatorLayout主流程,实现方式尽可能还原原生的CoordinatorLayout+AppBarLayout,主要是让我们可以更加容易理解CoordinatorLayout的工作过程

正文

效果图:

NestedParentView[00_00_02--00_00_09].gif

实现

先看下工程结构:


image.png

和原生CoordinatorLayout的对应关系:

自定义 原生
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,否则交给系统处理
            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的实现

首先看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在其本身所占区域内,因为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的坐标变化,通过坐标变化实现嵌套滑动的效果

再来看ScrollBehavior的实现:

@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的不可滑动区域
                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的可布局区域,将其置于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的不可滑动区域,这样做的结果很明显 就是当HeaderView滑动到需要悬浮处理时,ScrollableView正好可以全部显示出来

onLayoutChild负责ScrollableView的摆放,实现方法就是通过寻找HeaderView,将HeaderView的底边设为ScrollableView的顶边,再结合onMeasureChild后确定的高度,即可确定ScrollableView的绘制Rect

通过上面对NestedParentView和Behavior的拆分,我们应该能理解为什么我们自定义实现一些CoordinatorLayout的炫酷效果时要自定义Behavior了,也正因为Behavior如此强大的功能,CoordinatorLayout才会变为专治各种花里胡哨的利器

联动效果的实现:

我们只是将NestedParentView和它的子View摆放好肯定是远远不够的,关键要让它们联动起来,

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

具体的实现可以看NestedScrollingChild2, NestedScrollingChild3、NestedScrollingParent2, NestedScrollingParent3这四个接口文件中方法的定义,总之,通过一些操作继承于NestedScrollingChild2, NestedScrollingChild3的View是可以和继承于NestedScrollingParent2, NestedScrollingParent3的View进行联动的

现在将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;
                // 调用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;

                // 将嵌套滑动事件分发出去
                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 结束,开始本View的滑动,这里用ScrollView的滑动来模拟
                    ((ScrollView)getChildAt(0)).scrollBy(0, dy);
                }
                break;

            case MotionEvent.ACTION_UP:
                break;

            default:
                break;
        }
        return true;
    }

这里主要的工作,在ACTION_DOWN的时候,传递startNestedScroll事件至父布局,这个事件主要做两件事情:

  1. 父布局根据该View确定嵌套滑动事件的子View
  2. 寻找接受嵌套滑动的其他View(在本案例中为HeaderView)

接着看ACTION_MOVE:
这里的工作主要有几个
1.将dispatchNestedPreScroll传递给父布局
2.根据父布局对View坐标系的变化,修改mLastTouchX和mLastTouchY
3.计算总的嵌套滑动距离
4.处理本View的滑动

看下实现:
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初始化的时候进行的初始化,这是系统给我们提供的工具类,主要负责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)执行onNestedPreScroll,根据执行结果对坐标系进行转换

看下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执行,其中ScrollBehvior没做处理,HeaderBehavior的实现:

    @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);
            }
        }
    }

在这里执行的对NestedParentView整体的滚动,实现方式是更改其子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()));
        }
    }

通过以上步骤,基本实现了一个乞丐版的CoordinatorLayout

相信照着做一遍,会对CoordinatorLayout的理解加深很多

这里还有很多碎片代码的欠缺,全部代码可以参考文末的DEMO链接

计划接下来的几篇文章继续分析CoordinatorLayout的源码

代码地址:

各种DEMO

上一篇下一篇

猜你喜欢

热点阅读