CoordinatorLayout从源码到实战,一文搞懂

7198648929d5be7c93de71aad7a9d82b.png

/   今日科技快讯   /

近日,苹果、谷歌和微软等科技巨头联合宣布,将在未来一年内在他们控制的所有移动、桌面和浏览器平台上支持无密码登录功能。实际上,这意味着在不远的将来,无密码身份验证将进入所有主要的设备平台,包括安卓和iOS移动操作系统;Chrome、Edge和Safari浏览器;以及Windows和MacOS桌面环境。

/   作者简介   /

周六,上班。

2b0b5fe7c1c5d036889f52f0f9f54295.png

本篇文章来自android超级兵的投稿,文章主要分享了他对CoordinatorLayout详细的探索分析,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章。

android超级兵的博客地址:

https://blog.csdn.net/weixin_44819566?type=blog

/   前言   /

CoordinatorLayout源码版本:1.1.0。

废话不多说,先来看看今天要完成的效果:

f0f87903b4153f15518de70f34cc1101.gif

效果一

e9ddeaae8454b77919143147cd64a7eb.gif

效果二

/   什么是CoordinatorLayout?   /

CoordinatorLayout意为协调者布局,每个ViewGroup都有相应的特征。例如:

  • LinearLayout线性布局常用来水平/垂直摆放childView

  • RelativeLayout相对布局常用来AView在BView的某个位置,来摆放childView

  • ConstraintLayout约束布局常用来CView在DView的某个约束位置,很好的解决了布局嵌套层级过深以及布局困难的问题

所以CoordinatorLayout的特征是什么呢?

它的特征为:可以配合每个 childView 来协调使用。比如在移动A View的过程中我想让B View和C View发生改变(例如效果一),那么就可以用它,它的缺点也非常明显,布局起来稍稍优点麻烦...

难道说我用CoordinatorLayout就可以让他协调起来?用意念吗?那当然不行。

单指CoordinatorLayout没有太大的作用,重要的是CoordinatorLayout配合behavior来使用!

那什么是behavior呢?

behavior为行为,假设A View移动过程中B View想要跟随者A View移动,那么B View直接在xml中添加一个app:layout_behavior="XXX"属性,然后自定义CoordinatorLayout.Behavior即可。

最后再来看一张代码图,先有个初步的了解!

这个效果是如果MoveView发生移动,那么ImageView就发生颜色的变化。

6147fe7fac174c20d3ea56237b794bdb.png

所以本篇的重点就是CoordinatorLayout.Behavior源码分析,以及使用到实战!

c82de4584cde4f05a6f8a62e2306854c.gif

/   CoordinatorLayout.Behavior初步认识   /

# CoordinatorLayout.java

public static abstract class Behavior<V extends View> {

        /*
         * TODO 当解析layout完成时候调用 View#onAttachedToWindow() 然后紧接着调用该方法 
         */
        public void onAttachedToLayoutParams(@NonNull CoordinatorLayout.LayoutParams params) {
        }

        /*
         * TODO 当 view销毁的时候调用
         */
        public void onDetachedFromLayoutParams() {
        }

        /**
         *  TODO: 当 CoordinatorLayout#onInterceptTouchEvent() 事件的时候调用
         */
        public boolean onInterceptTouchEvent(@NonNull CoordinatorLayout parent, @NonNull V child,
                @NonNull MotionEvent ev) {
            return false;
        }

        /**
         *  TODO: 当 CoordinatorLayout#onTouchEvent() 事件的时候调用
         */
        public boolean onTouchEvent(@NonNull CoordinatorLayout parent, @NonNull V child,
                @NonNull MotionEvent ev) {
            return false;
        }

        /*
         * TODO  设置背景色
         *
         * 需要配合 getScrimOpacity() 使用 因为 getScrimOpacity() 默认 = 0f
         */
        @ColorInt
        public int getScrimColor(@NonNull CoordinatorLayout parent, @NonNull V child) {
            return Color.BLACK;
        }

        /*
         * TODO 设置不透明度
         */
        @FloatRange(from = 0, to = 1)
        public float getScrimOpacity(@NonNull CoordinatorLayout parent, @NonNull V child) {
            return 0.f;
        }

       /*
        * TODO 需要依赖的view
        *
        * @param child: 当前view
        * @param dependency: 需要依赖的View
        */
        public boolean layoutDependsOn(@NonNull CoordinatorLayout parent, @NonNull V child,
                @NonNull View dependency) {
            return false;
        }

        /*
         * TODO 需要依赖的view发生变化的时候调用
         */
        public boolean onDependentViewChanged(@NonNull CoordinatorLayout parent, @NonNull V child,
                @NonNull View dependency) {
            return false;
        }

        /*
         * TODO 当被依赖的view移除view的时候调用
         */
        public void onDependentViewRemoved(@NonNull CoordinatorLayout parent, @NonNull V child,
                @NonNull View dependency) {
        }

        /*
         * TODO 调用 CoordinatorLayout#onMeasureChild() 的时候调用
         */
        public boolean onMeasureChild(@NonNull CoordinatorLayout parent, @NonNull V child,
                int parentWidthMeasureSpec, int widthUsed,
                int parentHeightMeasureSpec, int heightUsed) {
            return false;
        }

        /*
         * TODO 调用CoordinatorLayout$onLayout() 的时候调用
         */
        public boolean onLayoutChild(@NonNull CoordinatorLayout parent, @NonNull V child,
                int layoutDirection) {
            return false;
        }

        /*
         * TODO 当 NestedScrollingChild#startNestedScroll() 的时候调用
         */
        @Deprecated
        public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
                @NonNull V child, @NonNull View directTargetChild, @NonNull View target,
                @ScrollAxis int axes) {
            return false;
        }

       /*
        * TODO  当 NestedScrollingChild2#startNestedScroll() 的时候调用
        */
        public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
                @NonNull V child, @NonNull View directTargetChild, @NonNull View target,
                @ScrollAxis int axes, @NestedScrollType int type) {
            if (type == ViewCompat.TYPE_TOUCH) {
                return onStartNestedScroll(coordinatorLayout, child, directTargetChild,
                        target, axes);
            }
            return false;
        }

        /*
         * TODO 当 NestedScrollingChild#startNestedScroll() = true的时候调用
         */
        @Deprecated
        public void onNestedScrollAccepted(@NonNull CoordinatorLayout coordinatorLayout,
                @NonNull V child, @NonNull View directTargetChild, @NonNull View target,
                @ScrollAxis int axes) {
        }

        /*
         * TODO 当 NestedScrollingChild2#startNestedScroll() = true的时候调用
         */
        public void onNestedScrollAccepted(@NonNull CoordinatorLayout coordinatorLayout,
                @NonNull V child, @NonNull View directTargetChild, @NonNull View target,
                @ScrollAxis int axes, @NestedScrollType int type) {
            if (type == ViewCompat.TYPE_TOUCH) {
                onNestedScrollAccepted(coordinatorLayout, child, directTargetChild,
                        target, axes);
            }
        }

        /*
         * TODO  当 NestedScrollingChild#stopNestedScroll() 调用的时候执行
         */
        @Deprecated
        public void onStopNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
                @NonNull V child, @NonNull View target) {
            // Do nothing
        }

        /*
         * TODO 当 NestedScrollingChild2#stopNestedScroll() 调用的时候执行
         */
        public void onStopNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
                @NonNull V child, @NonNull View target, @NestedScrollType int type) {
            ...
        }

        /*
         * TODO 当 NestedScrollingChild#dispatchNestedScroll() 调用的时候执行
         */
        @Deprecated
        public void onNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull V child,
                @NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed,
                int dyUnconsumed) {
            // Do nothing
        }


        /*
         * TODO 当 NestedScrollingChild2#dispatchNestedScroll() 调用的时候执行
         */
        @Deprecated
        public void onNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull V child,
                @NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed,
                int dyUnconsumed, @NestedScrollType int type) {
           ...
        }


        /*
         * TODO 当 NestedScrollingChild3#dispatchNestedScroll() 调用的时候执行
         */
        public void onNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull V child,
                @NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed,
                int dyUnconsumed, @NestedScrollType int type, @NonNull int[] consumed) {
            ...
        }

        /*
         * TODO  当 NestedScrollingChild#dispatchNestedPreScroll() 调用的时候执行
         */
        @Deprecated
        public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout,
                @NonNull V child, @NonNull View target, int dx, int dy, @NonNull int[] consumed) {
            // Do nothing
        }

       /*
        * TODO 当 NestedScrollingChild2#dispatchNestedPreScroll() 调用的时候执行
        */
        public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout,
                @NonNull V child, @NonNull View target, int dx, int dy, @NonNull int[] consumed,
                @NestedScrollType int type) {
            if (type == ViewCompat.TYPE_TOUCH) {
                onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
            }
        }

       /*
        * TODO 当 NestedScrollingChild#dispatchNestedFling() 调用的时候执行
        */
        public boolean onNestedFling(@NonNull CoordinatorLayout coordinatorLayout,
                @NonNull V child, @NonNull View target, float velocityX, float velocityY,
                boolean consumed) {
            return false;
        }

        /*
         * TODO 当 NestedScrollingChild2#dispatchNestedFling() 调用的时候执行
         */
        public boolean onNestedPreFling(@NonNull CoordinatorLayout coordinatorLayout,
                @NonNull V child, @NonNull View target, float velocityX, float velocityY) {
            return false;
        }

        /*
        * TODO 恢复状态
        */
        public void onRestoreInstanceState(@NonNull CoordinatorLayout parent, @NonNull V child,
                @NonNull Parcelable state) {

        }

        /*
          * TODO  保存状态 和V iew / Activity 保存状态一样
          *
          * tips: 1. 必须保证View在xml中设置了id (android:id="@+id/XXX")
          *       2. 必须保证view参数中有behavior属性 (app:behavior="www.com.XXX")
          */
        @Nullable
        public Parcelable onSaveInstanceState(@NonNull CoordinatorLayout parent, @NonNull V child) {
            return BaseSavedState.EMPTY_STATE;
        }
    }

搞这么多,谁能看懂啊...别着急,本篇换个思路,先来1个简单的 demo,然后再步入源码分析,由俭入奢。

我的第一个自定义 Behavior

先来自定义一个跟随手指滑动的View。

MoveView.kt

class MoveView @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {

    private var lastOffset = OffSet(0f, 0f)

    override fun onTouchEvent(event: MotionEvent): Boolean {
        when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                lastOffset = OffSet(event.x, event.y)
            }
            MotionEvent.ACTION_MOVE -> {
                val dx = event.x - lastOffset.x
                val dy = event.y - lastOffset.y

                ViewCompat.offsetLeftAndRight(this, dx.toInt())
                ViewCompat.offsetTopAndBottom(this, dy.toInt())
            }
        }
        return true
    }
}

这段代码有手就行,不用多讲。

70be000a6c876d77539f77e77a2e3453.gif

现在要做的就是,当View移动的时候,另一个View颜色跟随变化。

来看一眼布局:

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout 
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.example.meterialproject.view.behavior.demo1.MoveView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:background="@color/purple_700" />


    <androidx.appcompat.widget.AppCompatImageView
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:background="@color/green"
        app:layout_behavior=".view.behavior.demo1.ColorBehavior" />

</androidx.coordinatorlayout.widget.CoordinatorLayout>

自定义behavior

# ColorBehavior.kt

class ColorBehavior(val context: Context, attrs: AttributeSet?) :
    CoordinatorLayout.Behavior<AppCompatImageView>(context, attrs) {

    companion object {
        const val TAG = "szjColorBehavior"
    }

    /*
     * TODO 判断跟随变化的 View
     * @param parent: CoordinatorLayout
     * @param child: 当前的 view
     * @param dependency: 需要依赖的 View  
     */
    override fun layoutDependsOn(
        parent: CoordinatorLayout,
        child: AppCompatImageView,
        dependency: View
    ): Boolean {
        return dependency is MoveView
    }

    // 改变当前的状态
    override fun onDependentViewChanged(
        parent: CoordinatorLayout,
        child: AppCompatImageView,
        dependency: View
    ): Boolean {
        // 随机颜色
        child.setBackgroundColor(context.randomColor())
        return super.onDependentViewChanged(parent, child, dependency)
    }
}

参数介绍

56b93ea8d98b1cd8f98cab4d9eb6454f.png

b4e26490454636e8694468b5541f2025.png

这里有2个重要的角色。

  • 依赖方:AppCompatImageView

  • 被依赖方:MoveView

那么现在依赖方(AppCompatImageView)就可以监听到被依赖方(MoveView)的变化。

初步原理图:

b50ff20fb4341526e1e912154556e9b7.png

初步分析:

CoordinatorLayout保存记录所有的ChildView , 通过addOnPreDrawListener()监听所有View的变化。然后通过遍历所有保存的childView,判断childView中是否有behavior。如果有behavior那么在判断Behavior#layoutDependsOn()是否依赖MoveView。如果也依赖于MoveView,那么就将事件传递给对应的 Behavior#onDependentViewChanged。

所以到底是什么意思呢?

一句话总结就是如果Behavior#layoutDependsOn()返回true就会执行到 Behavior#onDependentViewChanged()的方法。

addOnPreDrawListener和setOnHierarchyChangeListener

  • addOnPreDrawListener是ViewGroup用来监听所有childView变化的,只要childView有变化,例如DOWN / MOVE事件等。

  • setOnHierarchyChangeListener是ViewGroup监听自身childView发生变化来响应的一共有2个方法。

    • onChildViewAdded添加响应

    • onChildViewRemoved删除响应

效果图:

452f8c7ccb60f86e4bb7d8e7bed5bd6b.gif

我认为这是CoordinatorLayout的核心!

/   CoordinatorLayout源码分析   /

看源码的小技巧

首先必须知道你要看的源码是做什么的,必须会使用。比如说本篇 CoordinatorLayout的源码,就是用来协调childView的。

  • 构造函数 (必看)

  • 如果是View根据View的生命周期看每个方法的实现,如果是ViewGroup别忘记看 generateLayoutParams()方法

  • 紧盯着主线流程

  • 先看某个方法的作用,在看细节

  • 熟能生巧,多看,多分析,多画流程图

  • 加注释!

  • 可以尝试打断点

CoordinatorLayout采取的策略就是跟随view的生命周期开始看,首先从构造聊起!

构造方法

# CoordinatorLayout.java

public CoordinatorLayout(@NonNull Context context) {
    this(context, null);
}

public CoordinatorLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
    this(context, attrs, R.attr.coordinatorLayoutStyle);
}

public CoordinatorLayout(@NonNull Context context, @Nullable AttributeSet attrs,
        @AttrRes int defStyleAttr) {
    // 监听 ViewGroup 的变化
    super.setOnHierarchyChangeListener(new HierarchyChangeListener());
    ...
}
# CoordinatorLayout.java

private class HierarchyChangeListener implements OnHierarchyChangeListener {
    @Override
    public void onChildViewAdded(View parent, View child) {
        ...
    }

    @Override
    public void onChildViewRemoved(View parent, View child) {
      // 重点 当 childView 被删除的时候调用
        onChildViewsChanged(EVENT_VIEW_REMOVED);
       ..
    }
}

核心方法下面源码也会用到!!

# CoordinatorLayout.java

final void onChildViewsChanged(@DispatchChangeEvent final int type) {
        // 当前的 childView
        final int childCount = mDependencySortedChildren.size();

               // 循环所有 childView 
        // 为了找到需要依赖的 view
        for (int i = 0; i < childCount; i++) {
            final View child = mDependencySortedChildren.get(i);
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
          // 如果 当前的状态 = EVENT_PRE_DRAW 并且 childView 不可见 
          // 那么退出本次循环
            if (type == EVENT_PRE_DRAW && child.getVisibility() == View.GONE) {
                continue;
            }
            ...

            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();

                // 如果 childView 有依赖的Behavior 并且 Behavior#layoutDependsOn() = true 就继续执行
                if (b != null && b.layoutDependsOn(this, checkChild, child)) {
                    if (type == EVENT_PRE_DRAW && checkLp.getChangedAfterNestedScroll()) {
                        checkLp.resetChangedAfterNestedScroll();
                        continue;
                    }

                    final boolean handled;
                    switch (type) {
                        case EVENT_VIEW_REMOVED:
                            // 传递的参数为 EVENT_VIEW_REMOVED
                            // 会执行到这里 
                                // 如果 CoordinatorLayout中 child 被删除,就会调用到对应的 Behavior 上
                            b.onDependentViewRemoved(this, checkChild, child);
                            handled = true;
                            break;
                        default:
                            handled = b.onDependentViewChanged(this, checkChild, child);
                            break;
                    }
                }
            }
        }
    }

效果图

bd0ac4c04fb90277b6e7b327baab98e5.gif

这里一旦CoordinatorLayout()删除,就会调用到对应Behavior#onDependentViewRemoved()方法上。

tips:具体代码细节请下载完整代码观看,完整代码底部给出。

看CoordinatorLayout(ViewGroup)的源码,还需要注意一个方法,generateLayoutParams。

generateLayoutParams

@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
    return new LayoutParams(getContext(), attrs);
}
# CoordinatorLayout#LayoutParams.java

public static class LayoutParams extends MarginLayoutParams {
   LayoutParams(@NonNull Context context, @Nullable AttributeSet attrs) {

     mBehaviorResolved = a.hasValue(
       R.styleable.CoordinatorLayout_Layout_layout_behavior);
     if (mBehaviorResolved) {
        // 解析 behavior 
       mBehavior = parseBehavior(context, attrs, a.getString(
         R.styleable.CoordinatorLayout_Layout_layout_behavior));
     }

     if (mBehavior != null) {
      // 如果behavior解析成功,则调用 behavior第一个生命周期方法 onAttachedToLayoutParams()
       mBehavior.onAttachedToLayoutParams(this);
     }
   }
}

通过反射解析Behavior。

# CoordinatorLayout#LayoutParams.java

static Behavior parseBehavior(Context context, AttributeSet attrs, String name) {
    if (TextUtils.isEmpty(name)) {
        return null;
    }

    final String fullName;
    if (name.startsWith(".")) {
        fullName = context.getPackageName() + name;
    } else if (name.indexOf('.') >= 0) {
        fullName = name;
    } else {
        fullName = !TextUtils.isEmpty(WIDGET_PACKAGE_NAME)
                ? (WIDGET_PACKAGE_NAME + '.' + name)
                : name;
    }

    try {
        Map<String, Constructor<Behavior>> constructors = sConstructors.get();
        if (constructors == null) {
            constructors = new HashMap<>();
            sConstructors.set(constructors);
        }
        Constructor<Behavior> c = constructors.get(fullName);
        if (c == null) {
            final Class<Behavior> clazz =
                    (Class<Behavior>) Class.forName(fullName, false, context.getClassLoader());
            c = clazz.getConstructor(CONSTRUCTOR_PARAMS);
            c.setAccessible(true);
            constructors.put(fullName, c);
        }
        return c.newInstance(context, attrs);
    } catch (Exception e) {
        throw new RuntimeException("Could not inflate Behavior subclass " + fullName, e);
    }
}

LayoutParams作用就一个 解析参数,最重要的是解析Behavior。

onAttachedToWindow()

onAttachedToWindow()之前的文章介绍过了。

@Override
    public void onAttachedToWindow() {
      super.onAttachedToWindow();
      resetTouchBehaviors(false);
      // mNeedsPreDrawListener = false
      if (mNeedsPreDrawListener) {
        if (mOnPreDrawListener == null) {
          mOnPreDrawListener = new OnPreDrawListener();
        }
        final ViewTreeObserver vto = getViewTreeObserver();
        vto.addOnPreDrawListener(mOnPreDrawListener);
      }
       // 不会执行
      if (mLastInsets == null && ViewCompat.getFitsSystemWindows(this)) {
        ViewCompat.requestApplyInsets(this);
      }
      mIsAttachedToWindow = true;
    }

这里只会执行resetTouchBehaviors(false)方法。

打个断点看看。

5bdfb64e33edcaba9cfb223005912ef1.png

@param notifyOnInterceptTouchEvent : false。

private void resetTouchBehaviors(boolean notifyOnInterceptTouchEvent) {
    final int childCount = getChildCount();
    for (int i = 0; i < childCount; i++) {
        final View child = getChildAt(i);
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
        final Behavior b = lp.getBehavior();
        if (b != null) {
            if (notifyOnInterceptTouchEvent) {
                b.onInterceptTouchEvent(this, child, cancelEvent);
            } else {
               // 当绑定Window的时候,会直接调用1次,behavior#onTouchEvent方法 
                b.onTouchEvent(this, child, cancelEvent);
            }
            cancelEvent.recycle();
        }
    }
}

onMeasure()

# CoordinatorLayout.java

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    prepareChildren();
    ensurePreDrawListener();
    ...
    final int childCount = mDependencySortedChildren.size();
    for (int i = 0; i < childCount; i++) {
            final Behavior b = lp.getBehavior();
      // 会在这里执行 Behavior#onMeasureChild() 方法 将onMeasure() 传递给childView
      if (b == null || !b.onMeasureChild(this, child, childWidthMeasureSpec, keylineWidthUsed,
                                         childHeightMeasureSpec, 0)) {
        onMeasureChild(child, childWidthMeasureSpec, keylineWidthUsed,
                       childHeightMeasureSpec, 0);
      }
    }
}
  • prepareChildren() 通过有向无环图的数据结构给mDependencySortedChildren赋值

  • ensurePreDrawListener()添加view变化监听

  • b.onMeasureChild()执行Behavior#onMeasureChild()

# prepareChildren:

// 保存子view
private final List<View> mDependencySortedChildren = new ArrayList<>();
// 有向无环图的数据结构
private final DirectedAcyclicGraph<View> mChildDag = new DirectedAcyclicGraph<>();

private void prepareChildren() {
    mDependencySortedChildren.clear();
    mChildDag.clear();
    // 这里采用有向无环图的数据结构保存,如果不知道有向无环图,只需要知道,这里会保存view即可
    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);
        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)) {
                    mChildDag.addNode(other);
                }
                mChildDag.addEdge(other, view);
            }
        }
    }

   // 添加到 mDependencySortedChildren 中 
    mDependencySortedChildren.addAll(mChildDag.getSortedList());
    Collections.reverse(mDependencySortedChildren);
}
# ensurePreDrawListener:

void ensurePreDrawListener() {
     ...
    if (hasDependencies != mNeedsPreDrawListener) {
        if (hasDependencies) {
          // 会走这里  
            addPreDrawListener();
        } else {
            removePreDrawListener();
        }
    }
}

void addPreDrawListener() {
  // mIsAttachedToWindow 在 onAttchedToWindow 设置为 true
  if (mIsAttachedToWindow) {
    if (mOnPreDrawListener == null) {
      mOnPreDrawListener = new OnPreDrawListener();
    }
    final ViewTreeObserver vto = getViewTreeObserver();
    vto.addOnPreDrawListener(mOnPreDrawListener);
  }
  mNeedsPreDrawListener = true;
}

class OnPreDrawListener implements ViewTreeObserver.OnPreDrawListener {
  @Override
  public boolean onPreDraw() {
    // 如果ChildView有任何变化 就会执行这里
    onChildViewsChanged(EVENT_PRE_DRAW);
    return true;
  }
}

onChildViewsChanged()这个方法上面提到过,是监听 CoordinatorLayout删除childView的时候,调用onChildViewsChanged(EVENT_VIEW_REMOVED),最终执行到 **behavior.onDependentViewRemoved(this, checkChild, child);**上。

这段代码的意思是如果childView有任何变化,就会通过双层for循环,执行到对应的 behavior#onDependentViewChanged()。

final void onChildViewsChanged(@DispatchChangeEvent final int type) {
   // 通过双层for循环,找到依赖的view
    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) {
            continue;
        }

        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();

            if (b != null && b.layoutDependsOn(this, checkChild, child)) {
                final boolean handled;
                switch (type) {
                    case EVENT_VIEW_REMOVED:
                        // 当view删除的时候调用这里
                        b.onDependentViewRemoved(this, checkChild, child);
                        handled = true;
                        break;
                    default:
                            // 如果coordinatorLayout的childView发生一点点变化,就会执行到这里 
                        handled = b.onDependentViewChanged(this, checkChild, child);
                        break;
                }
        }
    }
}

onLayout()

protected void onLayout(boolean changed, int l, int t, int r, int b) {
    final int layoutDirection = ViewCompat.getLayoutDirection(this);
    final int childCount = mDependencySortedChildren.size();
    for (int i = 0; i < childCount; i++) {
        final View child = mDependencySortedChildren.get(i);

        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
        final Behavior behavior = lp.getBehavior();
        // 在这里执行 Behavior#onLayoutChild()
        if (behavior == null || !behavior.onLayoutChild(this, child, layoutDirection)) {
            onLayoutChild(child, layoutDirection);
        }
    }
}

这段代码比较简单,就是通过for循环找到对应的Behavior调用Behavior#onLayoutChild()方法即可。

drawChild()

众所周知,ViewGroup如果想绘制调用onDraw是不起作用,得调用dispatchDraw()。当调用dispatchDraw()的时候,通过drawChild来绘制每一个ChildView。

protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
    final LayoutParams lp = (LayoutParams) child.getLayoutParams();
    if (lp.mBehavior != null) {
      // getScrimOpacity 默认为0
        final float scrimAlpha = lp.mBehavior.getScrimOpacity(this, child);
        if (scrimAlpha > 0f) {
          // 如果getScrimOpacity!= 0 就设置颜色 
            mScrimPaint.setColor(lp.mBehavior.getScrimColor(this, child));
            mScrimPaint.setAlpha(clamp(Math.round(255 * scrimAlpha), 0, 255));
                ....
    }
    return super.drawChild(canvas, child, drawingTime);
}

尝鲜一下:

class ColorBehavior(val context: Context, attrs: AttributeSet?) :
    CoordinatorLayout.Behavior<AppCompatImageView>(context, attrs) {
   override fun getScrimColor(parent: CoordinatorLayout, child: AppCompatImageView): Int {
        return Color.RED
    }
    override fun getScrimOpacity(parent: CoordinatorLayout, child: AppCompatImageView): Float {
        return 0.5f
    }
}

118d445ff79ce1e80f000dbdf61c3e4d.png

嚯,真是我的梦中情色~

那如果2个Behavior都设置颜色呢?

daae87c708b691fed2e10404a0ff609d.png

红色 + 黄色 = 橙色,合情合理。

onDetachedFromWindow()

当view从屏幕上消失后的时候调用该方法。Activity#onDestroy()方法后执行:

@Override
    public void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        resetTouchBehaviors(false);
        if (mNeedsPreDrawListener && mOnPreDrawListener != null) {
            final ViewTreeObserver vto = getViewTreeObserver();
            vto.removeOnPreDrawListener(mOnPreDrawListener);
        }
        if (mNestedScrollingTarget != null) {
            onStopNestedScroll(mNestedScrollingTarget);
        }
        mIsAttachedToWindow = false;
    }

这里面就是一些注销绑定。至此生命周期方法就结束了。那么再来看看处理事件的方法。

onInterceptTouchEvent和onTouchEvent

public boolean onInterceptTouchEvent(MotionEvent ev) {

        if (action == MotionEvent.ACTION_DOWN) {
            // 分发 Behavior#onInterceptTouchEvent事件
            // 上面多看过这段代码,这里就不赘述了
            resetTouchBehaviors(true);
        }
        final boolean intercepted = performIntercept(ev, TYPE_ON_INTERCEPT);

        if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
            // 分发 Behavior#onInterceptTouchEvent事件
            // 上面多看过这段代码,这里就不赘述了
            resetTouchBehaviors(true);
        }
}

public boolean onTouchEvent(MotionEvent ev) {
  //  performIntercept(ev, TYPE_ON_TOUCH) 分发 Behavior#onTouchEvent() 事件
  if (mBehaviorTouchView != null || (cancelSuper = performIntercept(ev, TYPE_ON_TOUCH))) {
            // Safe since performIntercept guarantees that
            // mBehaviorTouchView != null if it returns true
            final LayoutParams lp = (LayoutParams) mBehaviorTouchView.getLayoutParams();
            final Behavior b = lp.getBehavior();
            if (b != null) {
                handled = b.onTouchEvent(this, mBehaviorTouchView, ev);
            }
        }
}
private boolean performIntercept(MotionEvent ev, final int type) {
    ... 
    final int childCount = topmostChildList.size();
    for (int i = 0; i < childCount; i++) {
        final View child = topmostChildList.get(i);
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
        final Behavior b = lp.getBehavior();
        .... 

        if (!intercepted && b != null) {
            switch (type) {
                case TYPE_ON_INTERCEPT:
                // 分发 Behavior#onInterceptTouchEvent() 事件 
                    intercepted = b.onInterceptTouchEvent(this, child, ev);
                    break;
                case TYPE_ON_TOUCH:
                // 分发 Behavior#onTouchEvent() 事件
                    intercepted = b.onTouchEvent(this, child, ev);
                    break;
            }
            if (intercepted) {
                mBehaviorTouchView = child;
            }
        }
    }
    return intercepted;
}

小结

看了这么多源码,其实原理很简单,在onMeasure()的时候保存childView,通过 PreDrawListener监听childView的变化,最终通过双层for循环找到对应的Behavior,分发任务即可。CoordinatorLayout实现了NestedScrollingParent2,那么在childView实现了NestedScrollingChild方法时候也能解决滑动冲突问题。

比如childView为RecyclerView的时候,就会分发任务给 CoordinatorLayout ,上上篇NestedScrollView源码分析提到过。比如RecyclerView#dispatchNestedPreScroll()方法就会分发到**CoordantorLayout#onNestedPreScroll()**上。

13c6f342b57ccbdc1e1c65833e4d16a7.png

其他代码都相同,就不复制了,我相信我已经说清楚了!实战效果就是采用了这个特性来完成的!

onSaveInstanceState()和onRestoreInstanceState()

在View中。

  • 保存状态通过 onSaveInstanceState()

  • 恢复状态通过 onRestoreInstanceState()

在Behavior中也是如此。看一眼源码位置。

保存数据

# CoordinatorLayout.java

protected Parcelable onSaveInstanceState() {
    final SavedState ss = new SavedState(super.onSaveInstanceState());

    final SparseArray<Parcelable> behaviorStates = new SparseArray<>();
    for (int i = 0, count = getChildCount(); i < count; i++) {
        final View child = getChildAt(i);
        final int childId = child.getId();
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
        final Behavior b = lp.getBehavior();
        // ViewID != null && 有 beehavier
        if (childId != NO_ID && b != null) {
          // 执行 Behavior.onSaveInstanceState()
            Parcelable state = b.onSaveInstanceState(this, child);
            if (state != null) {
                behaviorStates.append(childId, state);
            }
        }
    }
    ss.behaviorStates = behaviorStates;
    return ss;
}

恢复数据

protected void onRestoreInstanceState(Parcelable state) {
     ....
    final SparseArray<Parcelable> behaviorStates = ss.behaviorStates;

    for (int i = 0, count = getChildCount(); i < count; i++) {
        ...
        final LayoutParams lp = getResolvedLayoutParams(child);
        final Behavior b = lp.getBehavior();
                 // ViewID != null && 有 beehavier
        if (childId != NO_ID && b != null) {
            Parcelable savedState = behaviorStates.get(childId);
            if (savedState != null) {
              // 执行 Behavior.onRestoreInstanceState()
                b.onRestoreInstanceState(this, child, savedState);
            }
        }
    }
}

这里要注意必须设置id才能起作用,不要问我为啥,说多了都是泪😭

使用

# MoveBehavior.java

override fun onSaveInstanceState(parent: CoordinatorLayout, child: T): Parcelable {
        return bundleOf(
            TEXT_STRING to "博主带你上高速!",
            TEXT_INT to 1120,
            TEXT_FLOAT to 88.88f,
            TEXT_STRING_LIST to arrayListOf("a", "n", "d", "r", "o", "i", "d"),

            // 重点
            PARCELABLE to super.onSaveInstanceState(parent, child),
        )
    }

    /*
     * 作者:android 超级兵
     * 创建时间: 4/22/22 4:24 PM
     * TODO 恢复参数
     */
    override fun onRestoreInstanceState(parent: CoordinatorLayout, child: T, state: Parcelable) {
        (state as? Bundle)?.apply {
            super.onRestoreInstanceState(parent, child, getParcelable(PARCELABLE) ?: return)

            val string = getString(TEXT_STRING)
            val int = getInt(TEXT_INT)
            val float = getFloat(TEXT_FLOAT)
            val stringArrayList = getStringArrayList(TEXT_STRING_LIST)
            Log.i(
                TAG, "onRestoreInstanceState"
                        + "\tstring:$string"
                        + "\tint:$int"
                        + "\tfloat:$float"
                        + "\tstringArrayList:$stringArrayList"
            )
        }
    }

效果图

e20792b552ac16bcecb48bdc7219e1d9.gif

/   实战   /

先来看看效果图:

5c0cee5de409b4ec29d95766956bb07e.gif

布局

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout 
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.appcompat.widget.AppCompatTextView
        android:id="@+id/headView"
        android:layout_width="match_parent"
        android:layout_height="200dp"
        android:text="我是head"
        app:layout_behavior=".view.behavior.demo2.HeadRecyclerViewBehavior" />


    <androidx.viewpager2.widget.ViewPager2
        android:id="@+id/viewpager"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior=".view.behavior.demo2.ScrollRecyclerViewBehavior" />


    <RadioGroup
        android:id="@+id/radioGroup"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_behavior=".view.behavior.demo2.BottomNavigationBehavior">
              ....
    </RadioGroup>

</androidx.coordinatorlayout.widget.CoordinatorLayout>

这里采用了3个自定义Behavior完成。文件地址如下所示:

https://gitee.com/lanyangyangzzz/material-project/tree/master/app/src/main/java/com/example/meterialproject/view/behavior/demo2

这里就不复制代码了,如果需要请下载完整代码查看。

完整代码如下所示:

https://gitee.com/lanyangyangzzz/material-project/tree/master

推荐阅读:

我写的书,《第一行代码 第3版》已出版!

关于 Android 渲染你应该了解的知识点

前人用GreenDao留下的坑,全线被扣了绩效

欢迎关注我的公众号

学习技术或投稿

6aba5928070283d4a57526234f39518c.png

9d2daa0d555c552b3a8e6ae998f1d1f1.png

长按上图,识别图中二维码即可关注

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值