android MD进阶(五) coordinatorLayout从源码到实战
本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布[2022-5-7]
前言: 上一篇:android View生命周期 介绍了view的生命周期,上上一篇:android MD进阶[四] NestedScrollView 从源码到实战…因为和本篇息息相关,没有了解过的同学可以先看看这两篇哦 ~
CoordinatorLayout源码版本: 1.1.0
废话不多说,先来看看今天要完成的效果:
效果一 | 效果二 |
---|---|
什么是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就发生颜色的变化
所以本篇的重点就是CoordinatorLayout.Behavior
源码分析,以及使用到实战!
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
}
}
这段代码有手就行,不用多讲
现在要做的就是,当 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() | 用来判断以来的 View | false |
onDependentViewChanged() | 当 layoutDependsOn() = ture,并且当前 view 可见状态下执行 | false |
这里有 2 个重要的角色
- 依赖方: AppCompatImageView
- 被依赖方: MoveView
那么现在依赖方(AppCompatImageView)
就可以监听到被依赖方(MoveView)
的变化
初步原理图:
初步分析:
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 删除响应
效果图:
我认为这是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;
}
}
}
}
}
效果图:
这里一旦 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);
方法
打个断点看看
- @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
}
}
嚯,真是我的梦中情色~
那如果2个Behavior都设置颜色呢?
红色 + 黄色 = 橙色 合情合理
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()**上
其他代码都相同,就不复制了,我相信我已经说清楚了 !
实战效果就是采用了这个特性来完成的!
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"
)
}
}
效果图:
实战
先来看看效果图:
布局:
<?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的小项目 给纯粹的博主一个三连吧
猜你喜欢:
原创不易,您的点赞与关注就是对我最大的支持!