android MD 进阶[五] CoordinatorLayout 从源码到实战..

303 阅读3分钟

android MD进阶(五) coordinatorLayout从源码到实战

本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布[2022-5-7]

前言: 上一篇:android View生命周期 介绍了view的生命周期,上上一篇:android MD进阶[四] NestedScrollView 从源码到实战…因为和本篇息息相关,没有了解过的同学可以先看看这两篇哦 ~

A7BCEF591048750AF18F85DB138D60A9

CoordinatorLayout源码版本: 1.1.0

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

效果一效果二
151453B7B10A19FB731C25BA9A1D3EB06194567F134EE6347085F1BEC3F8DA24

什么是coordinatorLayout?

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

  • LinearLayout 线性布局 常用来水平/垂直摆放childView
  • RelativeLayout 相对布局 常用来 AView 在 BView 的某个位置,来摆放 childView
  • ConstraintLayout 约束布局 常用来 CView 在 DView 的某个约束位置,很好的解决了布局嵌套层级过深以及布局困难的问题

所以coordinatorLayout的特征是什么呢?

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

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

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

那什么是 behavior 呢 ?

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

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

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

image-20220421104658891

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

1CEDEB5DD950E0704E85022716FFE60B

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

DBD3A473AE594E3C1C7824AF7472A1ED

搞这么多,谁能看懂啊… 别着急,本篇换个思路,先来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
    }
}

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

4580704514CA9327E2F524BB5A2C354B

现在要做的就是,当 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)
    }
}

参数介绍:

参数作用默认
layoutDependsOn()用来判断以来的 Viewfalse
onDependentViewChanged()当 layoutDependsOn() = ture,并且当前 view 可见状态下执行false

image-20220422201057102

这里有 2 个重要的角色

  • 依赖方: AppCompatImageView
  • 被依赖方: MoveView

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

初步原理图:

CoordinatorLayout

初步分析:

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 删除响应

效果图:

gif_1

我认为这是CoordinatorLayout的核心!

CoordinatorLayout 源码分析

看源码的小技巧:

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

  • 构造函数 (必看)
  • 如果是View 根据 View 的生命周期看每个方法的实现, 如果是ViewGroup别忘记看 generateLayoutParams() 方法
  • 紧盯着主线流程
  • 先看某个方法的作用,在看细节
  • 熟能生巧,多看,多分析,多画流程图
  • 加注释!
  • 可以尝试打断点

6DE23385E4379C7CA838FB40646EADB9

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

效果图:

gif_2

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

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

CoordinatorLayout(ViewGroup) 的源码,还需要注意一个方法, generateLayoutParams , 该方法 上一章:View 生命周期 介绍过 这里就不啰嗦了

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() 上一章:View 生命周期 介绍过了

@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); 方法

打个断点看看

image-20220424111104889

  • @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
    }
}

0D85D8A67091C8A088F8D26E9D65D776

嚯,真是我的梦中情色~

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

image-20220424142059737

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

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()**上

image-20220424151724473

其他代码都相同,就不复制了,我相信我已经说清楚了 !

实战效果就是采用了这个特性来完成的!

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"
            )
        }
    }

效果图:

gif_3

实战

先来看看效果图:

6194567F134EE6347085F1BEC3F8DA24

布局:

<?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完成 文件地址

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

完整代码

后续会写一个MD + MVVM + KT + jetpack的小项目 给纯粹的博主一个三连吧

67A9F5D70577316795377A8C30272711

猜你喜欢:

原创不易,您的点赞与关注就是对我最大的支持!