參考資料
CoordinatorLayout簡介(一)CoordinatorLayout的簡單使用
CoordinatorLayout簡介(二)幾種系統(tǒng)默認Behavior的使用
CoordinatorLayout簡介(三)手寫一個CoordinatorLayout怎么樣?
前言
這是CoordinatorLayout系列的第三篇文章,本來按計劃是準備解析源碼的,但是粗略規(guī)劃了一下,發(fā)現(xiàn)竟然無從下口袖扛,根本原因還是CoordinatorLayout體系過于紛繁復雜,其中包含了嵌套滑動框架的實現(xiàn)十籍、事件傳遞和攔截蛆封、坐標系變換、View繪制流程等等妓雾,以至于不知從何說起娶吞。
源碼中因為穩(wěn)定性和兼容性的需要,以及各種效果的事件械姻,包含了過多非主流程的代碼妒蛇,這給我們閱讀源碼也帶來了一定的困難机断,在閱讀的過程中經(jīng)常感覺亂花漸欲迷人眼,為邏輯所困绣夺,越陷越深吏奸,無奈放棄
本文將手動實現(xiàn)一個CoordinatorLayout+AppBarLayout效果的組件,盡量刪掉源碼中各種分支邏輯和變換邏輯陶耍,著重于CoordinatorLayout主流程奋蔚,實現(xiàn)方式盡可能還原原生的CoordinatorLayout+AppBarLayout,主要是讓我們可以更加容易理解CoordinatorLayout的工作過程
正文
效果圖:
實現(xiàn)
先看下工程結(jié)構(gòu):
和原生CoordinatorLayout的對應關(guān)系:
自定義 | 原生 |
---|---|
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,否則交給系統(tǒng)處理
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的實現(xiàn)
首先看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在其本身所占區(qū)域內(nèi)砚作,因為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的坐標變化,通過坐標變化實現(xiàn)嵌套滑動的效果
再來看ScrollBehavior的實現(xiàn):
@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的不可滑動區(qū)域
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的可布局區(qū)域求冷,將其置于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的不可滑動區(qū)域,這樣做的結(jié)果很明顯 就是當HeaderView滑動到需要懸浮處理時窍霞,ScrollableView正好可以全部顯示出來
onLayoutChild負責ScrollableView的擺放,實現(xiàn)方法就是通過尋找HeaderView,將HeaderView的底邊設為ScrollableView的頂邊拯坟,再結(jié)合onMeasureChild后確定的高度但金,即可確定ScrollableView的繪制Rect
通過上面對NestedParentView和Behavior的拆分,我們應該能理解為什么我們自定義實現(xiàn)一些CoordinatorLayout的炫酷效果時要自定義Behavior了郁季,也正因為Behavior如此強大的功能冷溃,CoordinatorLayout才會變?yōu)閷V胃鞣N花里胡哨的利器
聯(lián)動效果的實現(xiàn):
我們只是將NestedParentView和它的子View擺放好肯定是遠遠不夠的,關(guān)鍵要讓它們聯(lián)動起來梦裂,
原生CoordinatorLayout使用的是NestedParent 和 NestedChild組件
具體的實現(xiàn)可以看NestedScrollingChild2, NestedScrollingChild3似枕、NestedScrollingParent2, NestedScrollingParent3這四個接口文件中方法的定義,總之年柠,通過一些操作繼承于NestedScrollingChild2, NestedScrollingChild3的View是可以和繼承于NestedScrollingParent2, NestedScrollingParent3的View進行聯(lián)動的
現(xiàn)在將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;
// 調(diào)用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;
// 將嵌套滑動事件分發(fā)出去
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 結(jié)束,開始本View的滑動,這里用ScrollView的滑動來模擬
((ScrollView)getChildAt(0)).scrollBy(0, dy);
}
break;
case MotionEvent.ACTION_UP:
break;
default:
break;
}
return true;
}
這里主要的工作答憔,在ACTION_DOWN的時候味赃,傳遞startNestedScroll事件至父布局,這個事件主要做兩件事情:
- 父布局根據(jù)該View確定嵌套滑動事件的子View
- 尋找接受嵌套滑動的其他View(在本案例中為HeaderView)
接著看ACTION_MOVE:
這里的工作主要有幾個
1.將dispatchNestedPreScroll傳遞給父布局
2.根據(jù)父布局對View坐標系的變化虐拓,修改mLastTouchX和mLastTouchY
3.計算總的嵌套滑動距離
4.處理本View的滑動
看下實現(xiàn):
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初始化的時候進行的初始化心俗,這是系統(tǒng)給我們提供的工具類,主要負責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)執(zhí)行onNestedPreScroll蓉驹,根據(jù)執(zhí)行結(jié)果對坐標系進行轉(zhuǎn)換
看下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執(zhí)行城榛,其中ScrollBehvior沒做處理,HeaderBehavior的實現(xiàn):
@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);
}
}
}
在這里執(zhí)行的對NestedParentView整體的滾動态兴,實現(xiàn)方式是更改其子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()));
}
}
通過以上步驟,基本實現(xiàn)了一個乞丐版的CoordinatorLayout
相信照著做一遍敢订,會對CoordinatorLayout的理解加深很多
這里還有很多碎片代碼的欠缺王污,全部代碼可以參考文末的DEMO鏈接
計劃接下來的幾篇文章繼續(xù)分析CoordinatorLayout的源碼
代碼地址: