[Android]CoordinatorLayout简介(三)手
参考资料
CoordinatorLayout简介(一)CoordinatorLayout的简单使用
CoordinatorLayout简介(二)几种系统默认Behavior的使用
CoordinatorLayout简介(三)手写一个CoordinatorLayout怎么样?
前言
这是CoordinatorLayout系列的第三篇文章,本来按计划是准备解析源码的,但是粗略规划了一下,发现竟然无从下口,根本原因还是CoordinatorLayout体系过于纷繁复杂,其中包含了嵌套滑动框架的实现、事件传递和拦截、坐标系变换、View绘制流程等等,以至于不知从何说起。
源码中因为稳定性和兼容性的需要,以及各种效果的事件,包含了过多非主流程的代码,这给我们阅读源码也带来了一定的困难,在阅读的过程中经常感觉乱花渐欲迷人眼,为逻辑所困,越陷越深,无奈放弃
本文将手动实现一个CoordinatorLayout+AppBarLayout效果的组件,尽量删掉源码中各种分支逻辑和变换逻辑,着重于CoordinatorLayout主流程,实现方式尽可能还原原生的CoordinatorLayout+AppBarLayout,主要是让我们可以更加容易理解CoordinatorLayout的工作过程
正文
效果图:
NestedParentView[00_00_02--00_00_09].gif实现
先看下工程结构:
image.png
和原生CoordinatorLayout的对应关系:
自定义 | 原生 |
---|---|
NestedParentView | CoordinatorLayout |
NestedChildView | CoordinatorLayout 中定义的滑动组件 |
HeaderView | AppBarLayout |
HeaderBehavior | AppBarLayout$Behavor |
ScrollBehavior | AppBarLayout$ScrollingViewBehavior |
xml中的布局:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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">
<com.dafasoft.custombehavior.view.NestedParentView
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.dafasoft.custombehavior.view.HeaderView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
app:behavior="@string/header_behavior">
<ImageView
android:layout_width="match_parent"
android:layout_height="230dp"
android:scaleType="fitXY"
app:headerScrollFlag="1"
android:src="@drawable/yellow_zero"/>
<TextView
android:layout_width="match_parent"
android:layout_height="30dp"
android:background="@color/chip_background_invalid"
android:layout_alignParentBottom="true"
android:gravity="center"
android:textColor="@color/white"
android:text="页面标题栏"/>
</com.dafasoft.custombehavior.view.HeaderView>
<com.dafasoft.custombehavior.view.NestedChildView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
app:behavior="@string/scroll_behavior">
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<View
android:layout_width="match_parent"
android:layout_height="350dp"
android:background="@color/black"/>
<View
android:layout_width="match_parent"
android:layout_height="350dp"
android:background="@color/purple_200"/>
<View
android:layout_width="match_parent"
android:layout_height="350dp"
android:background="@color/teal_200"/>
</LinearLayout>
</ScrollView>
</com.dafasoft.custombehavior.view.NestedChildView>
</com.dafasoft.custombehavior.view.NestedParentView>
</RelativeLayout>
其中自定义属性behavior
和headerScrollFlag
分别对应CoordinatorLayout组件中的layout_behavior
和layout_scrollFlags
,这需要我们在attrs.xml中声明:
<declare-styleable name="NestedParentView">
<attr name="behavior" format="string" />
</declare-styleable>
<declare-styleable name="HeaderView">
<attr name="headerScrollFlag" format="integer" />
</declare-styleable>
behavior对应的两个String:
<string name="scroll_behavior">com.dafasoft.custombehavior.behavior.ScrollBehavior</string>
<string name="header_behavior">com.dafasoft.custombehavior.behavior.HeaderBehavior</string>
NestedChildView和HeaderView均是NestedParentView的子View,它们的LayoutParams属性是在NestedParentView中进行解析的,解析方法:
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
TypedArray array = c.obtainStyledAttributes(attrs, R.styleable.NestedParentView);
behavior = parseBehavior(c, attrs, array.getString(R.styleable.NestedParentView_behavior));
array.recycle();
}
static Behavior parseBehavior(Context context, AttributeSet attrs, String name) {
if (TextUtils.isEmpty(name)) {
return null;
}
try {
// 获取设置中behavior的值,通过反射初始化其实例
final Class<Behavior> clazz = (Class<Behavior>) Class.forName(name, true,
context.getClassLoader());
Constructor<Behavior> c = clazz.getConstructor(CONSTRUCTOR_PARAMS);
c.setAccessible(true);
return c.newInstance(context, attrs);
} catch (InstantiationException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return null;
}
NestedParentView$LayoutParams的初始化在LayoutInflate的过程中,这一部分属于XML解析的范畴,这里不多讲
NestedParentView$LayoutParams的总体设计:
public static class LayoutParams extends ViewGroup.MarginLayoutParams {
private NestedParentView.Behavior behavior; // 对应View的Behavior
private boolean mDidAcceptNestedScrollTouch; // 对应View接收嵌套滑动的触摸事件
public int gravity = Gravity.NO_GRAVITY;
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
TypedArray array = c.obtainStyledAttributes(attrs, R.styleable.NestedParentView);
behavior = parseBehavior(c, attrs, array.getString(R.styleable.NestedParentView_behavior));
array.recycle();
}
public LayoutParams(int width, int height) {
super(width, height);
}
public LayoutParams(MarginLayoutParams source) {
super(source);
}
public LayoutParams(ViewGroup.LayoutParams source) {
super(source);
}
public NestedParentView.Behavior getBehavior() {
return behavior;
}
public void setBehavior(NestedParentView.Behavior behavior) {
this.behavior = behavior;
}
void setNestedScrollAccepted(int type, boolean accept) {
switch (type) {
case ViewCompat.TYPE_TOUCH:
mDidAcceptNestedScrollTouch = accept;
break;
}
}
boolean isNestedScrollAccepted(int type) {
switch (type) {
case ViewCompat.TYPE_TOUCH:
return mDidAcceptNestedScrollTouch;
}
return false;
}
}
static Behavior parseBehavior(Context context, AttributeSet attrs, String name) {
if (TextUtils.isEmpty(name)) {
return null;
}
try {
// 获取设置中behavior的值,通过反射初始化其实例
final Class<Behavior> clazz = (Class<Behavior>) Class.forName(name, true,
context.getClassLoader());
Constructor<Behavior> c = clazz.getConstructor(CONSTRUCTOR_PARAMS);
c.setAccessible(true);
return c.newInstance(context, attrs);
} catch (InstantiationException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return null;
}
NestedParentView及其子View的布局初始化
这里会涉及到一些View绘制的知识,还不太熟悉的同学可以趁这个机会复习一下
直接看代码:
NestedParentView#onMeasure:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final int paddingLeft = getPaddingLeft();
final int paddingTop = getPaddingTop();
final int paddingRight = getPaddingRight();
final int paddingBottom = getPaddingBottom();
final int widthPadding = paddingLeft + paddingRight;
final int heightPadding = paddingTop + paddingBottom;
int widthUsed = getSuggestedMinimumWidth();
int heightUsed = getSuggestedMinimumHeight();
int childState = 0;
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
if (child.getVisibility() == GONE) {
// If the child is GONE, skip...
continue;
}
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
int keylineWidthUsed = 0;
int childWidthMeasureSpec = widthMeasureSpec;
int childHeightMeasureSpec = heightMeasureSpec;
final Behavior b = lp.getBehavior();
// 如果child的Behavior不为null且onMeasureChild的工作交给Behavior完成,则NestedParentView不处理子View的measure,否则交给系统处理
if (b == null || !b.onMeasureChild(this, child, childWidthMeasureSpec, keylineWidthUsed,
childHeightMeasureSpec, 0)) {
onMeasureChild(child, childWidthMeasureSpec, keylineWidthUsed,
childHeightMeasureSpec, 0);
}
// NestedParentView继承于ViewGroup,它所占用的宽高就是最大的子View占的宽或高
widthUsed = Math.max(widthUsed, widthPadding + child.getMeasuredWidth() +
lp.leftMargin + lp.rightMargin);
heightUsed = Math.max(heightUsed, heightPadding + child.getMeasuredHeight() +
lp.topMargin + lp.bottomMargin);
childState = View.combineMeasuredStates(childState, child.getMeasuredState());
}
final int width = View.resolveSizeAndState(widthUsed, widthMeasureSpec,
childState & View.MEASURED_STATE_MASK);
final int height = View.resolveSizeAndState(heightUsed, heightMeasureSpec,
childState << View.MEASURED_HEIGHT_STATE_SHIFT);
// 设置计算过的宽高
setMeasuredDimension(width, height);
}
NestedParentView#onLayout:
onLayout方法和onMeasure的逻辑类似,都是看behavior要不要处理,behavior不处理交给View作默认处理
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
final int layoutDirection = ViewCompat.getLayoutDirection(this);
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
if (child.getVisibility() == GONE) {
continue;
}
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final NestedParentView.Behavior behavior = lp.getBehavior();
if (behavior == null || !behavior.onLayoutChild(this, child, layoutDirection)) {
onLayoutChild(child, layoutDirection);
}
}
}
接着看下两个Behavior中onMeasure 和 onLayoutChild的实现
首先看HeaderBehavior:
@Override
public boolean onMeasureChild(NestedParentView parent, HeaderView child,
int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec,
int heightUsed) {
final NestedParentView.LayoutParams lp =
(NestedParentView.LayoutParams) child.getLayoutParams();
if (lp.height == NestedParentView.LayoutParams.WRAP_CONTENT) {
// 如果View的高度被设置为WRAP_CONTENT,NestedParentView默认会束缚这个View在其本身所占区域内,因为HeaderView是可以滑动的,
// 因此需要设置MesaureSpce为UNSPECIFIED从而允许其超过其父布局的高度
parent.onMeasureChild(child, parentWidthMeasureSpec, widthUsed,
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), heightUsed);
return true;
}
// Let the parent handle it as normal
return super.onMeasureChild(parent, child, parentWidthMeasureSpec, widthUsed,
parentHeightMeasureSpec, heightUsed);
}
@Override
public boolean onLayoutChild(NestedParentView parent, HeaderView child, int layoutDirection) {
// First let lay the child out
layoutChild(parent, child, layoutDirection);
// 初始化ViewOffsetHelper这个很重要
if (mViewOffsetHelper == null) {
mViewOffsetHelper = new ViewOffsetHelper(child);
}
// ViewOffsetHelper处理View的layout
mViewOffsetHelper.onViewLayout();
// 设置View的边界
if (mTempTopBottomOffset != 0) {
mViewOffsetHelper.setTopAndBottomOffset(mTempTopBottomOffset);
mTempTopBottomOffset = 0;
}
if (mTempLeftRightOffset != 0) {
mViewOffsetHelper.setLeftAndRightOffset(mTempLeftRightOffset);
mTempLeftRightOffset = 0;
}
return true;
}
protected void layoutChild(NestedParentView parent, HeaderView child, int layoutDirection) {
// Let the parent lay it out by default
parent.onLayoutChild(child, layoutDirection);
}
在HeaderBehavior的方法中,有一个非常重要的任务就是ViewOffsetHelper的初始化及其对View的Layout过程的处理,ViewOffsetHelper这个类就是后面我们处理嵌套滑动最重要的一个类,它主要负责NestedParentView的子View的坐标变化,通过坐标变化实现嵌套滑动的效果
再来看ScrollBehavior的实现:
@Override
public boolean onMeasureChild(NestedParentView parent, View child,
int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec,
int heightUsed) {
final int childLpHeight = child.getLayoutParams().height;
if (childLpHeight == ViewGroup.LayoutParams.MATCH_PARENT
|| childLpHeight == ViewGroup.LayoutParams.WRAP_CONTENT) {
// 寻找headerView
View header = null;
int count = parent.getChildCount();
for (int i = 0; i < count; i++) {
if (parent.getChildAt(i) instanceof HeaderView) {
header = parent.getChildAt(i);
}
}
if (header != null) {
if (ViewCompat.getFitsSystemWindows(header)
&& !ViewCompat.getFitsSystemWindows(child)) {
ViewCompat.setFitsSystemWindows(child, true);
if (ViewCompat.getFitsSystemWindows(child)) {
child.requestLayout();
return true;
}
}
int availableHeight = View.MeasureSpec.getSize(parentHeightMeasureSpec);
if (availableHeight == 0) {
availableHeight = parent.getHeight();
}
// 计算ScrollView的可绘制高度,其可绘制高度为父布局的可绘制高度 - header的不可滑动区域
final int height = availableHeight - header.getMeasuredHeight() + getScrollRange(header);
final int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(height,
childLpHeight == ViewGroup.LayoutParams.MATCH_PARENT
? View.MeasureSpec.EXACTLY
: View.MeasureSpec.AT_MOST);
parent.onMeasureChild(child, parentWidthMeasureSpec,
widthUsed, heightMeasureSpec, heightUsed);
return true;
}
}
return false;
}
@Override
public boolean onLayoutChild(@NonNull NestedParentView parent, @NonNull View child, int layoutDirection) {
View headerView = null;
int count = parent.getChildCount();
for (int i = 0; i < count; i++) {
if (parent.getChildAt(i) instanceof HeaderView) {
headerView = parent.getChildAt(i);
}
}
if (headerView != null) {
final NestedParentView.LayoutParams lp =
(NestedParentView.LayoutParams) child.getLayoutParams();
final Rect available = mTempRect1;
// 获取设置了ScrollBehavior属性的View的可布局区域,将其置于HeaderView的下方
available.set(parent.getPaddingLeft() + lp.leftMargin,
headerView.getBottom() + lp.topMargin,
parent.getWidth() - parent.getPaddingRight() - lp.rightMargin,
parent.getHeight() + headerView.getBottom()
- parent.getPaddingBottom() - lp.bottomMargin);
final Rect out = mTempRect2;
GravityCompat.apply(resolveGravity(lp.gravity), child.getMeasuredWidth(),
child.getMeasuredHeight(), available, out, layoutDirection);
child.layout(out.left, out.top, out.right, out.bottom);
}
return true;
}
ScrollBehavior的onMeasureChild和onLayoutChild的主要工作是计算设置了ScrollBehavior的View(这里简称ScrollableView)可绘制高度和其摆放位置
ScrollableView的可绘制高度计算方式为 NestedParentView的高度 - HeaderView的不可滑动区域,这样做的结果很明显 就是当HeaderView滑动到需要悬浮处理时,ScrollableView正好可以全部显示出来
onLayoutChild负责ScrollableView的摆放,实现方法就是通过寻找HeaderView,将HeaderView的底边设为ScrollableView的顶边,再结合onMeasureChild后确定的高度,即可确定ScrollableView的绘制Rect
通过上面对NestedParentView和Behavior的拆分,我们应该能理解为什么我们自定义实现一些CoordinatorLayout的炫酷效果时要自定义Behavior了,也正因为Behavior如此强大的功能,CoordinatorLayout才会变为专治各种花里胡哨的利器
联动效果的实现:
我们只是将NestedParentView和它的子View摆放好肯定是远远不够的,关键要让它们联动起来,
原生CoordinatorLayout使用的是NestedParent 和 NestedChild组件
具体的实现可以看NestedScrollingChild2, NestedScrollingChild3、NestedScrollingParent2, NestedScrollingParent3这四个接口文件中方法的定义,总之,通过一些操作继承于NestedScrollingChild2, NestedScrollingChild3的View是可以和继承于NestedScrollingParent2, NestedScrollingParent3的View进行联动的
现在将NestedChildView继承于NestedScrollingChild2, NestedScrollingChild3,看下onTouchEvent:
NestedChildView#onTouchEvent
@Override
public boolean onTouchEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
mNestedOffsets[0] = mNestedOffsets[1] = 0;
}
final MotionEvent vtev = MotionEvent.obtain(event);
vtev.offsetLocation(mNestedOffsets[0], mNestedOffsets[1]);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mLastTouchX = (int) event.getX();
mLastTouchY = (int) event.getY();
int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
// 设置滑动为垂直滑动
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
// 调用NestedScrollingChild2#startNestedScroll
startNestedScroll(nestedScrollAxis, TYPE_TOUCH);
break;
case MotionEvent.ACTION_MOVE:
final int x = (int) (event.getX());
final int y = (int) (event.getY());
int dy = mLastTouchY - y;
// 滑动布局的修复值
mReusableIntPair[0] = 0;
mReusableIntPair[1] = 0;
// 将嵌套滑动事件分发出去
if (dispatchNestedPreScroll(0, dy, mReusableIntPair, mScrollOffset, TYPE_TOUCH)) {
dy -= mReusableIntPair[1];
// 嵌套滑动的总距离
mNestedOffsets[0] += mScrollOffset[0];
mNestedOffsets[1] += mScrollOffset[1];
// 禁止父布局拦截事件
getParent().requestDisallowInterceptTouchEvent(true);
}
// 设置最后的接触坐标为 实际坐标 - 嵌套滑动的距离
mLastTouchX = x - mScrollOffset[0];
mLastTouchY = y - mScrollOffset[1];
Log.d("zyl", String.format("mLastTouchY = %d y = %d mNestedOffsetsY = %d mScrollOffsetY = %d dy = %d mReusableIntPair = %d", mLastTouchY, y, mNestedOffsets[1], mScrollOffset[1], dy, mReusableIntPair[1]));
if (dy != 0) {
// NestedPreScroll 结束,开始本View的滑动,这里用ScrollView的滑动来模拟
((ScrollView)getChildAt(0)).scrollBy(0, dy);
}
break;
case MotionEvent.ACTION_UP:
break;
default:
break;
}
return true;
}
这里主要的工作,在ACTION_DOWN的时候,传递startNestedScroll事件至父布局,这个事件主要做两件事情:
- 父布局根据该View确定嵌套滑动事件的子View
- 寻找接受嵌套滑动的其他View(在本案例中为HeaderView)
接着看ACTION_MOVE:
这里的工作主要有几个
1.将dispatchNestedPreScroll传递给父布局
2.根据父布局对View坐标系的变化,修改mLastTouchX和mLastTouchY
3.计算总的嵌套滑动距离
4.处理本View的滑动
看下实现:
dispatchNestedPreScroll方法:
@Override
public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed, @Nullable int[] offsetInWindow, int type) {
return getScrollingChildHelper().dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow,
type);
}
其中NestedScrollingChildHelper是在View初始化的时候进行的初始化,这是系统给我们提供的工具类,主要负责nestedScrollingChild 和nestedScrollingParent的通信
NestedScrollingChildHelper#dispatchNestedPreScroll
public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
@Nullable int[] offsetInWindow, @NestedScrollType int type) {
if (isNestedScrollingEnabled()) {
final ViewParent parent = getNestedScrollingParentForType(type);
if (parent == null) {
return false;
}
if (dx != 0 || dy != 0) {
int startX = 0;
int startY = 0;
if (offsetInWindow != null) {
mView.getLocationInWindow(offsetInWindow);
startX = offsetInWindow[0];
startY = offsetInWindow[1];
}
if (consumed == null) {
consumed = getTempNestedScrollConsumed();
}
consumed[0] = 0;
consumed[1] = 0;
ViewParentCompat.onNestedPreScroll(parent, mView, dx, dy, consumed, type);
if (offsetInWindow != null) {
mView.getLocationInWindow(offsetInWindow);
offsetInWindow[0] -= startX;
offsetInWindow[1] -= startY;
}
return consumed[0] != 0 || consumed[1] != 0;
} else if (offsetInWindow != null) {
offsetInWindow[0] = 0;
offsetInWindow[1] = 0;
}
}
return false;
}
在这里通知父布局(即NestedParentView)执行onNestedPreScroll,根据执行结果对坐标系进行转换
看下NestedParentView#onNestedPreScroll
@Override
public void onNestedPreScroll(@NonNull 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 View view = getChildAt(i);
if (view.getVisibility() == GONE) {
// If the child is GONE, skip...
continue;
}
final LayoutParams lp = (LayoutParams) view.getLayoutParams();
if (!lp.isNestedScrollAccepted(type)) {
continue;
}
final Behavior viewBehavior = lp.getBehavior();
if (viewBehavior != null) {
mBehaviorConsumed[0] = 0;
mBehaviorConsumed[1] = 0;
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;
}
}
consumed[0] = xConsumed;
consumed[1] = yConsumed;
if (accepted) {
onChildViewsChanged(EVENT_NESTED_SCROLL);
}
}
这里又交给了Behavior#onNestedPreScroll执行,其中ScrollBehvior没做处理,HeaderBehavior的实现:
@Override
public void onNestedPreScroll(@NonNull NestedParentView parent, @NonNull HeaderView child, @NonNull View target, int dx, int dy, int[] consumed, int type) {
if (dy != 0) {
int min;
int max;
min = -child.getTotalScrollRange();
max = 0;
if (min != max) {
consumed[1] = scroll(parent, child, dy, min, max);
}
}
}
在这里执行的对NestedParentView整体的滚动,实现方式是更改其子View的top和Bottom:
int setHeaderTopBottomOffset(NestedParentView parent, HeaderView header, int newOffset,
int minOffset, int maxOffset) {
final int curOffset = getTopAndBottomOffset();
int consumed = 0;
if (minOffset != 0 && curOffset >= minOffset && curOffset <= maxOffset) {
// If we have some scrolling range, and we're currently within the min and max
// offsets, calculate a new offset
newOffset = MathUtils.clamp(newOffset, minOffset, maxOffset);
if (curOffset != newOffset) {
setTopAndBottomOffset(newOffset);
// Update how much dy we have consumed
consumed = curOffset - newOffset;
}
}
return consumed;
}
int getTopBottomOffsetForScrollingSibling() {
return getTopAndBottomOffset();
}
public boolean setTopAndBottomOffset(int offset) {
if (mViewOffsetHelper != null) {
return mViewOffsetHelper.setTopAndBottomOffset(offset);
} else {
mTempTopBottomOffset = offset;
}
return false;
}
上面的过程是HeaderView的滑动,但是只有HeaderView滑动是不行的,NestedChildView华东也要跟上,回到NestedParentView#onNestedPreScroll,这个方法的最后一行就是处理NestedParentView中其他子View的滑动的:
final void onChildViewsChanged(final int type) {
final int childCount = getChildCount();
final Rect inset = acquireTempRect();
final Rect drawRect = acquireTempRect();
final Rect lastDrawRect = acquireTempRect();
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (child.getVisibility() == View.GONE) {
continue;
}
getChildRect(child, true, drawRect);
for (int j = i + 1; j < childCount; j++) {
final View checkChild = getChildAt(j);
final LayoutParams checkLp = (LayoutParams) checkChild.getLayoutParams();
final Behavior b = checkLp.getBehavior();
// 如果checkChild和child滑动互相依赖
if (b != null && b.layoutDependsOn(this, checkChild, child)) {
final boolean handled;
handled = b.onDependentViewChanged(this, checkChild, child);
}
}
}
releaseTempRect(inset);
releaseTempRect(drawRect);
releaseTempRect(lastDrawRect);
}
ScrollBehavior#onDependentViewChanged:
@Override
public boolean onDependentViewChanged(
@NonNull NestedParentView parent, @NonNull View child, @NonNull View dependency) {
offsetChildAsNeeded(child, dependency);
return false;
}
private void offsetChildAsNeeded(View child, View dependency) {
// 将View移动至dependency的下方
final NestedParentView.Behavior behavior =
((NestedParentView.LayoutParams) dependency.getLayoutParams()).getBehavior();
if (behavior instanceof HeaderBehavior) {
final HeaderBehavior ablBehavior = (HeaderBehavior) behavior;
ViewCompat.offsetTopAndBottom(child, (dependency.getBottom() - child.getTop()));
}
}
通过以上步骤,基本实现了一个乞丐版的CoordinatorLayout
相信照着做一遍,会对CoordinatorLayout的理解加深很多
这里还有很多碎片代码的欠缺,全部代码可以参考文末的DEMO链接
计划接下来的几篇文章继续分析CoordinatorLayout的源码
代码地址: