Android進階寶典 -- CoordinatorLayout協調者佈局原理分析並實現吸頂效果
在上一節Android進階寶典 -- NestedScroll嵌套滑動機制實現吸頂效果 中,我們通過自定義View的形式完成了TabBar的吸頂效果,其實除了這種方式之外,MD控件中提供了一個CoordinatorLayout,協調者佈局,這種佈局同樣可以實現吸頂效果,但是很多夥伴們對於CoordinatorLayout有點兒陌生,或者認為它用起來比較麻煩,其實大多數原因是因為對於它的原理不太熟悉,不知道什麼時候該用什麼樣的組件或者behavior,所以首先了解它的原理,就能夠對CoordinatorLayout駕輕就熟。
1 CoordinatorLayout功能介紹
首先我們先從源碼中能夠看到,CoordinatorLayout只實現了parent接口(這裏如果不清楚parent接口是幹什麼的,建議看看前面的文章,不然根本不清楚我講的是什麼),説明CoordinatorLayout只能作為父容器來使用。
java
public class CoordinatorLayout extends ViewGroup implements NestedScrollingParent2,
NestedScrollingParent3
所以對於CoordinatorLayout來説,它的主要作用就是用來管理子View或者子View之間的聯動交互。所以在上一篇文章中,我們介紹的NestScroll嵌套滑動機制,它其實能夠實現child與parent的嵌套滑動,但是是1對1的;而CoordinatorLayout是能夠管理子View之間的交互,屬於1對多的。
那麼CoordinatorLayout能夠實現哪些功能呢?\ (1)子控件之間的交互依賴;\ (2)子控件之間的嵌套滑動;\ (3)子控件寬高的測量;\ (4)子控件事件攔截與響應;
那麼以上所有的功能實現,全部都是依賴於CoordinatorLayout中提供的一個Behavior插件。CoordinatorLayout將所有的事件交互都扔給了Behavior,目的就是為了解耦;這樣就不需要在父容器中做太多的業務邏輯,而是通過不同的Behavior控制子View產生不同的行為。
1.1 CoordinatorLayout的依賴交互原理
首先我們先看第一個功能,處理子控件之間的依賴交互,這種處理方式其實在很多地方我們都能看到,例如一些小的懸浮窗,你可以拖動它到任何地方,點擊讓其消失的時候,跟隨這個View的其他View也會一併消失。
那麼如何使用CoordinatorLayout來實現這個功能呢?首先我們先看一下CoordinatorLayout處理這種事件的原理。
看一下上面的圖,在協調者佈局中,有3個子View:dependcy、child1、child2;當dependcy的發生位移或者消失的時候,那麼CoordinatorLayout會通知所有與dependcy依賴的控件,並且調用他們內部聲明的Behavior,告知其依賴的dependcy發生變化了。
那麼如何判斷依賴哪個控件,CoordinatorLayout-Behavior提供一個方法:layoutDependsOn,接收到的通知是什麼樣的呢?onDependentViewChanged / onDependentViewRemoved 分別代表依賴的View位置發生了變化和依賴的View被移除,這些都會交給Behavior來處理。
1.2 CoordinatorLayout的嵌套滑動原理
這部分其實還是挺簡單的,如果有上一篇文章的基礎,那麼對於嵌套滑動就非常熟悉了
因為我們前面説過, CoordinatorLayout只能作為父容器,因為只實現了parent接口,所以在CoordinatorLayout內部需要有一個child,那麼當child滑動時,首先會把實現傳遞給父容器,也就是CoordinatorLayout,再由CoordinatorLayout分發給每個child的Behavior,由Behavior來完成子控件的嵌套滑動。
這裏有個問題,每個child都一定是CoordinatorLayout的直接子View嗎?
剩下的兩個功能就比較簡單了,同樣也是在Behavior中進行處理,就不做介紹了。
2 CoordinatorLayout源碼分析
首先這裏先跟大家説一下,在看源碼的時候,我們最好依託於一個實例的實現,從而帶着問題去源碼中尋找答案,例如我們在第一節中提到過的CoordinatorLayout的四大功能,可能都會有這些問題:
(1)e.g. 控件之間的交互依賴,為什麼在一個child下設置一個Behavior,就能夠跟隨DependentView的位置變化一起變化,他們是如何做依賴通信的?
(2)我們在XML中設置Behavior,是在什麼時候實例化的?
(3)我們既然使用了CoordinatorLayout佈局,那麼內部是如何區分誰依賴誰呢?依賴關係是如何確定的?
(4)什麼時候需要重新 onMeasureChild?什麼時候需要重新onLayoutChild?
(5)每個設置Behavior的子View,一定要是CoordinatorLayout的直接子View嗎?
那麼帶着這些問題,我們通過源碼來得到答案。
2.1 CoordinatorLayout的依賴交互實現
如果要實現依賴交互效果,首先需要兩個角色,分別是:DependentView和子View
```kotlin class DependentView @JvmOverloads constructor( val mContext: Context, val attributeSet: AttributeSet? = null, val flag: Int = 0 ) : View(mContext, attributeSet, flag) {
private var paint: Paint
private var mStartX = 0
private var mStartY = 0
init {
paint = Paint()
paint.color = Color.parseColor("#000000")
paint.style = Paint.Style.FILL
isClickable = true
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
canvas?.let {
it.drawRect(
Rect().apply {
left = 200
top = 200
right = 400
bottom = 400
},
paint
)
}
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
when (event?.action) {
MotionEvent.ACTION_DOWN -> {
Log.e("TAG","ACTION_DOWN")
mStartX = event.rawX.toInt()
mStartY = event.rawY.toInt()
}
MotionEvent.ACTION_MOVE -> {
Log.e("TAG","ACTION_MOVE")
val endX = event.rawX.toInt()
val endY = event.rawY.toInt()
val dx = endX - mStartX
val dy = endY - mStartY
ViewCompat.offsetTopAndBottom(this, dy)
ViewCompat.offsetLeftAndRight(this, dx)
postInvalidate()
mStartX = endX
mStartY = endY
}
}
return super.onTouchEvent(event)
}
} ``` 這裏寫了一個很簡單的View,能夠跟隨手指滑動並一起移動,然後我們在當前View下加一個TextView,並讓這個TextView跟着DependentView一起滑動。
```kotlin
class DependBehavior @JvmOverloads constructor(context: Context, attributeSet: AttributeSet) :
CoordinatorLayout.Behavior
override fun layoutDependsOn(
parent: CoordinatorLayout,
child: View,
dependency: View
): Boolean {
return dependency is DependentView
}
override fun onDependentViewChanged(
parent: CoordinatorLayout,
child: View,
dependency: View
): Boolean {
//獲取dependency的位置
child.x = dependency.x
child.y = dependency.bottom.toFloat()
return true
}
} ``` 如果想要達到隨手的效果,那麼就需要給TextView設置一個Behavior,上面我們定義了一個Behavior,它的主要作用就是,當DependentView滑動的時候,通過CoordinatorLayout來通知所有的DependBehavior修飾的View。
在DependBehavior中,我們看主要有兩個方法:layoutDependsOn和onDependentViewChanged,這兩個方法之前在原理中提到過,layoutDependsOn主要是用來決定依賴關係,看child依賴的是不是DependentView;如果依賴的是DependentView,那麼在DependentView滑動的時候,就會通過回調onDependentViewChanged,告知子View當前dependency的位置信息,從而完成聯動。
2.2 CoordinatorLayout交互依賴的源碼分析
那麼接下來,我們看下CoordinatorLayout是如何實現這個效果的。
在看CoordinatorLayout源碼之前,我們首先需要知道View的生命週期,我們知道在onCreate的時候通過setContentView設置佈局文件,如下所示:
```xml
<com.lay.learn.asm.DependentView
android:layout_width="200dp"
android:layout_height="200dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="我是跟隨者"
app:layout_behavior="com.lay.learn.asm.behavior.DependBehavior"
android:textStyle="bold"
android:textColor="#000000"/>
如果我們熟悉setContentView的源碼,系統是通過Inflate的方式解析佈局文件,然後在onResume的時候顯示佈局,然後隨之會調用onAttachedToWindow將佈局顯示在Window上,我們看下onAttachedToWindow這個方法。
java
@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;
}
在這個方法中,設置了addOnPreDrawListener監聽,此監聽在頁面發生變化(滑動、旋轉、重新獲取焦點)會產生回調;
java
class OnPreDrawListener implements ViewTreeObserver.OnPreDrawListener {
@Override
public boolean onPreDraw() {
onChildViewsChanged(EVENT_PRE_DRAW);
return true;
}
}
java
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++) {
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;
}
// Update any behavior-dependent views for the change
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)) {
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);
} ``` 在onChildViewsChanged這個方法中,我們看到有兩個for循環,從mDependencySortedChildren中取出元素,首先我們先不需要關心mDependencySortedChildren這個數組,這個雙循環的目的就是用來判斷View之間是否存在綁定關係。
首先我們看下第二個循環,當拿到LayoutParams中的Behavior之後,就會調用Behavior的layoutDependsOn方法,假設此時child為DependentView,checkChild為TextView; ```java 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)) {
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);
}
}
} ``` 從上面的佈局文件中看,TextView的Behavior中,layoutDependsOn返回的就是true,那麼此時可以進入到代碼塊中,這裏會判斷type類型:EVENT_VIEW_REMOVED和其他type,因為此時的type不是REMOVE,所以就會調用BeHavior的onDependentViewChanged方法。
因為在onAttachedToWindow中,對View樹中所有的元素都設置了OnPreDrawListener的監聽,所以只要某個View發生了變化,都會走到onChildViewsChanged方法中,進行相應的Behavior檢查並實現聯動。
所以第2節開頭的第一個問題,當DependentView發生位置變化時,是如何通信到child中的,這裏就是通過設置了onPreDrawListener來監聽。
第二個問題,Behavior是如何被初始化的?如果自定義過XML屬性,那麼大概就能瞭解,一般都是在佈局初始化的時候,拿到layout_behavior屬性初始化,我們看下源碼。
java
if (mBehaviorResolved) {
mBehavior = parseBehavior(context, attrs, a.getString(
R.styleable.CoordinatorLayout_Layout_layout_behavior));
}
```java static Behavior parseBehavior(Context context, AttributeSet attrs, String name) { if (TextUtils.isEmpty(name)) { return null; }
final String fullName;
if (name.startsWith(".")) {
// Relative to the app package. Prepend the app package name.
fullName = context.getPackageName() + name;
} else if (name.indexOf('.') >= 0) {
// Fully qualified package name.
fullName = name;
} else {
// Assume stock behavior in this package (if we have one)
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);
}
} ``` 通過源碼我們可以看到,拿到全類名之後,通過反射的方式來創建Behavior,這裏需要注意一點,在自定義Behavior的時候,需要兩個構造參數CONSTRUCTOR_PARAMS,否則在創建Behavior的時候會報錯,因為在反射創建Behavior的時候需要獲取這兩個構造參數。
java
static final Class<?>[] CONSTRUCTOR_PARAMS = new Class<?>[] {
Context.class,
AttributeSet.class
};
報錯類型就是:
java
Could not inflate Behavior subclass com.lay.learn.asm.behavior.DependBehavior
2.3 CoordinatorLayout子控件攔截事件源碼分析
其實只要瞭解了其中一個功能的原理之後,其他功能都是類似的。對於CoordinatorLayout中的子View攔截事件,我們可以先看看CoordinatorLayout中的onInterceptTouchEvent方法。 ```java @Override public boolean onInterceptTouchEvent(MotionEvent ev) { final int action = ev.getActionMasked();
// Make sure we reset in case we had missed a previous important event.
if (action == MotionEvent.ACTION_DOWN) {
resetTouchBehaviors(true);
}
final boolean intercepted = performIntercept(ev, TYPE_ON_INTERCEPT);
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
resetTouchBehaviors(true);
}
return intercepted;
}
其中有一個核心方法performIntercept方法,這個方法中我們可以看到,同樣也是拿到了Behavior的onInterceptTouchEvent方法,來優先判斷子View是否需要攔截這個事件,如果不攔截,那麼交給父容器消費,當前一般Behavior中也不會攔截。
java
private boolean performIntercept(MotionEvent ev, final int type) {
boolean intercepted = false;
boolean newBlock = false;
MotionEvent cancelEvent = null;
final int action = ev.getActionMasked();
final List<View> topmostChildList = mTempList1;
getTopSortedChildren(topmostChildList);
// Let topmost child views inspect first
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 || newBlock) && action != MotionEvent.ACTION_DOWN) {
// Cancel all behaviors beneath the one that intercepted.
// If the event is "down" then we don't have anything to cancel yet.
if (b != null) {
if (cancelEvent == null) {
final long now = SystemClock.uptimeMillis();
cancelEvent = MotionEvent.obtain(now, now,
MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
}
switch (type) {
case TYPE_ON_INTERCEPT:
b.onInterceptTouchEvent(this, child, cancelEvent);
break;
case TYPE_ON_TOUCH:
b.onTouchEvent(this, child, cancelEvent);
break;
}
}
continue;
}
if (!intercepted && b != null) {
switch (type) {
case TYPE_ON_INTERCEPT:
intercepted = b.onInterceptTouchEvent(this, child, ev);
break;
case TYPE_ON_TOUCH:
intercepted = b.onTouchEvent(this, child, ev);
break;
}
if (intercepted) {
mBehaviorTouchView = child;
}
}
// Don't keep going if we're not allowing interaction below this.
// Setting newBlock will make sure we cancel the rest of the behaviors.
final boolean wasBlocking = lp.didBlockInteraction();
final boolean isBlocking = lp.isBlockingInteractionBelow(this, child);
newBlock = isBlocking && !wasBlocking;
if (isBlocking && !newBlock) {
// Stop here since we don't have anything more to cancel - we already did
// when the behavior first started blocking things below this point.
break;
}
}
topmostChildList.clear();
return intercepted;
} ```
2.4 CoordinatorLayout嵌套滑動原理分析
對於嵌套滑動,其實在上一篇文章中已經介紹的很清楚了,加上CoordinatorLayout自身的特性,我們知道當子View(指的是實現了nestscrollchild接口的View)嵌套滑動的時候,那麼首先會將事件向上分發到CoordinatorLayout中,所以在parent中的onNestedPreScroll的方法中會拿到回調。 ```java public void onNestedPreScroll(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);
}
} ``` 我們詳細看下這個方法,對於parent的onNestedPreScroll方法,當然也是會獲取到Behavior,這裏也是拿到了子View的Behavior之後,調用其onNestedPreScroll方法,會把手指滑動的距離傳遞到子View的Behavior中。
```xml
<TextView
android:layout_width="match_parent"
android:layout_height="80dp"
android:background="#2196F3"
android:text="這是頂部TextView"
android:gravity="center"
android:textColor="#FFFFFF"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_child"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="80dp"/>
```
所以這裏我們先定義一個Behavior,這個Behavior是用來接收滑動事件分發的。當手指向上滑動的時候,首先將TextView隱藏,然後才能滑動RecyclerView。
```kotlin
class ScrollBehavior @JvmOverloads constructor(
val mContext: Context,
val attributeSet: AttributeSet
) : CoordinatorLayout.Behavior
//相對於y軸滑動的距離
private var mScrollY = 0
//總共滑動的距離
private var totalScroll = 0
override fun onLayoutChild(
parent: CoordinatorLayout,
child: TextView,
layoutDirection: Int
): Boolean {
Log.e("TAG", "onLayoutChild----")
//實時測量
parent.onLayoutChild(child, layoutDirection)
return true
}
override fun onStartNestedScroll(
coordinatorLayout: CoordinatorLayout,
child: TextView,
directTargetChild: View,
target: View,
axes: Int,
type: Int
): Boolean {
//目的為了dispatch成功
return true
}
override fun onNestedPreScroll(
coordinatorLayout: CoordinatorLayout,
child: TextView,
target: View,
dx: Int,
dy: Int,
consumed: IntArray,
type: Int
) {
//邊界處理
var cosumedy = dy
Log.e("TAG","onNestedPreScroll $totalScroll dy $dy")
var scroll = totalScroll + dy
if (abs(scroll) > getMaxScroll(child)) {
cosumedy = getMaxScroll(child) - abs(totalScroll)
} else if (scroll < 0) {
cosumedy = 0
}
//在這裏進行事件消費,我們只需要關心豎向滑動
ViewCompat.offsetTopAndBottom(child, -cosumedy)
//重新賦值
totalScroll += cosumedy
consumed[1] = cosumedy
}
private fun getMaxScroll(child: TextView): Int {
return child.height
}
}
對應的佈局文件,區別在於TextView設置了ScrollBehavior。
xml
<TextView
android:layout_width="match_parent"
android:layout_height="80dp"
android:background="#2196F3"
android:text="這是頂部TextView"
android:gravity="center"
android:textColor="#FFFFFF"
app:layout_behavior=".behavior.ScrollBehavior"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_child"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="80dp"/>
``` 當滾動RecyclerView的時候,因為RecyclerView屬於nestscrollchild,所以事件先被傳遞到了CoordinatorLayout中,然後通過分發調用了TextView中的Behavior中的onNestedPreScroll,在這個方法中,我們是進行了TextView的上下滑動(邊界處理我這邊就不説了,其實還蠻簡單的),看下效果。
我們發現有個問題,就是在TextView上滑離開的之後,RecyclerView上方有一處空白,這個就是因為在TextView滑動的時候,RecyclerView沒有跟隨TextView一起滑動。
這個不就是我們在2.1中提到的這個效果嗎,所以RecyclerView是需要依賴TextView的,我們需要再次自定義一個Behavior,完成這種聯動效果。
```kotlin
class RecyclerViewBehavior @JvmOverloads constructor(
val context: Context,
val attributeSet: AttributeSet
) : CoordinatorLayout.Behavior
override fun layoutDependsOn(
parent: CoordinatorLayout,
child: RecyclerView,
dependency: View
): Boolean {
return dependency is TextView
}
override fun onDependentViewChanged(
parent: CoordinatorLayout,
child: RecyclerView,
dependency: View
): Boolean {
Log.e("TAG","onDependentViewChanged ${dependency.bottom} ${child.top}")
ViewCompat.offsetTopAndBottom(child,(dependency.bottom - child.top))
return true
}
}
對應的佈局文件,區別在於RecyclerView設置了RecyclerViewBehavior。
xml
<TextView
android:layout_width="match_parent"
android:layout_height="80dp"
android:background="#2196F3"
android:text="這是頂部TextView"
android:gravity="center"
android:textColor="#FFFFFF"
app:layout_behavior=".behavior.ScrollBehavior"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_child"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="80dp"
app:layout_behavior=".behavior.RecyclerViewBehavior"/>
``` 這裏我設置了RecyclerView依賴於TextView,當TextView的位置發生變化的時候,就會通知RecyclerView的Behavior中的onDependentViewChanged方法,在這個方法中可以設置RecyclerView豎直方向上的偏移量。
具體的偏移量計算,可以根據上圖自行推理,因為TextView移動的時候,會跟RecyclerView產生一塊位移,RecyclerView需要補上這塊,在onDependentViewChanged方法中。
這時候我們會發現,即便最外層沒有使用可滑動的佈局,依然能夠完成吸頂的效果,這就顯示了CoordinatorLayout的強大之處,當然除了移動之外,控制View的顯示與隱藏、動畫效果等等都可以完成,只要熟悉了CoordinatorLayout內部的原理,就不怕UI跟設計老師的任意需求了。