如果还不了解NestedScroll机制的同学可以看:嵌套滑动
1、作为应用的顶层布局
2、作为一个管理容器,管理与子View或者子View之间的交互
功能:
- 处理子控件之间依赖下的交互
- 处理子控件之间的嵌套滑动
- 处理子控件的测量与布局
- 处理子控件的事件拦截与响应
以上四个功能,都建立于 CoordainatorLayout中提供 的一个叫做Behavior的 “插件”之上。Behavior 内部也提供了相应方法来对 应这四个不同的功能
NestedScrolling机制的局限性:
child parent之间 1:1
当CoordainatorLayout中子控件depandency的位置、大小等发生改变的时候,那么在
CoordainatorLayout内部会通知所有依赖depandency的控件,
并调用对应声明的Behavior,告知其依赖的depandency发生改变。
那么如何判断依赖(layoutDependsOn),接受到通知后如何处理(onDependentViewChanged/onDepe ndentViewRemoved),这些都交由Behavior来处理。
这不就是观察者模式。
大家看这张图,由于我这边暂时没法录制gif图,直接就截图了一张,比较简单还是能说清楚的。
当我鼠标移动的时候下面的view跟随移动,最上面的那个view改变颜色。
被观察者的view
public class DependedView extends View {
private float mLastX;
private float mLastY;
private final int mDragSlop;
public DependedView(Context context) {
this(context, null);
}
public DependedView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public DependedView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mDragSlop = ViewConfiguration.get(context).getScaledTouchSlop();
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
mLastX = event.getX();
mLastY = event.getY();
break;
case MotionEvent.ACTION_MOVE:
int dx = (int) (event.getX() - mLastX);
int dy = (int) (event.getY() - mLastY);
if (Math.abs(dx) > mDragSlop || Math.abs(dy) > mDragSlop) {
ViewCompat.offsetTopAndBottom(this, dy);
ViewCompat.offsetLeftAndRight(this, dx);
}
mLastX = event.getX();
mLastY = event.getY();
break;
default:
break;
}
return true;
}
}
观察者view1
public class BrotherFollowBehavior extends CoordinatorLayout.Behavior<View> {
public BrotherFollowBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
return dependency instanceof DependedView;
}
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
child.setY(dependency.getBottom() + 50);
child.setX(dependency.getX());
return true;
}
}
观察者view2
public class BrotherChameleonBehavior extends CoordinatorLayout.Behavior<View> {
private ArgbEvaluator mArgbEvaluator = new ArgbEvaluator();
public BrotherChameleonBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
return dependency instanceof DependedView;
}
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
int color = (int) mArgbEvaluator.evaluate(dependency.getY() / parent.getHeight(), Color.WHITE, Color.BLACK);
child.setBackgroundColor(color);
return false;
}
}
xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".coordinatorstudy.Demo01Activity">
<com.zero.materialdesign.coordinatorstudy.view.DependedView
android:layout_width="80dp"
android:layout_height="40dp"
android:layout_gravity="center"
android:background="#f00"
android:gravity="center"
android:textColor="#fff"
android:textSize="18sp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="跟随兄弟"
app:layout_behavior=".coordinatorstudy.behavior.BrotherFollowBehavior"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="变色兄弟"
app:layout_behavior=".coordinatorstudy.behavior.BrotherChameleonBehavior"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
很明显能观察到想要观察的view:extends CoordinatorLayout.Behavior
layoutDependsOn–》判断该子View是我想要观察的view
onDependentViewChanged–》做出你自己想要的逻辑变化
通过上面的例如大家应该知道它是一对多进行通知的。
嵌套滑动:
CoordinatorLayout实现了NestedScrollingParent2和3接口。那么当事件(scroll或fling)产生后,内部实现了NestedScrollingChild接口的子控件会将事件分发给CoordinatorLayout,CoordinatorLayout又会将事件传递给所有的Behavior。然后在Behavior中实现子控件的嵌套滑动。
public class CoordinatorLayout extends ViewGroup implements NestedScrollingParent2,
NestedScrollingParent3
1.产生事件(scroll或fling)的控件必须是CoordinatorLayout的直接子View 吗?
不是,从嵌套滑动那一篇中我们知道嵌套滑动机制是递归查询父View的。
2.响应Behavior的控件必须是CoordinatorLayout的直接子View吗?
是的,因为它只收集了它的直接子类的之间的关系
测量与布局:
CoordainatorLayout主要负责的是子控件之间的交互,内部控件的测量与布局,都非常简单。在特殊的情况下,如子控件需要处理宽高和布局的时候,那么交由Behavior内部的onMeasureChild与onLayoutChild方法来进行处理
同理:拦截情况
1、为什么在依赖的控件下设置一个behavior,DepandedView位置发生改变的时候就能通知依赖方?
2、Behavior是在哪儿实例化的?
3、CoordinatorLayout是如何区分谁依赖于谁的?
4、onMeasure收集什么时候需要重写onMeasureChild?
5、什么时候需要重写onLayoutChild?
view的生命周期从onAttachedToWindow开始,那么我们就从CoordinatorLayout的对应这个方法找起。
先了解一下:ViewTreeObserver
ViewTreeObserver注册一个观察者来监听视图树,当视图树的布局、视图树的焦点、视图树将要绘制、视图树滚动等发生改变时,ViewTreeObserver都会收到通知,ViewTreeObserver不能被实例化,可以调用View.getViewTreeObserver()来获得
dispatchOnPreDraw():通知观察者绘制即将开始,如果其中的某个观察者返回true,那么绘制将会取消,并且重新安排绘制,如果想在ViewLayout或Viewhierarchy还未依附到Window时,或者在View处于GONE状态时强制绘制,可以手动调用这个方法
ViewTreeObserver常用内部类:
内部类接口 备注
ViewTreeObserver.OnPreDrawListener 当视图树将要被绘制时,会调用的接口
ViewTreeObserver.OnGlobalLayoutListener 当视图树的布局发生改变或者View在视图树的可见状态发生改变时会调用的接口
ViewTreeObserver.OnGlobalFocusChangeListener 当一个视图树的焦点状态改变时,会调用的接口
ViewTreeObserver.OnScrollChangedListener 当视图树的一些组件发生滚动时会调用的接口
ViewTreeObserver.OnTouchModeChangeListener 当视图树的触摸模式发生改变时,会调用的接口
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
resetTouchBehaviors(false);
if (mNeedsPreDrawListener) {
if (mOnPreDrawListener == null) {
mOnPreDrawListener = new OnPreDrawListener();
}
final ViewTreeObserver vto = getViewTreeObserver();
//添加监听
vto.addOnPreDrawListener(mOnPreDrawListener);
}
if (mLastInsets == null && ViewCompat.getFitsSystemWindows(this)) {
// We're set to fitSystemWindows but we haven't had any insets yet...
// We should request a new dispatch of window insets
ViewCompat.requestApplyInsets(this);
}
mIsAttachedToWindow = true;
}
class OnPreDrawListener implements ViewTreeObserver.OnPreDrawListener {
@Override
public boolean onPreDraw() {
//绘制之前
onChildViewsChanged(EVENT_PRE_DRAW);
return true;
}
}
总共三个标志:
static final int EVENT_PRE_DRAW = 0; //绘制之前
static final int EVENT_NESTED_SCROLL = 1;//嵌套滑动之前
static final int EVENT_VIEW_REMOVED = 2;//移除之前
都会调用onChildViewsChanged
凡是源码只需要看有中文注释的地方,跟着思路走
final void onChildViewsChanged(@DispatchChangeEvent final int type) {
final int layoutDirection = ViewCompat.getLayoutDirection(this);
final int childCount = mDependencySortedChildren.size();
final Rect inset = acquireTempRect();
final Rect drawRect = acquireTempRect();
final Rect lastDrawRect = acquireTempRect();
for (int i = 0; i < childCount; i++) {
//跟我们view三部曲中获取子View:getChildAt() 不一样
//mDependencySortedChildren是一个列表,里面存储了所有的子view,拿到子View
final View child = mDependencySortedChildren.get(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (type == EVENT_PRE_DRAW && child.getVisibility() == View.GONE) {
// Do not try to update GONE child views in pre draw updates.
continue;
}
// Check child views before for anchor
for (int j = 0; j < i; j++) {
final View checkChild = mDependencySortedChildren.get(j);
//过滤掉没有依赖关系的子View
if (lp.mAnchorDirectChild == checkChild) {
offsetChildToAnchor(child, layoutDirection);
}
}
// Get the current draw rect of the view
getChildRect(child, true, drawRect);
// Accumulate inset sizes
if (lp.insetEdge != Gravity.NO_GRAVITY && !drawRect.isEmpty()) {
final int absInsetEdge = GravityCompat.getAbsoluteGravity(
lp.insetEdge, layoutDirection);
switch (absInsetEdge & Gravity.VERTICAL_GRAVITY_MASK) {
case Gravity.TOP:
inset.top = Math.max(inset.top, drawRect.bottom);
break;
case Gravity.BOTTOM:
inset.bottom = Math.max(inset.bottom, getHeight() - drawRect.top);
break;
}
switch (absInsetEdge & Gravity.HORIZONTAL_GRAVITY_MASK) {
case Gravity.LEFT:
inset.left = Math.max(inset.left, drawRect.right);
break;
case Gravity.RIGHT:
inset.right = Math.max(inset.right, getWidth() - drawRect.left);
break;
}
}
// Dodge inset edges if necessary
if (lp.dodgeInsetEdges != Gravity.NO_GRAVITY && child.getVisibility() == View.VISIBLE) {
offsetChildByInset(child, inset, layoutDirection);
}
if (type != EVENT_VIEW_REMOVED) {
// Did it change? if not continue
getLastChildRect(child, lastDrawRect);
if (lastDrawRect.equals(drawRect)) {
continue;
}
recordLastChildRect(child, drawRect);
}
// Update any behavior-dependent views for the change
for (int j = i + 1; j < childCount; j++) {
//拿到子View
final View checkChild = mDependencySortedChildren.get(j);
//获取子View参数
final LayoutParams checkLp = (LayoutParams) checkChild.getLayoutParams();
//获取behavior
final Behavior b = checkLp.getBehavior();
//behavior不等于null,且是依赖的view则进入判断
//checkChild依赖方(观察者),child(被观察者)
if (b != null && b.layoutDependsOn(this, checkChild, child)) {
if (type == EVENT_PRE_DRAW && checkLp.getChangedAfterNestedScroll()) {
// If this is from a pre-draw and we have already been changed
// from a nested scroll, skip the dispatch and reset the flag
//绘制之前打个标记
checkLp.resetChangedAfterNestedScroll();
continue;
}
final boolean handled;
switch (type) {
case EVENT_VIEW_REMOVED:
// EVENT_VIEW_REMOVED means that we need to dispatch
// onDependentViewRemoved() instead
//被移除时调用
b.onDependentViewRemoved(this, checkChild, child);
handled = true;
break;
default:
// Otherwise we dispatch onDependentViewChanged()
//其他情况走这里
handled = b.onDependentViewChanged(this, checkChild, child);
break;
}
if (type == EVENT_NESTED_SCROLL) {
// If this is from a nested scroll, set the flag so that we may skip
// any resulting onPreDraw dispatch (if needed)
//当嵌套滑动时还会走这里
checkLp.setChangedAfterNestedScroll(handled);
}
}
}
}
releaseTempRect(inset);
releaseTempRect(drawRect);
releaseTempRect(lastDrawRect);
}
两个局部变量列表,一个是存储所有子View的列表,一个是存储子View之间依赖关系的列表。
private final List<View> mDependencySortedChildren = new ArrayList<>();
private final DirectedAcyclicGraph<View> mChildDag = new DirectedAcyclicGraph<>();
DirectedAcyclicGraph:图的数据结构
类似haspMap,有向无环图
临接表 = 数据+嵌套链表
这里几乎是纯数据结构的知识,不懂也没关系,知道这回事就行了。
1:N 关系
在onMeasure绘制的时候清空再重新存储赋值,度量的时候会先调用下面这个方法
private void prepareChildren() {
mDependencySortedChildren.clear();
mChildDag.clear();
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);
// Now iterate again over the other children, adding any dependencies to the graph
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)) {
// Make sure that the other node is added
mChildDag.addNode(other);
}
// Now add the dependency to the graph
mChildDag.addEdge(other, view);
}
}
}
// Finally add the sorted graph list to our list
mDependencySortedChildren.addAll(mChildDag.getSortedList());
// We also need to reverse the result since we want the start of the list to contain
// Views which have no dependencies, then dependent views after that
Collections.reverse(mDependencySortedChildren);
}
重新度量绘制的时候会调用LayoutParams
//重写LayoutParams
public static class LayoutParams extends MarginLayoutParams
if (mBehaviorResolved) {
//通过反射去实例化Behavior
mBehavior = parseBehavior(context, attrs, a.getString(
R.styleable.CoordinatorLayout_Layout_layout_behavior));
}
在CoordinatorLayout的构造函数里面:监听所有子View的情况
super.setOnHierarchyChangeListener(new HierarchyChangeListener());
private class HierarchyChangeListener implements OnHierarchyChangeListener {
HierarchyChangeListener() {
}
@Override
public void onChildViewAdded(View parent, View child) {
if (mOnHierarchyChangeListener != null) {
mOnHierarchyChangeListener.onChildViewAdded(parent, child);
}
}
@Override
public void onChildViewRemoved(View parent, View child) {
onChildViewsChanged(EVENT_VIEW_REMOVED);
if (mOnHierarchyChangeListener != null) {
mOnHierarchyChangeListener.onChildViewRemoved(parent, child);
}
}
}
监听view的移除和创建
在它的内部:onInterceptTouchEvent和onTouchEvent方法的时候会调用所有的有behavior的子View的对应onInterceptTouchEvent和onTouchEvent,当然一般情况下子View都不会去拦截。
它是一个Viewgroup,大部分情况下会把事件传递给实现了NestedScrollingChild的子View,基本上是recyclerView,然后传递给CoordinatorLayou的相关嵌套方法,之后再调用子View的behavior相关嵌套方法。
找了一张总结的图
下面看一个实例:(我的gif图还是上传失败了,好气!)
内容往上走的时候topView隐藏,当往下走的时候优先显示topView之后才走RecyclerView
可以看到最后一张图,topView先完全显示再走recyclerView
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="match_parent"
android:layout_height="100dp"
android:background="@color/colorPrimaryDark"
android:gravity="center"
android:text="Behavior的嵌套滑动展示"
android:textColor="#fff"
app:layout_behavior=".coordinatorstudy.simplebehavior.SampleHeaderBehavior"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior=".coordinatorstudy.simplebehavior.ScrollerBehavior"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
//没有重写layoutDependsOn,TextView没有依赖recyclerView
//它是依靠NestedScroll机制来变化的,所以下面直接ViewCompat.offsetTopAndBottom
public class SampleHeaderBehavior extends CoordinatorLayout.Behavior<TextView> {
private int mOffsetTopAndBottom;
private int mLayoutTop;
public SampleHeaderBehavior() {
}
public SampleHeaderBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onLayoutChild(CoordinatorLayout parent, TextView child, int layoutDirection) {
parent.onLayoutChild(child,layoutDirection);
//初始滑动的时候实例化了一次,一开始是0,后面没再变过
mLayoutTop = child.getTop();
return true;
}
@Override
public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull TextView child, @NonNull View directTargetChild, @NonNull View target, int axes, int type) {
return true;
}
/**
* 当前方法逻辑其实很简单,头部的布局初始高度300,屏幕内容往上走的时候dy是正值,
* 大家都知道屏幕的圆点在左上角,scrollBar往下走是Y是增加的,但是屏幕是内容是往上走。
* 反之亦然,下面的逻辑:
* 屏幕内容往上走top内容要隐藏,最多走300,mOffsetTopAndBottom初始值肯定是0,
* mOffsetTopAndBottom - dy;dy是正值,开始是负数,(-dy其实就是负数在增加)加上mOffsetTopAndBottom,
* offset = offset < minOffset ? minOffset : (offset > maxOffset ? maxOffset : offset);
*上面这个判断如果还在-300~0之间的话,一直是大于minOffset,小于maxOffset(0),去它自己
* int top = child.getTop();从0到-300,
* int lastOffset = offset - (top - mLayoutTop); 过程中top从0到负数
* 例如:-200-(-5(之间已经滑动了5)),当前最大可以滑动195,这个数字是dy传过来的最大值
* 如果是传递过来一个800,-805小于-300,取-300,已经滑了5,那么最大滑295
*
* 如果是屏幕内容往下走,topView要先显示出来,那么dy就是负值,mOffsetTopAndBottom - dy;
* 例如:-200-(dy==-30),其实就是数字在增加,
* mOffsetTopAndBottom已经是-300
* offset = offset < minOffset ? minOffset : (offset > maxOffset ? maxOffset : offset);
* 如果是先滑5,那么-300-(-5)=-295,大于-300,小于0,还是取它自己-295
* int lastOffset = offset - (top - mLayoutTop); top开始从-300计算
* -295-(-300) = 5,其实就是减去已经滑了多少,下面就是-295
*
* 首先要明白:childView.getHeight();是一个固定值,代表view自身的高度
* child.getTop();是一个变化值,从0到-300,再从-300到0
*
* mOffsetTopAndBottom记录滑了多少,child.getTop();也算是可以得出滑了多少,
* 一个局部变量记录,一个每次都动态获取,双重保障
*
* scrollTo、scrollY这些可以看做是translate的动画平移
*
* 而ViewCompat.offsetTopAndBottom是真正的改变了view的位置属性,可以看做是属性动画
* 往上走传递负数,往下走传递正值
*
* 这个方法我看到网上还有一些比较简单的写法,其实都可以,有很多写法,原理都一致。
*
* @param coordinatorLayout
* @param child
* @param target
* @param dx
* @param dy
* @param consumed
* @param type
*/
@Override
public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull TextView child, @NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
int consumedy = 0;
int offset = mOffsetTopAndBottom - dy;
//我们这个View的高度,固定的,这里是300
int minOffset = -getChildScrollRang(child);
int maxOffset = 0;
offset = offset < minOffset ? minOffset : (offset > maxOffset ? maxOffset : offset);
int top = child.getTop();//在屏幕坐标系的相对位置
int lastOffset = offset - (top - mLayoutTop);
ViewCompat.offsetTopAndBottom(child, lastOffset);
consumedy = mOffsetTopAndBottom - offset;
// 将本次滚动到的位置记录下来
mOffsetTopAndBottom = offset;
consumed[1] = consumedy;
}
// 获取childView最大可滑动距离
private int getChildScrollRang(View childView) {
if (childView == null) {
return 0;
}
return childView.getHeight();
}
@Override
public boolean onNestedPreFling(@NonNull CoordinatorLayout coordinatorLayout, @NonNull TextView child, @NonNull View target, float velocityX, float velocityY) {
return super.onNestedPreFling(coordinatorLayout, child, target, velocityX, velocityY);
}
}
public class ScrollerBehavior extends CoordinatorLayout.Behavior<RecyclerView> {
public ScrollerBehavior() {
}
public ScrollerBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
//很明显recyclerView依赖于TextView的变化
@Override
public boolean layoutDependsOn(CoordinatorLayout parent, RecyclerView child, View dependency) {
return dependency instanceof TextView;
}
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, RecyclerView child, View dependency) {
//dependency是头部的view,如果它已经往上走的话,
//dependency.getBottom()走之后的位置-child.getTop()原来的位置,是负数,则recyclerView也往上走这么多
//反之亦然
ViewCompat.offsetTopAndBottom(child, (dependency.getBottom() - child.getTop()));
return false;
}
}
谷歌官方自带的效果这边就不介绍了,我一开始是从郭霖大神的第一行代码那里看到比较详细的介绍,后来也在网上看了一些,都写得挺好的。