/ 今日科技快讯 /
近日,苹果、谷歌和微软等科技巨头联合宣布,将在未来一年内在他们控制的所有移动、桌面和浏览器平台上支持无密码登录功能。实际上,这意味着在不远的将来,无密码身份验证将进入所有主要的设备平台,包括安卓和iOS移动操作系统;Chrome、Edge和Safari浏览器;以及Windows和MacOS桌面环境。
/ 作者简介 /
周六,上班。
本篇文章来自android超级兵的投稿,文章主要分享了他对CoordinatorLayout详细的探索分析,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章。
android超级兵的博客地址:
https://blog.csdn.net/weixin_44819566?type=blog
/ 前言 /
CoordinatorLayout源码版本:1.1.0。
废话不多说,先来看看今天要完成的效果:
效果一
效果二
/ 什么是CoordinatorLayout? /
CoordinatorLayout意为协调者布局,每个ViewGroup都有相应的特征。例如:
LinearLayout线性布局常用来水平/垂直摆放childView
RelativeLayout相对布局常用来AView在BView的某个位置,来摆放childView
ConstraintLayout约束布局常用来CView在DView的某个约束位置,很好的解决了布局嵌套层级过深以及布局困难的问题
所以CoordinatorLayout的特征是什么呢?
它的特征为:可以配合每个 childView 来协调使用。比如在移动A View的过程中我想让B View和C View发生改变(例如效果一),那么就可以用它,它的缺点也非常明显,布局起来稍稍优点麻烦...
难道说我用CoordinatorLayout就可以让他协调起来?用意念吗?那当然不行。
单指CoordinatorLayout没有太大的作用,重要的是CoordinatorLayout配合behavior来使用!
那什么是behavior呢?
behavior为行为,假设A View移动过程中B View想要跟随者A View移动,那么B View直接在xml中添加一个app:layout_behavior="XXX"属性,然后自定义CoordinatorLayout.Behavior即可。
最后再来看一张代码图,先有个初步的了解!
这个效果是如果MoveView发生移动,那么ImageView就发生颜色的变化。
所以本篇的重点就是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)
}
}
参数介绍
这里有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。
generateLayoutParams
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LayoutParams(getContext(), attrs);
}
# CoordinatorLayout#LayoutParams.java
public static class LayoutParams extends MarginLayoutParams {
LayoutParams(@NonNull Context context, @Nullable AttributeSet attrs) {
mBehaviorResolved = a.hasValue(
R.styleable.CoordinatorLayout_Layout_layout_behavior);
if (mBehaviorResolved) {
// 解析 behavior
mBehavior = parseBehavior(context, attrs, a.getString(
R.styleable.CoordinatorLayout_Layout_layout_behavior));
}
if (mBehavior != null) {
// 如果behavior解析成功,则调用 behavior第一个生命周期方法 onAttachedToLayoutParams()
mBehavior.onAttachedToLayoutParams(this);
}
}
}
通过反射解析Behavior。
# CoordinatorLayout#LayoutParams.java
static Behavior parseBehavior(Context context, AttributeSet attrs, String name) {
if (TextUtils.isEmpty(name)) {
return null;
}
final String fullName;
if (name.startsWith(".")) {
fullName = context.getPackageName() + name;
} else if (name.indexOf('.') >= 0) {
fullName = name;
} else {
fullName = !TextUtils.isEmpty(WIDGET_PACKAGE_NAME)
? (WIDGET_PACKAGE_NAME + '.' + name)
: name;
}
try {
Map<String, Constructor<Behavior>> constructors = sConstructors.get();
if (constructors == null) {
constructors = new HashMap<>();
sConstructors.set(constructors);
}
Constructor<Behavior> c = constructors.get(fullName);
if (c == null) {
final Class<Behavior> clazz =
(Class<Behavior>) Class.forName(fullName, false, context.getClassLoader());
c = clazz.getConstructor(CONSTRUCTOR_PARAMS);
c.setAccessible(true);
constructors.put(fullName, c);
}
return c.newInstance(context, attrs);
} catch (Exception e) {
throw new RuntimeException("Could not inflate Behavior subclass " + fullName, e);
}
}
LayoutParams作用就一个 解析参数,最重要的是解析Behavior。
onAttachedToWindow()
onAttachedToWindow()之前的文章介绍过了。
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
resetTouchBehaviors(false);
// mNeedsPreDrawListener = false
if (mNeedsPreDrawListener) {
if (mOnPreDrawListener == null) {
mOnPreDrawListener = new OnPreDrawListener();
}
final ViewTreeObserver vto = getViewTreeObserver();
vto.addOnPreDrawListener(mOnPreDrawListener);
}
// 不会执行
if (mLastInsets == null && ViewCompat.getFitsSystemWindows(this)) {
ViewCompat.requestApplyInsets(this);
}
mIsAttachedToWindow = true;
}
这里只会执行resetTouchBehaviors(false)方法。
打个断点看看。
@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完成。文件地址如下所示:
https://gitee.com/lanyangyangzzz/material-project/tree/master/app/src/main/java/com/example/meterialproject/view/behavior/demo2
这里就不复制代码了,如果需要请下载完整代码查看。
完整代码如下所示:
https://gitee.com/lanyangyangzzz/material-project/tree/master
推荐阅读:
欢迎关注我的公众号
学习技术或投稿
长按上图,识别图中二维码即可关注