美文网首页
CoordinatorLayout协调子view交互浅析

CoordinatorLayout协调子view交互浅析

作者: StudyForLong | 来源:发表于2019-10-02 09:13 被阅读0次

    1.CoordinatorLayout是什么?

    CoordinatorLayout is a super-powered FrameLayout.

    在谷歌官方文档中解释CoordinatorLayout是一个超级帧布局,可在两种情况下使用:

    1.作为窗口布局的顶层父布局

    2.作为与一个或者多个子视图进行交互的容器

    通过给CoordinatorLayout的直接子控件指定一个Behavior可以在不同的兄弟控件之间得到许多不同的交互,Behaviors可以被用来实现各种各样的交互动效,

    例如:滑动列表时的悬浮按钮自动显示与隐藏,滑动时头部控件的缩放、位移等。

    2.CoordinatorLayout是如何协调子控件进行交互的?

    CoordinatorLayout本身不具备实际交互能力,所有的交互行为都会被分发给子view的Behavior去实现,如果子view都没有指定Behavior,只能作为一个FrameLayout存在。
    CoordinatorLayout实现了自己的LayoutParams,而Behavior就存储在LayoutParams中,由于子控件的LayoutParams类型是由父控件决定的,所以能拥有CoordinatorLayout.LayoutParams的控件只能是CoordinatorLayout的直接子view。
    这在一定程度上也使得交互产生了一定的局限性。可以弥补这个局限性的功能是CoordinatorLayout实现了NestedScrollingParent接口,而嵌套滚动的实现是可以多层级传递事件的。和普通实现NestedScrollingParent的父控件向上传递事件的行为不同的是
    CoordinatorLayout会向下传递滚动事件,依然是传递到Behavior中。
    所以一个实现了NestedScrollingChild接口的view可以直接包含在CoordinatorLayout的直接子view中,且可以多层级包含,而CoordinatorLayout的直接子view不需要再次实现NestedScrollingParent接口。
    总结一句话就是CoordinatorLayout会拦截作用于它的一切行为并分发给它的所有直接子view。这使得不同子view之间的行为相互影响成为可能。

    3.CoordinatorLayout是如何拦截行为并分发的?

    这里我们可以进入源码先观察一下CoordinatorLayout是如何测量并布局子view的,如下:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        prepareChildren(); //准备子view
        ensurePreDrawListener();
        //省略若干代码...
        final int childCount = mDependencySortedChildren.size();
        for (int i = 0; i < childCount; i++) {
            final View child = mDependencySortedChildren.get(i);
            if (child.getVisibility() == GONE) {
                // If the child is GONE, skip...
                continue;
            }
    
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
     
            //省略若干代码...
     
            final Behavior b = lp.getBehavior();
            
            //如果子view的Behavior为空,或者子view的测量方法返回false就使用CoordinatorLayout默认的
    //onMeasureChild方式进行测量。如果子view的onMeasureChild返回true表示子view自己已经进行了测量。
            if (b == null || !b.onMeasureChild(this, child, childWidthMeasureSpec, keylineWidthUsed,
                    childHeightMeasureSpec, 0)) {
                onMeasureChild(child, childWidthMeasureSpec, keylineWidthUsed,
                        childHeightMeasureSpec, 0);
            }
        }
            //省略若干代码...
        setMeasuredDimension(width, height);
    }
    

    以上可看见CoordinatorLayout实际上在自己的各个行为处理方法中优先通知询问子view是否要先处理,如果子view处理了,则不会再处理。
    但是这只是把处理权限赋予了子view,在每个子view之间并没有建立相互的的行为关联。但在以上方法中有两处关键信息不能忽视,就是prepareChildren()和ensurePreDrawListener()两个方法。

    下面继续追踪源码:

    private void prepareChildren() {
        //mDependencySortedChildren用于存储有依赖于其他子view行为的子view
    
        mDependencySortedChildren.clear();
        mChildDag.clear();
    
        for (int i = 0, count = getChildCount(); i < count; i++) {
            final View view = getChildAt(i);
    
            final LayoutParams lp = getResolvedLayoutParams(view);
            lp.findAnchorView(this, view);
    
            mChildDag.addNode(view);
    
            // Now iterate again over the other children, adding any dependencies to the graph
            for (int j = 0; j < count; j++) {
                if (j == i) {
                    continue;
                }
                final View other = getChildAt(j);
                if (lp.dependsOn(this, view, other)) {
                    if (!mChildDag.contains(other)) {
                        // Make sure that the other node is added
                        mChildDag.addNode(other);
                    }
                    // Now add the dependency to the graph
                    mChildDag.addEdge(other, view);
                }
            }
        }
    
        // Finally add the sorted graph list to our list
        mDependencySortedChildren.addAll(mChildDag.getSortedList());
        // We also need to reverse the result since we want the start of the list to contain
        // Views which have no dependencies, then dependent views after that
        Collections.reverse(mDependencySortedChildren);
    }
     
    /**
     * Add or remove the pre-draw listener as necessary.
     */
    void ensurePreDrawListener() {
        boolean hasDependencies = false;
        final int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View child = getChildAt(i);
            if (hasDependencies(child)) {
                hasDependencies = true;
                break;
            }
        }
    
        if (hasDependencies != mNeedsPreDrawListener) {
            if (hasDependencies) {
                //如果有依赖则添加相应的视图绘制监听回调
                addPreDrawListener(); 
            } else {
                removePreDrawListener();
            }
        }
    }
     
    /**
     * Add the pre-draw listener if we're attached to a window and mark that we currently
     * need it when attached.
     */
    void addPreDrawListener() {
        if (mIsAttachedToWindow) {
            // Add the listener
            if (mOnPreDrawListener == null) {
                mOnPreDrawListener = new OnPreDrawListener();
            }
    
            //获取视图树观察者,添加CoordinatorLayout自身实现的mOnPreDrawListener监听
            final ViewTreeObserver vto = getViewTreeObserver();
            vto.addOnPreDrawListener(mOnPreDrawListener);
        }
    
        // Record that we need the listener regardless of whether or not we're attached.
        // We'll add the real listener when we become attached.
        mNeedsPreDrawListener = true;
    }
     
    //CoordinatorLayout内部类OnPreDrawListener,相当简洁,只是调用了onChildViewsChanged方法
    class OnPreDrawListener implements ViewTreeObserver.OnPreDrawListener {
        @Override
        public boolean onPreDraw() {
            onChildViewsChanged(EVENT_PRE_DRAW);
            return true;
        }
    }
    

    从上面可以追踪到监听被依赖子view的绘制之后会调用onChildViewsChanged方法,下面来看一下onChildViewsChanged中做了什么

    final void onChildViewsChanged(@DispatchChangeEvent final int type) {
        //省略代码若干...
    
        for (int i = 0; i < childCount; i++) {
            final View child = mDependencySortedChildren.get(i);
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            if (type == EVENT_PRE_DRAW && child.getVisibility() == View.GONE) {
                // Do not try to update GONE child views in pre draw updates.
                continue;
            }
     
        //省略代码若干...
    
            // Update any behavior-dependent views for the change
            for (int j = i + 1; j < childCount; j++) {
                final View checkChild = mDependencySortedChildren.get(j);
                final LayoutParams checkLp = (LayoutParams) checkChild.getLayoutParams();
                final Behavior b = checkLp.getBehavior();
                
                //获取Behavior是否是依赖于当前遍历的子view,如果是则调用Behavior相关方法进行回调通知依赖view的改变
                if (b != null && b.layoutDependsOn(this, checkChild, child)) {
     
                //在通知依赖view改变之前先检查是否存在嵌套滚动,如果存在则此次不通知,因为此次事件
    //并没有作用于当前依赖view而是通过嵌套滚动机制传递给嵌套view。
    
                    if (type == EVENT_PRE_DRAW && checkLp.getChangedAfterNestedScroll()) {
                        // If this is from a pre-draw and we have already been changed
                        // from a nested scroll, skip the dispatch and reset the flag
                        checkLp.resetChangedAfterNestedScroll();
                        continue;
                    }
    
                    final boolean handled;
                    switch (type) {
                        case EVENT_VIEW_REMOVED:
                            // EVENT_VIEW_REMOVED means that we need to dispatch
                            // onDependentViewRemoved() instead
                            b.onDependentViewRemoved(this, checkChild, child);
                            handled = true;
                            break;
                        default:
                            // Otherwise we dispatch onDependentViewChanged()
                            handled = b.onDependentViewChanged(this, checkChild, child);
                            break;
                    }
    
                    if (type == EVENT_NESTED_SCROLL) {
                        // If this is from a nested scroll, set the flag so that we may skip
                        // any resulting onPreDraw dispatch (if needed)
                        checkLp.setChangedAfterNestedScroll(handled);
                    }
                }
            }
        }
    
        releaseTempRect(inset);
        releaseTempRect(drawRect);
        releaseTempRect(lastDrawRect);
    }
    

    从以上代码分析我们知道在测量子view时的大致流程如下:


    微信截图_20180104150129.png

    4.一个实现自定义Behavior的小例子

    ezgif-4-5e4a8abb69.gif

    以上的例子实现了红色控件依赖于按钮的缩放属性,当按钮缩放时跟着缩放,同时获取依赖控件的文本值计算结果。主要展示了依赖控件可以监听到被依赖控件的重新绘制变化,以及可以获取被依赖控件的相关属性值等。

    以下是例子的具体实现:

    package com.example.myapplication;
    
    import android.os.Bundle;
    import android.support.v7.app.AppCompatActivity;
    import android.view.View;
    import android.widget.Button;
    
    import java.util.Random;
    
    public class TestBehaviorActivity extends AppCompatActivity {
    
       Button button;
       Random random = new Random();
       boolean flag;
    
    
       @Override
       protected void onCreate(Bundle savedInstanceState) {
          super.onCreate(savedInstanceState);
          setContentView(R.layout.activity_test_behavior);
    
          button = findViewById(R.id.button);
          button.setOnClickListener(new View.OnClickListener() {
             @Override
             public void onClick(View v) {
                button.setText(random.nextInt(10) + " + " + random.nextInt(10));
    
                //改变控件属性使控件重新绘制,以便CoordinatorLayout通知Behavior的回调函数
                if (flag) {
                   button.setScaleX(1);
                } else {
                   button.setScaleX(2);
                }
                flag = !flag;
             }
          });
       }
    }
    
    <?xml version="1.0" encoding="utf-8"?>
    <android.support.design.widget.CoordinatorLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        tools:context="com.example.yychen.myapplication.TestBehaviorActivity">
    
        <Button
            android:id="@+id/button"
            android:layout_width="100dp"
            android:layout_height="50dp"
            android:layout_gravity="center"
            android:textSize="14sp"
            android:layout_marginBottom="100dp"
            android:text="1+2"/>
    
        <TextView
            android:id="@+id/textView"
            android:layout_width="wrap_content"
            android:padding="10dp"
            android:layout_height="40dp"
            android:layout_gravity="center"
            android:gravity="center"
            android:textSize="16sp"
            android:textColor="#fff"
            android:background="@color/colorAccent"
            app:layout_behavior=".TestBehavior"
            android:layout_marginTop="100dp"
            android:text="依赖view"/>
    
    </android.support.design.widget.CoordinatorLayout>
    
    package com.example.myapplication;
    
    import android.content.Context;
    import android.support.design.widget.CoordinatorLayout;
    import android.util.AttributeSet;
    import android.view.View;
    import android.widget.Button;
    import android.widget.TextView;
    
    import java.util.regex.Matcher;
    import java.util.regex.Pattern;
    
    public class TestBehavior extends CoordinatorLayout.Behavior<TextView> {
    
       //在布局中使用 app:layout_behavior=".TestBehavior"方式,必须实现的一个方法
       public TestBehavior(Context context, AttributeSet attrs) {
          super(context, attrs);
       }
    
       // 设置依赖于哪个控件,这里设置依赖于类型为Button的控件,返回true表示依赖
       @Override
       public boolean layoutDependsOn(CoordinatorLayout parent, TextView child, View dependency) {
          return dependency instanceof Button;
       }
    
       //依赖的view发生改变,这里的dependency和layoutDependsOn设定的依赖控件一致
       @Override
       public boolean onDependentViewChanged(CoordinatorLayout parent, TextView child, View dependency) {
          Button button = (Button) dependency;
          String text = button.getText().toString();
          child.setScaleX(button.getScaleX());
    
          try {
             int[] num = getNum(text);
             child.setText("两数之和 :" + (num[0] + num[1]));
          } catch (Exception e) {
             e.printStackTrace();
          }
    
          return true;
       }
    
       //依赖的view在窗体中被移除消失
       @Override
       public void onDependentViewRemoved(CoordinatorLayout parent, TextView child, View dependency) {
          super.onDependentViewRemoved(parent, child, dependency);
       }
    
       public int[] getNum(String text) throws Exception {
          int[] numArray = new int[2];
          Pattern pattern = Pattern.compile("\\d+");
          Matcher matcher = pattern.matcher(text);
    
          matcher.find();
          String num = matcher.group();
          numArray[0] = Integer.valueOf(num);
    
          matcher.find();
          num = matcher.group();
          numArray[1] = Integer.valueOf(num);
    
          return numArray;
       }
    }
    

    5.CoordinatorLayout.Behavior相关信息列举

    微信图片_20180104153129.png

    6.嵌套滚动简析

    1、什么是嵌套滚动? 嵌套滚动就是存在两个滚动行为的相互嵌套,当一个开始滚动另一个可以随着滚动的行为。
    在一般的交互过程中 ,嵌套的滚动是单一的行为,子view的滚动影响这父view,父view并不影响子view。
    所以一般的嵌套滚动交互流程图部分如下:


    微信截图_20180104163733.png

    2、关于嵌套滚动相关核心类有以下两组:
    被滚动类需实现:NestedScrollingParent接口并实例化NestedScrollingParentHelper帮助类
    滚动类需实现:NestedScrollingChild接口并实例化NestedScrollingChildHelper帮助类
    NestedScrollingParentHelper、NestedScrollingChildHelper承担了实现相应接口的具体嵌套操作。在实现接口的相关类中调用帮助类的对应方法即可完成嵌套流程。

    3、NestedScrollingParent和NestedScrollingChild接口简析图

    WX20180101-131052@2x.png
    WX20180101-122051@2x.png

    7.一个实现嵌套滚动的小例子

    ezgif-4-c5520d9b19.gif

    上面的例子滚动流程分为两块,向上滑动和向下滑动处理逻辑的先后顺序正好完全相反。
    向上滑动:先滑动父view,再滑动子view,再滑动子view中的内容。
    向下滑动,先滑动子view中的内容,再滑动子view,再滑动父view。
    下面来看下具体实现:

    <?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">
    
        <com.nestedscroll.nesteddemo.NestedScrollParentView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="#ffeebb"
            android:layout_alignParentBottom="true"
            android:orientation="vertical">
    
            <TextView
                android:layout_width="match_parent"
                android:layout_height="200dp"
                android:gravity="center"
                android:text="Parent" />
    
            <com.nestedscroll.nesteddemo.NestedScrollChildView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:background="#eeaa00"
                android:orientation="vertical">
    
                <TextView
                    android:layout_width="match_parent"
                    android:layout_height="200dp"
                    android:gravity="center"
                    android:padding="20dp"
                    android:text="Child"
                    android:textColor="#fff" />
    
            </com.nestedscroll.nesteddemo.NestedScrollChildView>
    
    
        </com.nestedscroll.nesteddemo.NestedScrollParentView>
    
    </RelativeLayout>
    
    package com.nestedscroll.nesteddemo;
    
    import android.content.Context;
    import android.support.annotation.Nullable;
    import android.support.v4.view.NestedScrollingChild;
    import android.support.v4.view.NestedScrollingChildHelper;
    import android.util.AttributeSet;
    import android.view.MotionEvent;
    import android.widget.LinearLayout;
    
    public class NestedScrollChildView extends LinearLayout implements NestedScrollingChild {
       private NestedScrollingChildHelper helper;
       private int[] consumed = new int[2];
       private int[] offsetInWindow = new int[2];
       private float lastY;
       private float initY;
    
    
       public NestedScrollChildView(Context context) {
          super(context);
          init();
       }
    
       public NestedScrollChildView(Context context, @Nullable AttributeSet attrs) {
          super(context, attrs);
          init();
       }
    
       public NestedScrollChildView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
          super(context, attrs, defStyleAttr);
          init();
       }
    
       private void init() {
          helper = new NestedScrollingChildHelper(this);
          setNestedScrollingEnabled(true);
       }
    
       @Override
       protected void onLayout(boolean changed, int l, int t, int r, int b) {
          super.onLayout(changed, l, t, r, b);
    
          initY = getY();
       }
    
       @Override
       public void setNestedScrollingEnabled(boolean enabled) {
          helper.setNestedScrollingEnabled(enabled);
       }
    
       @Override
       public boolean isNestedScrollingEnabled() {
          return helper.isNestedScrollingEnabled();
       }
    
       @Override
       public boolean startNestedScroll(int axes) {
          return helper.startNestedScroll(axes);
       }
    
       @Override
       public void stopNestedScroll() {
          helper.stopNestedScroll();
       }
    
       @Override
       public boolean hasNestedScrollingParent() {
          return helper.hasNestedScrollingParent();
       }
    
       @Override
       public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int 
    dyUnconsumed, int[] offsetInWindow) {
          return helper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow);
       }
    
       @Override
       public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
          return helper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
       }
    
       @Override
       public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
          return helper.dispatchNestedFling(velocityX, velocityY, consumed);
       }
    
       @Override
       public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
          return helper.dispatchNestedPreFling(velocityX, velocityY);
       }
    
       @Override
       public boolean onTouchEvent(MotionEvent event) {
          switch (event.getActionMasked()) {
             case MotionEvent.ACTION_DOWN:
                lastY = event.getRawY();
                //通知父view要开始滑动了
                startNestedScroll(SCROLL_AXIS_VERTICAL);
                break;
             case MotionEvent.ACTION_MOVE:
                float deltaY = lastY - event.getRawY();
                lastY = event.getRawY();
    
                if (deltaY > 0) { //嵌套子view向上滑动时
    
                   //通知父view滑动
                   if (dispatchNestedPreScroll(0, (int) deltaY, consumed, offsetInWindow)) {
                      // 如果父view消耗了滑动,需减去消耗距离
                      deltaY -= consumed[1];
                   }
    
                   //父view消耗的距离为int类型,损耗了精度,这里向下取整去掉剩余值
                   if (Math.floor(deltaY) == 0) {
                      break;
                   }
    
                   //滑动子view
                   if (getY() - deltaY >= 0) {
                      setY(getY() - deltaY);
                      deltaY = 0;
                   } else {
                      deltaY -= getY();
                      setY(0);
                   }
    
                   //滑动子view内容
                   if (getScrollY() + deltaY <= (getMeasuredHeight() - 40) / 2) {
                      scrollBy(0, (int) deltaY);
                   } else {
                      scrollTo(0, (getMeasuredHeight() - 40) / 2);
                   }
                } else { // 嵌套子view向下滑动时
                   float dyConsumed; //子view滑动消耗距离
    
                   //滑动子view内容
                   if (getScrollY() + deltaY >= -(getMeasuredHeight() - 40) / 2) {
                      scrollBy(0, (int) deltaY);
                      dyConsumed = deltaY;
                   } else {
                      scrollTo(0, -(getMeasuredHeight() - 40) / 2);
                      dyConsumed = getScrollY() + (getMeasuredHeight() - 40) / 2;
                   }
    
                   float dyUnconsumed = deltaY - dyConsumed;
                   //滑动子view
                   if (getY() - dyUnconsumed <= initY) {
                      setY(getY() - dyUnconsumed);
                      dyConsumed += dyUnconsumed;
                   } else {
                      dyConsumed = (int) (dyConsumed + (initY - getY()));
                      setY(initY);
                   }
                   //通知父view滑动
                   dispatchNestedScroll(0, (int) dyConsumed, 0, (int) (deltaY - dyConsumed), offsetInWindow);
                }
                break;
             case MotionEvent.ACTION_CANCEL:
             case MotionEvent.ACTION_UP:
                //通知父view停止本次嵌套滑动
                stopNestedScroll();
                break;
          }
    
          return true;
       }
    }
    
    package com.nestedscroll.nesteddemo;
    
    import android.content.Context;
    import android.support.annotation.Nullable;
    import android.support.v4.view.NestedScrollingParent;
    import android.support.v4.view.NestedScrollingParentHelper;
    import android.util.AttributeSet;
    import android.view.View;
    import android.widget.LinearLayout;
    
    
    public class NestedScrollParentView extends LinearLayout implements NestedScrollingParent {
    
       NestedScrollingParentHelper helper;
       private float initY;
    
       public NestedScrollParentView(Context context) {
          super(context);
          init();
       }
    
       public NestedScrollParentView(Context context, @Nullable AttributeSet attrs) {
          super(context, attrs);
          init();
       }
    
       public NestedScrollParentView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
          super(context, attrs, defStyleAttr);
          init();
       }
    
       private void init() {
          helper = new NestedScrollingParentHelper(this);
       }
    
    
       @Override
       protected void onLayout(boolean changed, int l, int t, int r, int b) {
          super.onLayout(changed, l, t, r, b);
    
          initY = getY();
       }
    
       @Override
       public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
          return (nestedScrollAxes & SCROLL_AXIS_VERTICAL) != 0;
       }
    
       @Override
       public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
          helper.onNestedScrollAccepted(child, target, nestedScrollAxes);
       }
    
       @Override
       public void onStopNestedScroll(View target) {
          helper.onStopNestedScroll(target);
       }
    
       @Override
       public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
          if (getY() - dyUnconsumed <= initY) {
             setY(getY() - dyUnconsumed);
          } else {
             setY(initY);
          }
       }
    
       @Override
       public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
          if (getY() - dy >= 0) {
             setY(getY() - dy);
             consumed[1] = dy;
          } else {
             consumed[1] = (int) getY();
             setY(0);
          }
       }
    
       @Override
       public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
          return false;
       }
    
       @Override
       public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
          return false;
       }
    
       @Override
       public int getNestedScrollAxes() {
          return helper.getNestedScrollAxes();
       }
    }
    

    8.在CoordinatorLayout中的嵌套滚动

    前面分析CoordinatorLayout实现了NestedScrollingParent接口,而RecyclerView实现了NestedScrollingChild接口,

    利用CoordinatorLayout和RecyclerView可以快速实现上面例子的嵌套滚动,以及需要的视图依赖变换。

    相关文章

      网友评论

          本文标题:CoordinatorLayout协调子view交互浅析

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