CoordinatorLayout

作者: sollian | 来源:发表于2020-12-27 21:56 被阅读0次

    通常说到CoordinatorLayout,我们首先想到的就是和AppBarLayout一起使用,实现布局中特殊的Header效果
    实际上,CoordinatorLayout是可以单独使用的,不必和AppBarLayout绑定到一起
    CoordinatorLayoutLayoutParam中,有个Behavior参数,CoordinatorLayout正是通过这个参数来决定子View的某些行为

    Behavior

    如何设置Bahavior

    共有四种方式

    1. xml文件中通过app:layout_behavior="com.example.touchdemo.coordinator.behavior1.TextBehavior"
    2. 若是动态添加的子View,则通过LayoutParam#setBehavior方法
    3. 若是自定义的View,则有额外两种方式
      1. 自定义的View继承CoordinatorLayout.AttachedBehavior接口。如BottomAppBar
      2. 自定义View的class添加CoordinatorLayout.DefaultBehavior注解,如AppBarLayout

    需要注意的是,只有CoordinatorLayout的直接子View设置Behavior才有效果

    Behavior可以实现哪些功能

    Behavior有很多方法,我们可以归为以下四类

    1. 布局相关
    2. 事件传递
    3. 依赖相关
    4. 嵌套滑动

    Behavior布局相关方法

    public boolean onMeasureChild(@NonNull CoordinatorLayout parent, @NonNull V child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
        return false;
    }
    
    public boolean onLayoutChild(@NonNull CoordinatorLayout parent, @NonNull V child,
            int layoutDirection) {
        return false;
    }
    

    Behavior事件传递方法

    public boolean onInterceptTouchEvent(@NonNull CoordinatorLayout parent, @NonNull V child,
            @NonNull MotionEvent ev) {
        return false;
    }
    
    public boolean onTouchEvent(@NonNull CoordinatorLayout parent, @NonNull V child,
            @NonNull MotionEvent ev) {
        return false;
    }
    

    也就是说,如果有需要,我们可以通过Behavior拦截兄弟View的touch事件

    Behavior依赖相关方法

    public boolean layoutDependsOn(@NonNull CoordinatorLayout parent, @NonNull V child,
            @NonNull View dependency) {
        //返回true表明child依赖于dependency
        return false;
    }
    
    public boolean onDependentViewChanged(@NonNull CoordinatorLayout parent, @NonNull V child,
            @NonNull View dependency) {
        //在dependency的size、position变化时,会回调该方法。
        //返回true表明已处理
        return false;
    }
    
    public void onDependentViewRemoved(@NonNull CoordinatorLayout parent, @NonNull V child,
            @NonNull View dependency) {
    }
    

    Behavior嵌套滑动相关方法

    所有方法名中有Nested的方法。如果对嵌套滑动不熟悉,需要先掌握NestedScrollingParent、NestedScrollingChild这些预备知识

    CoordinatorLayout如何处理依赖关系

    onMeasure方法中通过prepareChildren方法,将children的依赖关系保存到一个有向无环图中:

    private final List<View> mDependencySortedChildren = new ArrayList<>();
    //有向无环图结构,不了解没关系,知道作用就可以
    private final DirectedAcyclicGraph<View> mChildDag = new DirectedAcyclicGraph<>();
    
    private void prepareChildren() {
        //清空
        mDependencySortedChildren.clear();
        mChildDag.clear();
    
        //遍历children,将依赖关系保存到mChildDag中
        for (int i = 0, count = getChildCount(); i < count; i++) {
            final View view = getChildAt(i);
    
            final LayoutParams lp = getResolvedLayoutParams(view);
            lp.findAnchorView(this, view);
    
            //将i对应的子view添加为图的一个节点
            mChildDag.addNode(view);
    
            for (int j = 0; j < count; j++) {
                //遍历其他所有的子View,查找被i依赖的j
                if (j == i) {
                    continue;
                }
                final View other = getChildAt(j);
                if (lp.dependsOn(this, view, other)) {
                    //view依赖于other
                    if (!mChildDag.contains(other)) {
                        // 保证other被添加为图的节点
                        mChildDag.addNode(other);
                    }
                    // 添加从view指向other的一条路径
                    mChildDag.addEdge(other, view);
                    //注意这里没有break,说明view可以依赖于多个other
                }
            }
        }
        // mChildDag.getSortedList会将所有节点进行拓扑排序
        //拓扑排序的结果保证 依赖的view在前,被依赖的view在后
        //然后将结果保存到mDependencySortedChildren
        mDependencySortedChildren.addAll(mChildDag.getSortedList());
        // 翻转列表,使被依赖的view在前,依赖的view在后
        //这样当被依赖的view有事件需要通知依赖的view时,只需要从对应的索引向后遍历即可
        Collections.reverse(mDependencySortedChildren);
    }
    

    从以上处理依赖关系的逻辑可以得出以下结论:

    1. CoordinatorLayout只能处理直接子view的依赖,即Behavior只能应用到直接子view上
    2. 子view不能相互依赖,否则有向无环图无法保证
    3. 依赖是多对多的关系,一个View可以被多个View依赖,也可以依赖于多个View

    Behavior依赖相关方法的分发

    依赖相关方法的分发在onChildViewsChanged,有三种事件类型:

    static final int EVENT_PRE_DRAW = 0;
    static final int EVENT_NESTED_SCROLL = 1;
    static final int EVENT_VIEW_REMOVED = 2;
    

    主要代码不再贴出来

    举个例子

    实现下面这样嵌套滑动的例子

    scq1y-kbp84.gif

    header是一个TextView,下面跟着一个RecyclerView,布局如下:

    <?xml version="1.0" encoding="utf-8"?>
    <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".coordinator.CoordinatorActivity">
    
        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="@android:color/holo_blue_light"
            android:gravity="center"
            android:padding="40dp"
            android:text="我可以嵌套滑动"
            android:textColor="#fff"
            android:textSize="20dp"
            app:layout_behavior="com.example.touchdemo.coordinator.behavior1.TextBehavior" />
    
        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/list"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:layout_behavior="com.example.touchdemo.coordinator.behavior1.RecyclerViewBehavior" />
    </androidx.coordinatorlayout.widget.CoordinatorLayout>
    

    TextBehavior:

    public class TextBehavior extends CoordinatorLayout.Behavior<TextView> {
        private int currentOffsetY;
    
        public TextBehavior() {
        }
    
        public TextBehavior(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
    
        @Override
        public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
                                           @NonNull TextView child,
                                           @NonNull View directTargetChild,
                                           @NonNull View target,
                                           int axes,
                                           int type) {
            //在垂直方向开启嵌套滑动
            return axes == ViewCompat.SCROLL_AXIS_VERTICAL;
        }
    
        @Override
        public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout,
                                      @NonNull TextView child,
                                      @NonNull View target,
                                      int dx,
                                      int dy,
                                      @NonNull int[] consumed,
                                      int type) {
            //获取可滑动的offsetY值
            int offsetY = calOffsetY(child, dy);
            //滑动TextView
            ViewCompat.offsetTopAndBottom(child, offsetY - currentOffsetY);
    
            consumed[1] = currentOffsetY - offsetY;
            currentOffsetY = offsetY;
        }
    
        private int calOffsetY(TextView child, int dy) {
            int offsetY = currentOffsetY - dy;
            int maxOffsetY = 0;
            int minOffsetY = -child.getHeight();
            if (offsetY < minOffsetY) {
                offsetY = minOffsetY;
            }
            if (offsetY > maxOffsetY) {
                offsetY = maxOffsetY;
            }
            return offsetY;
        }
    }
    

    RecyclerViewBehavior:

    public class RecyclerViewBehavior extends CoordinatorLayout.Behavior<RecyclerView> {
        public RecyclerViewBehavior() {
        }
    
        public RecyclerViewBehavior(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
    
        @Override
        public boolean layoutDependsOn(@NonNull CoordinatorLayout parent, @NonNull RecyclerView child, @NonNull View dependency) {
            //依赖于TextView
            return dependency instanceof TextView;
        }
    
        @Override
        public boolean onDependentViewChanged(@NonNull CoordinatorLayout parent, @NonNull RecyclerView child, @NonNull View dependency) {
            //TextView布局改变时,修改RecyclerView的offsetY
            ViewCompat.offsetTopAndBottom(child, dependency.getBottom() - child.getTop());
            return true;
        }
    }
    

    相关文章

      网友评论

        本文标题:CoordinatorLayout

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