文章目录
CoordinatorLayout的behavior解析
在上篇文章一篇文章教你学会安卓的嵌套滑动中实现的嵌套滑动是子View
和父View
进行通信,若想实现和兄弟节点通信该怎么实现的,其实也不难,当父View
拿到事件后,就可以遍历到子View
想通信的节点,再把事件交给此兄弟节点则可实现,谷歌是很体谅我们这种小白的,知道让我们自己写会有BUG
,因此谷歌封装了一套布局CoordinatorLayout
。
CoordinatorLayout
协调者布局,可以协调子View
的各种事件,先看效果:
三个View
,一个可以拖动的View
,一个根据拖动View
的高度进行变色的View
,一个跟随拖动View
的View
使用
上面的效果就是借助CoordinatorLayout
实现的,先看如何使用,上面效果的布局文件如下:
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<!-- 可拖动View -->
<com.hbsd.mdviewdemo.view.TouchView
android:layout_marginLeft="150dp"
android:layout_marginTop="150dp"
android:layout_width="100dp"
android:background="@color/teal_700"
android:layout_height="100dp"/>
<!-- 跟随View -->
<TextView
android:background="@color/black"
android:layout_width="100dp"
android:textColor="@color/white"
android:layout_height="wrap_content"
android:gravity="center"
android:text="跟随"
app:layout_behavior=".behavior.BrotherFollowBehavior"/>
<!-- 变色View -->
<TextView
android:gravity="center"
android:layout_width="100dp"
android:layout_height="100dp"
android:text="变色"
app:layout_behavior=".behavior.BrotherChangeColorBehavior"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
不难发现跟随View和变色View有一个陌生的属性则是app:layout_behavior,先说结论,联动效果就是此属性控制的 ,后续我们对其进行详细讲解。
可拖动View
是自定义的,代码如下:
TouchView
class TouchView : View {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int)
: super(context, attrs, defStyleAttr)
private var mLastX = 0f
private var mLastY = 0f
override fun onTouchEvent(event: MotionEvent?): Boolean {
val action = event?.action
when (action) {
MotionEvent.ACTION_DOWN -> {
mLastX = event.getRawX()
mLastY = event.getRawY()
}
MotionEvent.ACTION_MOVE -> {
val dx = (event.getRawX() - mLastX).toInt()
val dy = (event.getRawY() - mLastY).toInt()
ViewCompat.offsetTopAndBottom(this, dy)
ViewCompat.offsetLeftAndRight(this, dx)
mLastX = event.getRawX()
mLastY = event.getRawY()
}
else -> {
}
}
return true
}
}
app:layout_behavior属性指定的值,也是需要我们自己自定义的,先把代码贴出来,目前只需要知道设置此属性就可以产生联动
自定义Behavior
需要实现CoordinatorLayout.Behavior
,其需要一个依赖的泛型,目前只需要知道他的两个方法
layoutDependsOn
和onDependentViewChanged
layoutDependsOn决定是否依赖
onDependentViewChanged决定怎么变化
两个自定义的Behavior
如下:
跟随View
的behavior
BrotherFollowBehavior
class BrotherFollowBehavior : CoordinatorLayout.Behavior<View> {
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
override fun layoutDependsOn(
parent: CoordinatorLayout,
child: View,
dependency: View
): Boolean {
//依赖滑动View,表示和滑动View产生联动
return dependency is TouchView
}
//若layoutDependsOn返回为true,视图改变时就会回调此方法
override fun onDependentViewChanged(
parent: CoordinatorLayout,
child: View,
dependency: View
): Boolean {
//改变当前View的位置
child.y = (dependency.bottom + 20).toFloat()
child.x = dependency.x
return true
}
}
变色View
的behavior
BrotherChangeColorBehavior
class BrotherChangeColorBehavior : CoordinatorLayout.Behavior<View> {
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
private lateinit var mArgbEvaluator : ArgbEvaluator;
init {
mArgbEvaluator = ArgbEvaluator()
}
override fun layoutDependsOn(
parent: CoordinatorLayout,
child: View,
dependency: View,
): Boolean {
//依赖滑动View,表示和滑动View产生联动
return dependency is TouchView
}
override fun onDependentViewChanged(
parent: CoordinatorLayout,
child: View,
dependency: View,
): Boolean {
//改变当前View的北京颜色
val color = mArgbEvaluator.evaluate(dependency.y / parent.height, Color.WHITE, Color.BLACK) as Int
child.setBackgroundColor(color)
return false
}
}
现在还不需要知道Behavior中方法的原理,只需要存在一个疑问,为什么这么写就能绑定依赖,为什么在onDependentViewChanged()中加入变化代码就可以生效 ,这也是本篇文章要解决的问题,接下来我们就对CoordinatorLayout
和Behavior
的原理进行解析,解决上述疑问。
在这开启一下上帝视角Behavior的功能还不仅仅是协调嵌套滑动,还有三个协调功能分别是子控件之间的相互依赖,子控件测量和布局,子控件事件拦截和响应
我们着重分析四个功能是如何实现的
子控件的依赖交互
什么是子控件的依赖交互呢,就是当一个View
删除时或者状态改变,别的View可以感知的到 ,我们文章开头的效果就是使用此功能实现的,监听的是状态改变,诸如位置信息,背景颜色也是状态的一种。
在讲解之前必须提两个类ViewTreeObserver,OnHierarchyChangeListener
两个监听器
ViewTreeObserver
ViewTreeObserver
注册一个观察者来监听视图树,当视图树的布局、视图树的焦点、视图树将要绘制、视图树滚动等发生改变时,ViewTreeObserver
都会收到通知,ViewTreeObserver
不能被实例化,可以调用View.getViewTreeObserver()
来获得。
ViewTreeObserver常用内部类:
内部类接口 | 备注 |
---|---|
ViewTreeObserver.OnPreDrawListener | 当视图树将要被绘制时,会调用的接口 |
ViewTreeObserver.OnGlobalLayoutListener | 当视图树的布局发生改变或者View在视图树的可见状态发生改变时会调用的接口 |
ViewTreeObserver.OnGlobalFocusChangeListener | 当一个视图树的焦点状态改变时,会调用的接口 |
ViewTreeObserver.OnScrollChangedListener | 当视图树的一些组件发生滚动时会调用的接口 |
ViewTreeObserver.OnTouchModeChangeListener | 当视图树的触摸模式发生改变时,会调用的接口 |
在使用时可以给ViewTreeObserver
注册上述监听器,从而监听视图树的一举一动,此篇文章不ViewTreeObserver
的运行原理进行分析,只需要知道作用即可
说到这,是不是对CoordinatorLayout
子控件的依赖交互功能的实现有些思路了呢
在CoordinatorLayout
中注册ViewTreeObserver
并添加某个监听器,从而监听到视图树的变化,再根据子View
的依赖关系进行处理即可,现在不需要理解,后续会深入分析
OnHierarchyChangeListener
此监听器可以监听当前ViewGroup
的add
和remove
public interface OnHierarchyChangeListener {
//在将新子视图添加到父视图时调用
void onChildViewAdded(View parent, View child);
//当子视图从父视图中移除时调用
void onChildViewRemoved(View parent, View child);
}
子View
之间的依赖交互基本就是靠上述两个类实现,后续着重分析CoordinatorLayout
中如何绑定上述的两个监听器
两个监听器的绑定
本节我们分析两个监听器在什么地方进行绑定
OnHierarchyChangeListener的绑定
在CoordinatorLayout
的构造中绑定了OnHierarchyChangeListener
CoordinatorLayout#CoordinatorLayout
public CoordinatorLayout(@NonNull Context context, @Nullable AttributeSet attrs,
@AttrRes int defStyleAttr) {
...
//绑定HierarchyChangeListener,此监听器监听子View的增删
super.setOnHierarchyChangeListener(new HierarchyChangeListener());
...
}
HierarchyChangeListener
private class HierarchyChangeListener implements OnHierarchyChangeListener {
HierarchyChangeListener() {
}
//添加View则触发add
@Override
public void onChildViewAdded(View parent, View child) {
if (mOnHierarchyChangeListener != null) {
mOnHierarchyChangeListener.onChildViewAdded(parent, child);
}
}
//删除View则触发此remove
@Override
public void onChildViewRemoved(View parent, View child) {
//最最最重要的方法,后续进行分析
onChildViewsChanged(EVENT_VIEW_REMOVED);
if (mOnHierarchyChangeListener != null) {
mOnHierarchyChangeListener.onChildViewRemoved(parent, child);
}
}
}
ViewTreeObserver监听器的绑定
ViewTreeObserver
监听器的绑定和View
的生命周期是存在关系的
View的生命周期
回顾View
的生命周期:
Activity
的onCreate
,调用setContentView()
解析xml
,反射创建View
在onResume
中触发绘制流程,执行ViewRootImpl
中的performTraversals
方法
private void performTraversals() {
...
host.dispatchAttachedToWindow(mAttachInfo, 0);
...
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
...
performLayout(lp, mWidth, mHeight);
...
performDraw();
}
measure,layout,draw
都是我们熟知的方法,在他们之前还有一个方法就是dispatchAttachedToWindow()
,在dispatchAttachedToWindow()
中则会执行一系列的生命周期方法,我们不对其进行深入研究,只需要关注生命周期中的一个重要的方法就是onAttachedToWindow(),此方法类似于Activity
的onCreate()
,只要没有被移除屏幕销毁,则onAttachedToWindow()
只会执行一次,其职责和onCreate()
相似,一般是做初始化处理。
开启上帝视角,分析onAttachedToWindow()
onAttachedToWindow()
CoordinatorLayout#onAttachedToWindow
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
...
//第一次加载时不会执行if,不会执行绑定
//但是这里存在思路的,ViewTreeObserver确实是存在的,只是目前没有创建和绑定监听器
if (mNeedsPreDrawListener) {
if (mOnPreDrawListener == null) {
mOnPreDrawListener = new OnPreDrawListener();
}
final ViewTreeObserver vto = getViewTreeObserver();
vto.addOnPreDrawListener(mOnPreDrawListener);
}
...
mIsAttachedToWindow = true;
}
后续根据View的生命周期会执行onMeasure
onMeasure
onMeasure
主要处理依赖图的初始化和ViewTreeObserver
监听器的绑定
绑定ViewTreeObserver监听器
ViewTreeObserver
的监听器在onMeasure
方法中添加
CoordinatorLayout#onMeasure
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//重要中的重要,构建依赖关系,图的创建
prepareChildren(); //先去看下节 依赖的构建
//给ViewTreeObserver添加监听器
ensurePreDrawListener(); //看下1
...
}
1.CoordinatorLayout#ensurePreDrawListener
void ensurePreDrawListener() {
boolean hasDependencies = false;
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
//当前View是否存在依赖关系
if (hasDependencies(child)) { //看下2
hasDependencies = true;
break;
}
}
//mNeedsPreDrawListener初始为false,若存在依赖关系,hasDependencies为true,会命中if
if (hasDependencies != mNeedsPreDrawListener) {
if (hasDependencies) {
//给ViewTreeObserver添加监听器
addPreDrawListener(); //看下3
} else {
removePreDrawListener();
}
}
}
2.CoordinatorLayout#hasDependencies
private boolean hasDependencies(View child) {
//在prepareChildren已经构建过图,此方法判断存不存在此child的依赖
return mChildDag.hasOutgoingEdges(child);
}
3.CoordinatorLayout#addPreDrawListener
void addPreDrawListener() {
//若执行过onAttachedToWindow()则mIsAttachedToWindow为true
if (mIsAttachedToWindow) {
// 绑定监听器
if (mOnPreDrawListener == null) {
mOnPreDrawListener = new OnPreDrawListener();
}
final ViewTreeObserver vto = getViewTreeObserver();
vto.addOnPreDrawListener(mOnPreDrawListener);//mOnPreDrawListener为OnPreDrawListener 看下面分析
}
mNeedsPreDrawListener = true;
}
OnPreDrawListener
class OnPreDrawListener implements ViewTreeObserver.OnPreDrawListener {
@Override
public boolean onPreDraw() {
//和OnHierarchyChangeListener的remove方法类似,只是调用时传入的参数不同,绘制时就会触发此方法
onChildViewsChanged(EVENT_PRE_DRAW);
return true;
}
}
总结:在onMeasure
中初始化子View
的依赖关系,是否依赖取决于是否锚点和Behavior
的layoutDependsOn
方法,紧接着会绑定ViewTreeObserver
监听器,去监听视图树的一举一动
依赖的构建
依赖的构建全部依靠DirectedAcyclicGraph,数据结构为 有向无环图,采用邻接表实现 ,不对其进行深入研究,只需要知道依赖关系使用此类完成,其提供创建边和节点的函数。
CoordinatorLayout#prepareChildren
private void prepareChildren() {
mDependencySortedChildren.clear();
mChildDag.clear();
for (int i = 0, count = getChildCount(); i < count; i++) {
//获取一个子View
final View view = getChildAt(i);
//将Behavior正确赋值,先看下节Behavior的创建,再回来看下面方法
final LayoutParams lp = getResolvedLayoutParams(view);//从(Behavior的创建)小节回来以后,看下1
//设置锚点,可看下下节(锚点的创建),不要跟丢了流程,回顾一下我们为什么看此节,是因为在绑定监听器之前需要先构建View之间的依赖
lp.findAnchorView(this, view);
//图中添加节点
mChildDag.addNode(view);
//第二重循环,找到第一重循环拿到View所有依赖的View
for (int j = 0; j < count; j++) {
//跳过当前View
if (j == i) {
continue;
}
final View other = getChildAt(j);
//判断view(第一重循环的View)是否依赖于other,看下2
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);
}
}
}
//将依赖进行排序
mDependencySortedChildren.addAll(mChildDag.getSortedList());
//因为上述两重循环处理中,是一个View去找它的所有的依赖View,但是最终我们希望集合开头是不依赖任何View的View,因此进行反转
Collections.reverse(mDependencySortedChildren);
}
1.CoordinatorLayout#getResolvedLayoutParams
LayoutParams getResolvedLayoutParams(View child) {
//获取当前View的LayoutParams
final LayoutParams result = (LayoutParams) child.getLayoutParams();
//如果在XML中未指定Behavior属性则命中if
if (!result.mBehaviorResolved) {
//若child本身实现了AttachedBehavior,则将Behavior赋值到LayoutParams中
if (child instanceof AttachedBehavior) {
Behavior attachedBehavior = ((AttachedBehavior) child).getBehavior();
if (attachedBehavior == null) {
Log.e(TAG, "Attached behavior class is null");
}
result.setBehavior(attachedBehavior);
result.mBehaviorResolved = true;
} else {
//根据注释确定Behavior,已弃用,了解即可
//获取当前child的类
Class<?> childClass = child.getClass();
DefaultBehavior defaultBehavior = null;
//往上寻找有没有被注解的类,直到找到一个存在注解的或者childClass为null
while (childClass != null
&& (defaultBehavior = childClass.getAnnotation(DefaultBehavior.class))
== null) {
childClass = childClass.getSuperclass();
}
//如果找到了defaultBehavior则设置Behavior
if (defaultBehavior != null) {
try {
result.setBehavior(
defaultBehavior.value().getDeclaredConstructor().newInstance());
} catch (Exception e) {
Log.e(TAG, "Default behavior class " + defaultBehavior.value().getName()
+ " could not be instantiated. Did you forget"
+ " a default constructor?", e);
}
}
result.mBehaviorResolved = true;
}
}
return result;
}
2.LayoutParams#dependsOn
boolean dependsOn(CoordinatorLayout parent, View child, View dependency) {
return dependency == mAnchorDirectChild //若dependency == 锚点则也算做依赖,举个例子若FloatingActionButton锚点AppBarLayout中的TextView,那么FloatingActionButton会和AppBarLayout建立依赖
|| shouldDodge(dependency, ViewCompat.getLayoutDirection(parent)) //检查具有此 LayoutParams 的视图是否应该避开指定的视图
|| (mBehavior != null && mBehavior.layoutDependsOn(parent, child, dependency));//调用mBehavior的layoutDependsOn判断是否依赖,这也是三种情况中最常见的依赖情况,这是我们接触的Behavior的第一个方法,作用是决定两个View是否依赖
}
总结:子View
的依赖关系通过prepareChildren()
方法建立,内部是图的数据结构,判断两个View
是否依赖主要通过锚点和Behavior的layoutDependsOn方法。
Behavior的创建
Behavior的官方定义为CoordinatorLayout
子视图的交互行为插件。行为实现了用户可以对子视图进行的一个或多个交互。这些交互可能包括拖拽、滑动、甩动或任何其他手势。
笔者是这么理解的,假设ViewA
指定和ViewB
交互,触发协调事件会先通知到ViewA
的父(CoordinatorLayout
),父通过调用ViewA
中指定的Behavior的方法来实现想要的效果。也就是说ViewA
和ViewB
是通过Behavior来协调的。
目前先不分析Behavior中的方法,只需要知道它的作用即可,后续我们会分析Behavior
中的方法
Behavior属性是保存在LayoutParams
中的,下面分析一下他是怎么创建的
熟悉View
创建流程的读者应该知道,View
是在LayoutInflater#rInflate
方法中创建的
LayoutInflater#rInflate
void rInflate(XmlPullParser parser, View parent, Context context,
AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
...
//假设创建的为CoordinatorLayout的子View,那么parent就是CoordinatorLayout
final View view = createViewFromTag(parent, name, context, attrs);
final ViewGroup viewGroup = (ViewGroup) parent;
//generateLayoutParams执行的为CoordinatorLayout的,看下面分析
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
//递归创建子的子View
rInflateChildren(parser, view, attrs, true);
//添加此次创建的View
viewGroup.addView(view, params);
...
}
CoordinatorLayout#generateLayoutParams
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LayoutParams(getContext(), attrs);
}
CoordinatorLayout#LayoutParams
public static class LayoutParams extends MarginLayoutParams {
//Behavior的声明
Behavior mBehavior;
//Behavior是否创建的标志位
boolean mBehaviorResolved = false;
...
//锚点,锚点也是是否依赖的一个因素,关于锚点的作用在 锚点View的创建小节中解释
View mAnchorView;
View mAnchorDirectChild;
...
public LayoutParams(int width, int height) {
super(width, height);
}
LayoutParams(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
final TypedArray a = context.obtainStyledAttributes(attrs,
R.styleable.CoordinatorLayout_Layout);
...
//获取当前View的锚点View的id
mAnchorId = a.getResourceId(R.styleable.CoordinatorLayout_Layout_layout_anchor,
View.NO_ID);
//获取锚点方向
this.anchorGravity = a.getInteger(
R.styleable.CoordinatorLayout_Layout_layout_anchorGravity,
Gravity.NO_GRAVITY);
...
//当前View是否包含layout_behavior属性
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绑定成功
mBehavior.onAttachedToLayoutParams(this);
}
}
}
CoordinatorLayout#parseBehavior
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 {
//static final ThreadLocal<Map<String, Constructor<Behavior>>> sConstructors = new ThreadLocal<>();
//sConstructors为ThreadLocal,内部的map是Behavior构造的缓存
//此处获取到Behavior的缓存
Map<String, Constructor<Behavior>> constructors = sConstructors.get();
//若此时还未创建过,则创建
if (constructors == null) {
constructors = new HashMap<>();
//将map交与ThreadLocal
sConstructors.set(constructors);
}
//用全限定名拿到map的之前Behavior的缓存
Constructor<Behavior> c = constructors.get(fullName);
//若为null,则通过反射获取到相应的Behavior的构造,并将此构造放入缓存
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);
}
//创建新的Behavior
return c.newInstance(context, attrs);
} catch (Exception e) {
throw new RuntimeException("Could not inflate Behavior subclass " + fullName, e);
}
}
总结:在解析xml
时创建LayoutParams
,LayoutParams
中会获取xml
中指定的值,通过路径进行反射创建,本节完毕回到依赖的构建小节继续往下分析
锚点View的创建
锚点View
是做啥的,直接看效果:
看右边的按钮,这个按钮是锚点在Bar
的右下角的
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:src="@drawable/ic_comment"
app:layout_anchor="@id/appBar"
app:layout_anchorGravity="bottom|end" />
本篇文章不对material design
控件进行解释,只需要知道我们将此按钮锚点在了AppBarLayout
上,当AppBarLayout
缩小时Button
的状态也会发生改变,此功能也是依赖于CoordinatorLayout
来实现的
在创建FloatingActionButton
的LayoutParams
时会初始化LayoutParams
中的两个属性,下面代码为CoordinatorLayout#LayoutParams
的构造方法
//获取当前View的锚点View的id
mAnchorId = a.getResourceId(R.styleable.CoordinatorLayout_Layout_layout_anchor,
View.NO_ID);
//获取锚点方向
this.anchorGravity = a.getInteger(
R.styleable.CoordinatorLayout_Layout_layout_anchorGravity,
Gravity.NO_GRAVITY);
在后续创建依赖时,在onMeasure
的prepareChildren
中对其合法性进行判断
CoordinatorLayout#prepareChildren
for (int i = 0, count = getChildCount(); i < count; i++) {
lp.findAnchorView(this, view);
}
LayoutParams#findAnchorView
View findAnchorView(CoordinatorLayout parent, View forChild) {
//若没有指定View,则直接返回
if (mAnchorId == View.NO_ID) {
mAnchorView = mAnchorDirectChild = null;
return null;
}
//若mAnchorView不为null,则判断其是否合法,下面方法主要寻找CoordinatorLayout的锚点的直接布局
//在LayoutParams中有两个属性mAnchorView,mAnchorDirectChild
//mAnchorView的代表真正需要锚点的View
//mAnchorDirectChild的mAnchorView的上层View,是CoordinatorLayout的直系View
//比如说FloatingActionButton锚点AppBarLayout中的TextView,那么mAnchorView则为TextView,mAnchorDirectChild为AppBarLayout
//如此设计的道理也是很简单的,因为锚点到了某个布局的内部,如果CoordinatorLayout不知道它要协调的直系儿子,那么他怎么协调
if (mAnchorView == null || !verifyAnchorView(forChild, parent)) {//看下1
//若之前没有初始化过mAnchorView,那么则在下面方法中初始化
resolveAnchorView(forChild, parent);//看下2
}
return mAnchorView;
}
1.LayoutParams#verifyAnchorView
private boolean verifyAnchorView(View forChild, CoordinatorLayout parent) {
//若id不一致则为不合法
if (mAnchorView.getId() != mAnchorId) {
return false;
}
//directChild最终指向CoordinatorLayout的直接儿子
View directChild = mAnchorView;
//若p不是CoordinatorLayout,就继续往上找
for (ViewParent p = mAnchorView.getParent();
p != parent;
p = p.getParent()) {
//如果p最终为null或者锚点的View就是自己的祖宗,则锚点是无效的
if (p == null || p == forChild) {
mAnchorView = mAnchorDirectChild = null;
return false;
}
if (p instanceof View) {
directChild = (View) p;
}
}
mAnchorDirectChild = directChild;
return true;
}
2.LayoutParams#resolveAnchorView
private void resolveAnchorView(final View forChild, final CoordinatorLayout parent) {
//找到在xml中声明的layout_anchor的View
mAnchorView = parent.findViewById(mAnchorId);
//若此View不为null
if (mAnchorView != null) {
//若锚点View是CoordinatorLayout,且CoordinatorLayout处于编辑模式则此锚点无效
if (mAnchorView == parent) {
if (parent.isInEditMode()) {
mAnchorView = mAnchorDirectChild = null;
return;
}
throw new IllegalStateException(
"View can not be anchored to the the parent CoordinatorLayout");
}
//寻找直接子View,跟上述1中的寻找差不多
View directChild = mAnchorView;
for (ViewParent p = mAnchorView.getParent();
p != parent && p != null;
p = p.getParent()) {
if (p == forChild) {
if (parent.isInEditMode()) {
mAnchorView = mAnchorDirectChild = null;
return;
}
throw new IllegalStateException(
"Anchor must not be a descendant of the anchored view");
}
if (p instanceof View) {
directChild = (View) p;
}
}
mAnchorDirectChild = directChild;
} else {
...
}
}
总结:LayoutParams
的构造中获取两个属性分别是锚点View
的id
和锚点的方向,在依赖构建prepareChildren方法中会初始化两个属性,mAnchorView
和mAnchorDirectChild
,mAnchorDirectChild
是mAnchorView
的父View
,是CoordinatorLayout
的直接子View
,这里一定要记清楚,锚点View
是某个需要锚点View
的LayoutParams
的一个属性,这和CoordinatorLayout
没有关系,分析完毕回到依赖的构建小节继续往下分析。
依赖交互的实现
上述讲了这么多,都是前置内容,本小节则对依赖交互的实现进行解析
上述我们知道了依赖关系的构建,依赖信息最终都保存在了mDependencySortedChildren集合中
在此之前绑定了两个监听器OnHierarchyChangeListener,OnPreDrawListener
回顾一下两个监听器的作用
当删除View
时触发
HierarchyChangeListener#onChildViewRemoved
@Override
public void onChildViewRemoved(View parent, View child) {
//传入删除的标志位,处理删除事件,非常非常重要的方法
onChildViewsChanged(EVENT_VIEW_REMOVED);
//不设置则不执行,一般也不会设置
if (mOnHierarchyChangeListener != null) {
mOnHierarchyChangeListener.onChildViewRemoved(parent, child);
}
}
当视图树绘制时触发:
class OnPreDrawListener implements ViewTreeObserver.OnPreDrawListener {
@Override
public boolean onPreDraw() {
onChildViewsChanged(EVENT_PRE_DRAW);
return true;
}
}
都会触发一个方法,onChildViewsChanged()
这也是最最重要的方法,下面我们对此方法进行分析
CoordinatorLayout#onChildViewsChanged
final void onChildViewsChanged(@DispatchChangeEvent final int type) {
final int layoutDirection = ViewCompat.getLayoutDirection(this);
final int childCount = mDependencySortedChildren.size();
...
//遍历全部有依赖关系的子View,集合中的元素是后边依赖于前面的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;
}
// 检查是否存在锚点View,遍历i之前View,为什么是之前,因为集合是排序了的,永远都是后面依赖前面
for (int j = 0; j < i; j++) {
final View checkChild = mDependencySortedChildren.get(j);
//之前在锚点View的创建小节中知道mAnchorDirectChild的作用,忘记的读者看前面的内容,若找到直接依赖则命中if
if (lp.mAnchorDirectChild == checkChild) {
//处理锚点更新,因为锚点的意思是将某个View固定到某个View的某处,这里需要保持View的锚点View的相对距离不变,下面方法则是处理相对位置
offsetChildToAnchor(child, layoutDirection); //感兴趣可以看(锚点View的偏移处理)小节,不感兴趣可跳过
}
}
...
// 往后查找,后边的View依赖前面的View,现在寻找后面依赖于当前View的View
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();
//若layoutDependsOn返回true则代表依赖,命中if
if (b != null && b.layoutDependsOn(this, checkChild, child)) {
//若是ViewTreeObserver回调的当前方法,则传入EVENT_PRE_DRAW
if (type == EVENT_PRE_DRAW && checkLp.getChangedAfterNestedScroll()) {
//如果这是来自预绘制并且我们已经从嵌套滚动中更改,则跳过调度并重置标志
checkLp.resetChangedAfterNestedScroll();
continue;
}
final boolean handled;
switch (type) {
//只有OnHierarchyChangeListener回调时会命中此case
case EVENT_VIEW_REMOVED:
//本篇文章Behavior的第二个方法,作用是处理删除View时的回调
b.onDependentViewRemoved(this, checkChild, child);
handled = true;
break;
//其他情况都会走下面case
default:
// 本篇文章Behavior的第三个方法,作用是通知Behavior依赖的View状态发生了变化,不同的业务有不同的实现效果
handled = b.onDependentViewChanged(this, checkChild, child);
break;
}
if (type == EVENT_NESTED_SCROLL) {
// 如果是嵌套滑动则设置标志位
checkLp.setChangedAfterNestedScroll(handled);
}
}
}
}
releaseTempRect(inset);
releaseTempRect(drawRect);
releaseTempRect(lastDrawRect);
}
总结:只要视图树要绘制或者有删除View
的操作,都会回调onChildViewsChanged()
方法,在此方法中第一步处理锚点,第二步则会根据传入的type
处理其他依赖,type
右三种分别是
static final int EVENT_PRE_DRAW = 0; //绘制前触发,来自于OnPreDrawListener的回调
static final int EVENT_NESTED_SCROLL = 1; //嵌套滑动触发
static final int EVENT_VIEW_REMOVED = 2; //HierarchyChangeListener的remove回调触发
锚点View的偏移处理
上述我们知道锚点也属于依赖交互的一种,此小节分析锚点时怎么生效的,下面方法是在onChildViewsChanged()
中调用的,职责就是移动相应的View
,固定锚点。
CoordinatorLayout#offsetChildToAnchor
void offsetChildToAnchor(View child, int layoutDirection) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (lp.mAnchorView != null) {
//锚点View的Rect声明,此时为空,安卓的常见操作在缓存里获取空Rect
final Rect anchorRect = acquireTempRect();
//当前View的Rect声明
final Rect childRect = acquireTempRect();
//最终View的位置信息的Rect声明
final Rect desiredChildRect = acquireTempRect();
//不用再往里看,只是获取lp.mAnchorView的位置信息,并给anchorRect赋值
getDescendantRect(lp.mAnchorView, anchorRect);
//不用再往里看,只是获取当前View的位置信息,并给childRect赋值
getChildRect(child, false, childRect);
//获取子View的宽高
int childWidth = child.getMeasuredWidth();
int childHeight = child.getMeasuredHeight();
//此方法初始化desiredChildRect,看下1
getDesiredAnchoredChildRectWithoutConstraints(child, layoutDirection, anchorRect,
desiredChildRect, lp, childWidth, childHeight);
//若最终View,左和左不等或者上和上不等则意味着发生了改变
boolean changed = desiredChildRect.left != childRect.left ||
desiredChildRect.top != childRect.top;
//最终的位置信息可能是越界无效的,在此方法中更正错误的位置信息,看下2
//无效举例:假设锚点左上角,若指定layout_anchorGravity为left,top,且layout_gravity也指定为left,top此时按照对getDesiredAnchoredChildRectWithoutConstraints方法的分析,此时View的left = -childWidth,top = -childHeight,right = 0, bottom = 0,View正好在屏幕外,下面函数就能解决这个问题
constrainChildRect(lp, desiredChildRect, childWidth, childHeight);
//计算滑动的距离
final int dx = desiredChildRect.left - childRect.left;
final int dy = desiredChildRect.top - childRect.top;
//开始滑动
if (dx != 0) {
ViewCompat.offsetLeftAndRight(child, dx);
}
if (dy != 0) {
ViewCompat.offsetTopAndBottom(child, dy);
}
//只要改变了,则回调Behavior的onDependentViewChanged,根据业务进行下一步操作,此方法我们自己指定或者实现的
if (changed) {
// If we have needed to move, make sure to notify the child's Behavior
final Behavior b = lp.getBehavior();
if (b != null) {
b.onDependentViewChanged(this, child, lp.mAnchorView);
}
}
//释放Rect
releaseTempRect(anchorRect);
releaseTempRect(childRect);
releaseTempRect(desiredChildRect);
}
}
1.CoordinatorLayout.getDesiredAnchoredChildRectWithoutConstraints
private void getDesiredAnchoredChildRectWithoutConstraints(View child, int layoutDirection,
Rect anchorRect, Rect out, LayoutParams lp, int childWidth, int childHeight) {
//获取layout_gravity属性的值
final int absGravity = GravityCompat.getAbsoluteGravity(//根据相对重心信息获取绝对重心位置
resolveAnchoredChildGravity(lp.gravity), layoutDirection);//先获取相对重心位置
final int absAnchorGravity = GravityCompat.getAbsoluteGravity(
resolveGravity(lp.anchorGravity),
layoutDirection);
//只需要记住,若指定gravity则一定会包含指定的信息,若没有指定gravity,会有默认值
//layout_gravity默认为CENTER
//layout_anchorGravity默认会根据当前的阅读方式来决定,如果是LTR,则会指定为left|top,若RIL,则会指定为RTL
final int hgrav = absGravity & Gravity.HORIZONTAL_GRAVITY_MASK;
final int vgrav = absGravity & Gravity.VERTICAL_GRAVITY_MASK;
final int anchorHgrav = absAnchorGravity & Gravity.HORIZONTAL_GRAVITY_MASK;
final int anchorVgrav = absAnchorGravity & Gravity.VERTICAL_GRAVITY_MASK;
//左上角坐标信息
int left;
int top;
//定位水平坐标,View和锚点View的相对位置信息不能改变
switch (anchorHgrav) {
default:
case Gravity.LEFT:
left = anchorRect.left;
break;
case Gravity.RIGHT:
left = anchorRect.right;
break;
case Gravity.CENTER_HORIZONTAL:
left = anchorRect.left + anchorRect.width() / 2;
break;
}
//定位纵坐标
switch (anchorVgrav) {
default:
case Gravity.TOP:
top = anchorRect.top;
break;
case Gravity.BOTTOM:
top = anchorRect.bottom;
break;
case Gravity.CENTER_VERTICAL:
top = anchorRect.top + anchorRect.height() / 2;
break;
}
// 若View指定了layout_gravity,默认走CENTER,居中
switch (hgrav) {
default:
case Gravity.LEFT:
left -= childWidth;
break;
case Gravity.RIGHT:
// Do nothing, we're already in position.
break;
case Gravity.CENTER_HORIZONTAL:
left -= childWidth / 2;
break;
}
switch (vgrav) {
default:
case Gravity.TOP:
top -= childHeight;
break;
case Gravity.BOTTOM:
// Do nothing, we're already in position.
break;
case Gravity.CENTER_VERTICAL:
top -= childHeight / 2;
break;
}
//设置当前View应该在的新位置
out.set(left, top, left + childWidth, top + childHeight);
}
2.CoordinatorLayout.constrainChildRect
private void constrainChildRect(LayoutParams lp, Rect out, int childWidth, int childHeight) {
final int width = getWidth();
final int height = getHeight();
//拿到较大大的,让View尽量往右下角偏移
int left = Math.max(getPaddingLeft() + lp.leftMargin, //由于存在getPaddingLeft()的兜底,永远不可能超出边界
//从左边界和从右往左间过去的边界中取一个相对靠左的
Math.min(out.left,
width - getPaddingRight() - childWidth - lp.rightMargin));
//分析和上面差不多
int top = Math.max(getPaddingTop() + lp.topMargin,
Math.min(out.top,
height - getPaddingBottom() - childHeight - lp.bottomMargin));
out.set(left, top, left + childWidth, top + childHeight);
}
总结:ViewTreeObserver
绑定OnPreDrawListener监听器,在视图滚动时,最终会调用draw
,draw
会回调监听器中方法onChildViewsChanged,此时measure
和layout
已经执行完毕,所以锚点View
的Rect
已经发生变化,而当前View
还是之前未滚动锚点View
的位置数据,与新的锚点View
的位置信息不同,因此会判定需要改变,最终发生滑动并回调Behavior
的onDependentViewChanged方法,让程序员进行自己的变化业务处理。此时回到依赖交互的实现小节,在处理锚点的地方继续往下分析。
文章开头的效果的原理解析
先回到文章开头的使用小节,把代码通读一遍,试着自己理解原理(5分钟时间)。时间到,不理解也没有关系,下面对其原理进行解释。
因为我们在XML
中给跟随View指定了BrotherFollowBehavior,给变色View
指定了BrotherChangeColorBehavior
两个Behavior
的layoutDependsOn()
方法是一样的,方法如下:
//读者没有忘记此方法的作用吧,此方法决定两个View是否存在依赖关系,在构建图的时候调用了此fang'fa
override fun layoutDependsOn(
parent: CoordinatorLayout,
child: View,
dependency: View,
): Boolean {
//都依赖于TouchView
return dependency is TouchView
}
无论是跟随还是变色,最终都会触发绘制,只要触发绘制,ViewTreeObserver
绑定的OnPreDrawListener中的onPreDraw
方法就一定会执行,又因为onPreDraw
调用了onChildViewsChanged()
方法,onChildViewsChanged()
会遍历处理所有存在依赖的View
,挨个回调Behavior
的onDependentViewChanged方法
我们再看两种Behavior
中onDependentViewChanged方法的实现
BrotherChangeColorBehavior#onDependentViewChanged
override fun onDependentViewChanged(
parent: CoordinatorLayout,
child: View,
dependency: View,
): Boolean {
//根据依赖的View的高度改变颜色
val color = mArgbEvaluator.evaluate(dependency.y / parent.height, Color.WHITE, Color.BLACK) as Int
child.setBackgroundColor(color)
return false
}
BrotherFollowBehavior#onDependentViewChanged
override fun onDependentViewChanged(
parent: CoordinatorLayout,
child: View,
dependency: View
): Boolean {
//根据所依赖的View的位置改变位置
child.y = (dependency.bottom + 20).toFloat()
child.x = dependency.x
return true
}
说到这开头的效果的原理就很清晰了。
使用时要注意的问题
只要触发View
的整套绘制流程,就要重建依赖图,并遍历所有存在依赖的View
,回调其Behavior
中的方法,假设说CoordinatorLayout
存在的依赖过多,势必会造成卡顿
我们在回想依赖由什么决定
主要是两个方式
Behavior
的layoutDependsOn()
,由layout_behavior属性确定- 锚点的指定,由layout_anchor属性确定
因此这两个属性不易过多使用
子控件协调嵌套滑动
CoordinatorLayout
继承了NestedScrollingParent2和NestedScrollingParent3
嵌套滑动在笔者的另一篇文章中已经分析过,详情请看一篇文章教你学会安卓的嵌套滑动
在这篇文章中默认读者掌握嵌套滑动的相关知识,直接分析原理
NestedScrollingParent
中有7个方法,CoordinatorLayout
的实现如下:
NestedScrollingParent中的方法实现
CoordinatorLayout#onStartNestedScroll
public boolean onStartNestedScroll(View child, View target, int axes, int type) {
boolean handled = false;
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View view = getChildAt(i);
//判断View是否可见,不可见则下一个
if (view.getVisibility() == View.GONE) {
// If it's GONE, don't dispatch
continue;
}
//获取到当前View的LayoutParams
final LayoutParams lp = (LayoutParams) view.getLayoutParams();
//获取Behavior
final Behavior viewBehavior = lp.getBehavior();
//Behavior不为null,则需要回调
if (viewBehavior != null) {
//回调onStartNestedScroll,本篇文章出现的Behavior的第四个方法
final boolean accepted = viewBehavior.onStartNestedScroll(this, view, child,
target, axes, type);
handled |= accepted;
//设置此次嵌套滑动的事件的类型,只有为accepted为true后续的事件才可以消费
lp.setNestedScrollAccepted(type, accepted);
} else {
lp.setNestedScrollAccepted(type, false);
}
}
return handled;
}
CoordinatorLayout#onNestedScrollAccepted
//省略一些非空判断,重点在调用哪个回调
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes, int type) {
//在之前的文章中讲过,不再赘述
mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes, type);
...
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
...
//上个方法的末尾调用了setNestedScrollAccepted,若此方法的第二个参数为false,则不会相应此次嵌套滑动
if (!lp.isNestedScrollAccepted(type)) {
continue;
}
...
if (viewBehavior != null) {
//回调onNestedScrollAccepted,本篇文章出现的Behavior的第五个方法
viewBehavior.onNestedScrollAccepted(this, view, child, target,
nestedScrollAxes, type);
}
}
}
CoordinatorLayout#onNestedPreScroll
//省略非空判断
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed, int type) {
int xConsumed = 0;
int yConsumed = 0;
boolean accepted = false;
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
...
final Behavior viewBehavior = lp.getBehavior();
if (viewBehavior != null) {
//初始化消费的举例
mBehaviorConsumed[0] = 0;
mBehaviorConsumed[1] = 0;
//回调onNestedPreScroll()方法,给mBehaviorConsumed赋值,说明消费了举例,本篇文章出现的Behavior的第六个方法
viewBehavior.onNestedPreScroll(this, view, target, dx, dy, mBehaviorConsumed, type);
//记录消费的举例
xConsumed = dx > 0 ? Math.max(xConsumed, mBehaviorConsumed[0])
: Math.min(xConsumed, mBehaviorConsumed[0]);
yConsumed = dy > 0 ? Math.max(yConsumed, mBehaviorConsumed[1])
: Math.min(yConsumed, mBehaviorConsumed[1]);
accepted = true;
}
}
//记录消费的距离,等待方法执行结束,传回触发嵌套滑动的View
consumed[0] = xConsumed;
consumed[1] = yConsumed;
if (accepted) {
onChildViewsChanged(EVENT_NESTED_SCROLL);
}
}
上述方法中,CoordinatorLayout
自身并没有消费距离,消费与否全由Behavior
的onNestedPreScroll
方法决定,这是很容易想明白的,CoordinatorLayout
是协调者布局,它自身没有消费事件的必要,全权交由Behavior
处理符合自己的职责,不仅是onNestedPreScroll
,下面全部可消费距离的方法都没有消费。
CoordinatorLayout#onNestedScroll
//处理大致和onNestedPreScroll一样,只分析其回调函数即可
public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed, @ViewCompat.NestedScrollType int type,
@NonNull int[] consumed) {
...
for (int i = 0; i < childCount; i++) {
...
//本篇文章出现的Behavior的第七个方法
viewBehavior.onNestedScroll(this, view, target, dxConsumed, dyConsumed,
dxUnconsumed, dyUnconsumed, type, mBehaviorConsumed);
...
}
...
}
CoordinatorLayout#onNestedPreFling
public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
boolean handled = false;
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
...
//本篇文章出现的Behavior的第八个方法
handled |= viewBehavior.onNestedPreFling(this, view, target, velocityX, velocityY);
...
}
return handled;
}
CoordinatorLayout#onNestedFling
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
boolean handled = false;
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
...
本篇文章出现的Behavior的第九个方法
handled |= viewBehavior.onNestedFling(this, view, target, velocityX, velocityY,
consumed);
...
}
...
return handled;
}
CoordinatorLayout#onStopNestedScroll
public void onStopNestedScroll(View target, int type) {
//之前文章已经解析,不再赘述
mNestedScrollingParentHelper.onStopNestedScroll(target, type);
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
...
//本篇文章出现的Behavior的第十个方法
viewBehavior.onStopNestedScroll(this, view, target, type);
...
}
mNestedScrollingTarget = null;
}
接口中的方法和Behavior中方法的对应关系如下:
NestedScrollingParent | Behavior |
---|---|
onStartNestedScroll() | onStartNestedScroll() |
onStopNestedScroll() | onStopNestedScroll() |
onNestedScroll() | onNestedScroll() |
onNestedPreScroll() | onNestedPreScroll() |
onNestedFling() | onNestedFling() |
onNestedPreFling() | onNestedPreFling() |
onNestedScrollAccepted() | onNestedScrollAccepted() |
上述的7个方法都是全权依赖于Behavior
,属于委托实现
总结
绝大部分知识点都是嵌套滑动中的知识,我们只需要注意CoordinatorLayout
是怎么回调Behavior
即可,上面分析可知,每个方法中都有一个for
循环,其回调的时机和是否指定Behavior
存在直接关系,只要指定了Behavior
且支持嵌套滑动,则Behavior
的相关方法一定会触发,这也是和上个功能最大的区别,在依赖交互中只有存在依赖关系的View
会触发回调。
子控件的测量和布局
测量和布局则是onMeasure()
方法和onLayout()
方法
测量onMeasure()
不对CoordinatorLayout
具体的测量细节进行解析,我们关注的重点是onMeasure()
如何做到协调子View
CoordinatorLayout#onMeasure
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
prepareChildren();
ensurePreDrawListener();
...
final int childCount = mDependencySortedChildren.size();
for (int i = 0; i < childCount; i++) {
final View child = mDependencySortedChildren.get(i);
...
//
final Behavior b = lp.getBehavior();
//假设说Behavior的onMeasureChild返回true则代表子View测量事件交由Behavior处理,本篇文章出现的Behavior的第11个方法
if (b == null || !b.onMeasureChild(this, child, childWidthMeasureSpec, keylineWidthUsed,
childHeightMeasureSpec, 0)) {
onMeasureChild(child, childWidthMeasureSpec, keylineWidthUsed,
childHeightMeasureSpec, 0);
}
...
}
...
setMeasuredDimension(width, height);
}
总结:在方法的开始构建依赖,后面遍历依赖,执行依赖View
的Behavior
的onMeasureChild()
方法,如果返回true
则代表此次测量交由Behavior
处理,如果false
则调用默认的子View
测量方法onMeasureChild()
布局onLayout()
实现与onMeasure()
类似
CoordinatorLayout#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的第12个方法
if (behavior == null || !behavior.onLayoutChild(this, child, layoutDirection)) {
onLayoutChild(child, layoutDirection);
}
}
}
总结
上述出现的两个Behavior
方法的作用是拦截,如果Behavior
实现且返回true
,则不走默认实现
子控件事件拦截和响应
本篇文章不对传统事件分发进行讲解,若读者不熟悉传统事件分发,请看笔者的Android事件分发机制详解
CoordinatorLayout
并没有实现dispatchTouchEvent(),CoordinatorLayout
作为协调者布职责就是协调,自身也不应该干预事件的分发,这是符合设计逻辑的。
CoordinatorLayout
实现了onInterceptTouchEvent()和onTouchEvent()
onInterceptTouchEvent()
CoordinatorLayout#onInterceptTouchEvent
public boolean onInterceptTouchEvent(MotionEvent ev) {
final int action = ev.getActionMasked();
if (action == MotionEvent.ACTION_DOWN) {
//重置上次的事件,为新的事件流做准备
resetTouchBehaviors(true); //看下1
}
//是否拦截此次事件
final boolean intercepted = performIntercept(ev, TYPE_ON_INTERCEPT); //看下2,拦截的重要方法
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
resetTouchBehaviors(true);
}
return intercepted;
}
如果是Down,Up,Cancel
事件则会清空上次的事件,而真正的拦截是交给了**performIntercept()**方法
1.CoordinatorLayout#resetTouchBehaviors
private void resetTouchBehaviors(boolean notifyOnInterceptTouchEvent) {
final int childCount = getChildCount();
//遍历所有存在Behavior的View处理cancel事件,如果notifyOnInterceptTouchEvent为true则成功触发onInterceptTouchEvent,如果为false则触发onTouchEvent
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) {
final long now = SystemClock.uptimeMillis();
final MotionEvent cancelEvent = MotionEvent.obtain(now, now,
MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
if (notifyOnInterceptTouchEvent) {
b.onInterceptTouchEvent(this, child, cancelEvent); //Behavior的第13个方法
} else {
b.onTouchEvent(this, child, cancelEvent); //Behavior的第14个方法
}
cancelEvent.recycle();
}
}
//清空所有View的滑动标志位
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
lp.resetTouchBehaviorTracking();//mDidBlockInteraction = false;此标志位在未来是有用的
}
//重置上次拦截事件的View,此View会在下2中赋值
mBehaviorTouchView = null;
mDisallowInterceptReset = false;
}
2.CoordinatorLayout#performIntercept
private boolean performIntercept(MotionEvent ev, final int type) {
boolean intercepted = false;
boolean newBlock = false;
MotionEvent cancelEvent = null;
final int action = ev.getActionMasked();
//Material Design与普通View的最大不同就是Z轴的存在,上层的View和下层的View,到底谁应该来触发事件呢?,肯定是上层的View,因此在CoordinatorLayout中,要对子View进行高度排序,在遍历时从高到低遍历
final List<View> topmostChildList = mTempList1;
getTopSortedChildren(topmostChildList);
//遍历子View
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();
//在非Down事件下,之前的View已经拦截或者不允许后续的View相应,则后续的View要执行Cancel事件
if ((intercepted || newBlock) && action != MotionEvent.ACTION_DOWN) {
// Cancel all behaviors beneath the one that intercepted.
// If the event is "down" then we don't have anything to cancel yet.
if (b != null) {
if (cancelEvent == null) {
final long now = SystemClock.uptimeMillis();
cancelEvent = MotionEvent.obtain(now, now,
MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
}
switch (type) {
case TYPE_ON_INTERCEPT:
b.onInterceptTouchEvent(this, child, cancelEvent);
break;
case TYPE_ON_TOUCH:
b.onTouchEvent(this, child, cancelEvent);
break;
}
}
continue;
}
//分发事件,拦截与否由Behavior的onInterceptTouchEvent()和onTouchEvent()决定
if (!intercepted && b != null) {
switch (type) {
case TYPE_ON_INTERCEPT:
intercepted = b.onInterceptTouchEvent(this, child, ev);
break;
//在onTouchEvent()中会走此分支,主要是保护作用
case TYPE_ON_TOUCH:
intercepted = b.onTouchEvent(this, child, ev);
break;
}
//拦截则保存下此View,mBehaviorTouchView在新的事件流或者此次事件流结束时会被在上述1中被清空
if (intercepted) {
mBehaviorTouchView = child;
}
}
//后边这段代码不好理解
//旧的是否拦截标志位,Down事件一定为false
final boolean wasBlocking = lp.didBlockInteraction(); //返回mDidBlockInteraction的值
//当前新的标志位的值,修改mDidBlockInteraction的值,并返回新的mDidBlockInteraction
final boolean isBlocking = lp.isBlockingInteractionBelow(this, child); //看下3
newBlock = isBlocking && !wasBlocking;
if (isBlocking && !newBlock) {//只有当Behavior的blocksInteractionBelow()方法返回为true且wasBlocking为true时,if才会命中
// Stop here since we don't have anything more to cancel - we already did
// when the behavior first started blocking things below this point.
break;
}
//笔者感觉for循环开头的if和结尾处的if可能有问题,笔者说下自己的理解,可能笔者的理解也存在错误
//首先要正确理解普通事件分发,什么情况下onInterceptTouchEvent方法会被调用,Down事件或者是其他事件流的Down被子View响应的情况下才会执行。mDidBlockInteraction标志位在down事件分发时被正确赋值,假设其他子View响应了Down事件,且能消费Move事件,例如文章开头的效果,则performIntercept()在滑动过程中会被不断被调用。for循环开头的if在不是down事件且newBlock || intercepted的时候命中,我们现在分析的是Move事件因此第一个条件满足,newBlock由Behavior的blocksInteractionBelow()方法决定,intercepted由Behavior的onInterceptTouchEvent()方法决定,我们进行排列组合一共有四种情况,(intercepted,newBlock)(假,假),(假,真),(真,真),(真,假)
//(假,假)每个View都有可能有响应事件的权力,直到层级高的View拦截事件或者不希望层级低的View响应事件时
//(假,真)newBlock为true只有在down事件时才有可能(读者可以试着自己分析),看for循环的最后几行代码,wasBlocking为flase且isBlocking为true的时候newBlock才为true,又因为此if只可能在不是down的情况下命中,因此此条件不成立。
// (真,真)上一种情况newBlock为true不成立,此情况也不存在
// (真,假)高层级View拦截事件,则其他View触发Cancel事件
//再分析最后一个if,其作用是没有需要执行cancel事件的View了,直接break,笔者感觉效果不是这样的,分析如下if命中的条件是isBlocking为true,newBlock为false时,也就是说wasBlocking为true时,Down事件中由于wasBlocking一定为false,永远无法命中if,Move事件中当wasBlocking为true时,则isBlocking也必然为true,此时if命中,结束循环
//综上,只要设置Behavior的blocksInteractionBelow()方法为true,则在move事件中,比当前View层级低的View永远无法响应事件,包括cancel事件,cancel事件只有当非Down且Behavior的blocksInteractionBelow()为false,Behavior的onInterceptTouchEvent()为true时才会触发。笔者的理解为Behavior的blocksInteractionBelow()方法不应该有拦截cancel事件的能力,但是恰恰此种代码设计让其有了这种能力。
}
topmostChildList.clear();
return intercepted;
}
3.CoordinatorLayout#isBlockingInteractionBelow
boolean isBlockingInteractionBelow(CoordinatorLayout parent, View child) {
if (mDidBlockInteraction) {
return true;
}
//此表达式有些华丽花哨拆解一下
//mDidBlockInteraction = (mDidBlockInteraction | (mBehavior != null)) ? mBehavior.blocksInteractionBelow(parent, child) : false;最终return mDidBlockInteraction;
//在Behavior存在时,此值由Behavior的blocksInteractionBelow()方法决定,此方法的作用就是给不给下层的View事件,如果为true,层级低的View就不能相应事件
return mDidBlockInteraction |= mBehavior != null
? mBehavior.blocksInteractionBelow(parent, child)//Behavior的第15个方法
: false;
}
onTouchEvent
public boolean onTouchEvent(MotionEvent ev) {
boolean handled = false;
boolean cancelSuper = false;
MotionEvent cancelEvent = null;
final int action = ev.getActionMasked();
//这里也存在一个问题,虽然说mBehaviorTouchView绝大是否不为null,但是如果为null,将会触发两次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);)//Behavior的第16个方法
}
}
// Keep the super implementation correct
if (mBehaviorTouchView == null) {
handled |= super.onTouchEvent(ev);
} else if (cancelSuper) {
if (cancelEvent == null) {
final long now = SystemClock.uptimeMillis();
cancelEvent = MotionEvent.obtain(now, now,
MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
}
super.onTouchEvent(cancelEvent);
}
if (cancelEvent != null) {
cancelEvent.recycle();
}
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
resetTouchBehaviors(false);
}
return handled;
}
上述代码还是挺好理解,就是执行performIntercept
中保存下来的View
的Behavior
的onTouchEvent
Behavior的方法总结
public static abstract class Behavior<V extends View> {
public Behavior() {
}
public Behavior(Context context, AttributeSet attrs) {
}
//Behavior和LayoutParams关联上时会被调用
public void onAttachedToLayoutParams关联上时会被调用(@NonNull CoordinatorLayout.LayoutParams params) {
}
//Behavior和LayoutParams取消关联上时会被调用
public void onDetachedFromLayoutParams() {
}
//子View拦截事件
public boolean onInterceptTouchEvent(@NonNull CoordinatorLayout parent, @NonNull V child,
@NonNull MotionEvent ev) {
return false;
}
//子View消费事件
public boolean onTouchEvent(@NonNull CoordinatorLayout parent, @NonNull V child,
@NonNull MotionEvent ev) {
return false;
}
//决定视图是否依赖
public boolean layoutDependsOn(@NonNull CoordinatorLayout parent, @NonNull V child,
@NonNull View dependency) {
return false;
}
//依赖试图改变时回调
public boolean onDependentViewChanged(@NonNull CoordinatorLayout parent, @NonNull V child,
@NonNull View dependency) {
return false;
}
//移除依赖时回调
public void onDependentViewRemoved(@NonNull CoordinatorLayout parent, @NonNull V child,
@NonNull View dependency) {
}
//测量视图时回调
public boolean onMeasureChild(@NonNull CoordinatorLayout parent, @NonNull V child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
return false;
}
//布局视图时回调
public boolean onLayoutChild(@NonNull CoordinatorLayout parent, @NonNull V child,
int layoutDirection) {
return false;
}
//NestedScrol则是嵌套滑动中的内容,不过多解释
@Deprecated
public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
@NonNull V child, @NonNull View directTargetChild, @NonNull View target,
@ScrollAxis int axes) {
return false;
}
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;
}
@Deprecated
public void onNestedScrollAccepted(@NonNull CoordinatorLayout coordinatorLayout,
@NonNull V child, @NonNull View directTargetChild, @NonNull View target,
@ScrollAxis int axes) {
// Do nothing
}
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);
}
}
@Deprecated
public void onStopNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
@NonNull V child, @NonNull View target) {
// Do nothing
}
public void onStopNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
@NonNull V child, @NonNull View target, @NestedScrollType int type) {
if (type == ViewCompat.TYPE_TOUCH) {
onStopNestedScroll(coordinatorLayout, child, target);
}
}
@Deprecated
public void onNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull V child,
@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed,
int dyUnconsumed) {
// Do nothing
}
@Deprecated
public void onNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull V child,
@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed,
int dyUnconsumed, @NestedScrollType int type) {
if (type == ViewCompat.TYPE_TOUCH) {
onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed,
dxUnconsumed, dyUnconsumed);
}
}
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) {
// In the case that this nested scrolling v3 version is not implemented, we call the v2
// version in case the v2 version is. We Also consume all of the unconsumed scroll
// distances.
consumed[0] += dxUnconsumed;
consumed[1] += dyUnconsumed;
onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed,
dxUnconsumed, dyUnconsumed, type);
}
@Deprecated
public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout,
@NonNull V child, @NonNull View target, int dx, int dy, @NonNull int[] consumed) {
// Do nothing
}
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);
}
}
public boolean onNestedFling(@NonNull CoordinatorLayout coordinatorLayout,
@NonNull V child, @NonNull View target, float velocityX, float velocityY,
boolean consumed) {
return false;
}
public boolean onNestedPreFling(@NonNull CoordinatorLayout coordinatorLayout,
@NonNull V child, @NonNull View target, float velocityX, float velocityY) {
return false;
}
...
}
为什么需要Behavior?
Behavior
是插件,热拔插,想用时set
,不想使用时拔出即可,
将协调的工作交给Behavior
,做到解耦的效果
总结
CoordinatorLayout
来说还是比较复杂的,我们研究的重点就是其怎么协调,Behavior
是怎么工作的,重点是Behavior
的四个功能
笔者的理解可能有错,希望读者提出自己的见解,笔者加以改正。
✨ 原 创 不 易 , 还 希 望 各 位 大 佬 支 持 一 下 \textcolor{blue}{原创不易,还希望各位大佬支持一下} 原创不易,还希望各位大佬支持一下
👍 点 赞 , 你 的 认 可 是 我 创 作 的 动 力 ! \textcolor{green}{点赞,你的认可是我创作的动力!} 点赞,你的认可是我创作的动力!
⭐️ 收 藏 , 你 的 青 睐 是 我 努 力 的 方 向 ! \textcolor{green}{收藏,你的青睐是我努力的方向!} 收藏,你的青睐是我努力的方向!
✏️ 评 论 , 你 的 意 见 是 我 进 步 的 财 富 ! \textcolor{green}{评论,你的意见是我进步的财富!} 评论,你的意见是我进步的财富!