美文网首页Android-CoordinatorLayout.……
[Android]CoordinatorLayout简介(三)手

[Android]CoordinatorLayout简介(三)手

作者: dafasoft | 来源:发表于2020-12-23 16:28 被阅读0次

    参考资料

    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

    相关文章

      网友评论

        本文标题:[Android]CoordinatorLayout简介(三)手

        本文链接:https://www.haomeiwen.com/subject/ztrynktx.html