Android進(jìn)階CoordinatorLayout協(xié)調(diào)者布局實(shí)現(xiàn)吸頂效果
引言
在上一節(jié)Android進(jìn)階寶典 -- NestedScroll嵌套滑動(dòng)機(jī)制實(shí)現(xiàn)吸頂效果 中,我們通過(guò)自定義View的形式完成了TabBar的吸頂效果,其實(shí)除了這種方式之外,MD控件中提供了一個(gè)CoordinatorLayout,協(xié)調(diào)者布局,這種布局同樣可以實(shí)現(xiàn)吸頂效果,但是很多伙伴們對(duì)于CoordinatorLayout有點(diǎn)兒陌生,或者認(rèn)為它用起來(lái)比較麻煩,其實(shí)大多數(shù)原因是因?yàn)閷?duì)于它的原理不太熟悉,不知道什么時(shí)候該用什么樣的組件或者behavior,所以首先了解它的原理,就能夠?qū)oordinatorLayout駕輕就熟。
1 CoordinatorLayout功能介紹
首先我們先從源碼中能夠看到,CoordinatorLayout只實(shí)現(xiàn)了parent接口(這里如果不清楚parent接口是干什么的,建議看看前面的文章,不然根本不清楚我講的是什么),說(shuō)明CoordinatorLayout只能作為父容器來(lái)使用。
public class CoordinatorLayout extends ViewGroup implements NestedScrollingParent2,
NestedScrollingParent3
所以對(duì)于CoordinatorLayout來(lái)說(shuō),它的主要作用就是用來(lái)管理子View或者子View之間的聯(lián)動(dòng)交互。所以在上一篇文章中,我們介紹的NestScroll嵌套滑動(dòng)機(jī)制,它其實(shí)能夠?qū)崿F(xiàn)child與parent的嵌套滑動(dòng),但是是1對(duì)1的;而CoordinatorLayout是能夠管理子View之間的交互,屬于1對(duì)多的。
那么CoordinatorLayout能夠?qū)崿F(xiàn)哪些功能呢?
(1)子控件之間的交互依賴(lài);
(2)子控件之間的嵌套滑動(dòng);
(3)子控件寬高的測(cè)量;
(4)子控件事件攔截與響應(yīng);
那么以上所有的功能實(shí)現(xiàn),全部都是依賴(lài)于CoordinatorLayout中提供的一個(gè)Behavior插件。CoordinatorLayout將所有的事件交互都扔給了Behavior,目的就是為了解耦;這樣就不需要在父容器中做太多的業(yè)務(wù)邏輯,而是通過(guò)不同的Behavior控制子View產(chǎn)生不同的行為。
1.1 CoordinatorLayout的依賴(lài)交互原理
首先我們先看第一個(gè)功能,處理子控件之間的依賴(lài)交互,這種處理方式其實(shí)在很多地方我們都能看到,例如一些小的懸浮窗,你可以拖動(dòng)它到任何地方,點(diǎn)擊讓其消失的時(shí)候,跟隨這個(gè)View的其他View也會(huì)一并消失。
那么如何使用CoordinatorLayout來(lái)實(shí)現(xiàn)這個(gè)功能呢?首先我們先看一下CoordinatorLayout處理這種事件的原理。

看一下上面的圖,在協(xié)調(diào)者布局中,有3個(gè)子View:dependcy、child1、child2;當(dāng)dependcy的發(fā)生位移或者消失的時(shí)候,那么CoordinatorLayout會(huì)通知所有與dependcy依賴(lài)的控件,并且調(diào)用他們內(nèi)部聲明的Behavior,告知其依賴(lài)的dependcy發(fā)生變化了。
那么如何判斷依賴(lài)哪個(gè)控件,CoordinatorLayout-Behavior提供一個(gè)方法:layoutDependsOn,接收到的通知是什么樣的呢?onDependentViewChanged / onDependentViewRemoved 分別代表依賴(lài)的View位置發(fā)生了變化和依賴(lài)的View被移除,這些都會(huì)交給Behavior來(lái)處理。
1.2 CoordinatorLayout的嵌套滑動(dòng)原理
這部分其實(shí)還是挺簡(jiǎn)單的,如果有上一篇文章的基礎(chǔ),那么對(duì)于嵌套滑動(dòng)就非常熟悉了

因?yàn)槲覀兦懊嬲f(shuō)過(guò), CoordinatorLayout只能作為父容器,因?yàn)橹粚?shí)現(xiàn)了parent接口,所以在CoordinatorLayout內(nèi)部需要有一個(gè)child,那么當(dāng)child滑動(dòng)時(shí),首先會(huì)把實(shí)現(xiàn)傳遞給父容器,也就是CoordinatorLayout,再由CoordinatorLayout分發(fā)給每個(gè)child的Behavior,由Behavior來(lái)完成子控件的嵌套滑動(dòng)。

這里有個(gè)問(wèn)題,每個(gè)child都一定是CoordinatorLayout的直接子View嗎?
剩下的兩個(gè)功能就比較簡(jiǎn)單了,同樣也是在Behavior中進(jìn)行處理,就不做介紹了。
2 CoordinatorLayout源碼分析
首先這里先跟大家說(shuō)一下,在看源碼的時(shí)候,我們最好依托于一個(gè)實(shí)例的實(shí)現(xiàn),從而帶著問(wèn)題去源碼中尋找答案,例如我們?cè)诘谝还?jié)中提到過(guò)的CoordinatorLayout的四大功能,可能都會(huì)有這些問(wèn)題:
(1)e.g. 控件之間的交互依賴(lài),為什么在一個(gè)child下設(shè)置一個(gè)Behavior,就能夠跟隨DependentView的位置變化一起變化,他們是如何做依賴(lài)通信的?
(2)我們?cè)赬ML中設(shè)置Behavior,是在什么時(shí)候?qū)嵗模?/p>
(3)我們既然使用了CoordinatorLayout布局,那么內(nèi)部是如何區(qū)分誰(shuí)依賴(lài)誰(shuí)呢?依賴(lài)關(guān)系是如何確定的?
(4)什么時(shí)候需要重新 onMeasureChild?什么時(shí)候需要重新onLayoutChild?
(5)每個(gè)設(shè)置Behavior的子View,一定要是CoordinatorLayout的直接子View嗎?
那么帶著這些問(wèn)題,我們通過(guò)源碼來(lái)得到答案。
2.1 CoordinatorLayout的依賴(lài)交互實(shí)現(xiàn)
如果要實(shí)現(xiàn)依賴(lài)交互效果,首先需要兩個(gè)角色,分別是:DependentView和子View
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)
}
}
這里寫(xiě)了一個(gè)很簡(jiǎn)單的View,能夠跟隨手指滑動(dòng)并一起移動(dòng),然后我們?cè)诋?dāng)前View下加一個(gè)TextView,并讓這個(gè)TextView跟著DependentView一起滑動(dòng)。
class DependBehavior @JvmOverloads constructor(context: Context, attributeSet: AttributeSet) :
CoordinatorLayout.Behavior<View>(context, attributeSet) {
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
}
}
如果想要達(dá)到隨手的效果,那么就需要給TextView設(shè)置一個(gè)Behavior,上面我們定義了一個(gè)Behavior,它的主要作用就是,當(dāng)DependentView滑動(dòng)的時(shí)候,通過(guò)CoordinatorLayout來(lái)通知所有的DependBehavior修飾的View。
在DependBehavior中,我們看主要有兩個(gè)方法:layoutDependsOn和onDependentViewChanged,這兩個(gè)方法之前在原理中提到過(guò),layoutDependsOn主要是用來(lái)決定依賴(lài)關(guān)系,看child依賴(lài)的是不是DependentView;如果依賴(lài)的是DependentView,那么在DependentView滑動(dòng)的時(shí)候,就會(huì)通過(guò)回調(diào)onDependentViewChanged,告知子View當(dāng)前dependency的位置信息,從而完成聯(lián)動(dòng)。
2.2 CoordinatorLayout交互依賴(lài)的源碼分析
那么接下來(lái),我們看下CoordinatorLayout是如何實(shí)現(xiàn)這個(gè)效果的。
在看CoordinatorLayout源碼之前,我們首先需要知道View的生命周期,我們知道在onCreate的時(shí)候通過(guò)setContentView設(shè)置布局文件,如下所示:
<androidx.coordinatorlayout.widget.CoordinatorLayout 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.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"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
如果我們熟悉setContentView的源碼,系統(tǒng)是通過(guò)Inflate的方式解析布局文件,然后在onResume的時(shí)候顯示布局,然后隨之會(huì)調(diào)用onAttachedToWindow將布局顯示在Window上,我們看下onAttachedToWindow這個(gè)方法。
@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;
}
在這個(gè)方法中,設(shè)置了addOnPreDrawListener監(jiān)聽(tīng),此監(jiān)聽(tīng)在頁(yè)面發(fā)生變化(滑動(dòng)、旋轉(zhuǎn)、重新獲取焦點(diǎn))會(huì)產(chǎn)生回調(diào);
class OnPreDrawListener implements ViewTreeObserver.OnPreDrawListener {
@Override
public boolean onPreDraw() {
onChildViewsChanged(EVENT_PRE_DRAW);
return true;
}
}
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這個(gè)方法中,我們看到有兩個(gè)for循環(huán),從mDependencySortedChildren中取出元素,首先我們先不需要關(guān)心mDependencySortedChildren這個(gè)數(shù)組,這個(gè)雙循環(huán)的目的就是用來(lái)判斷View之間是否存在綁定關(guān)系。
首先我們看下第二個(gè)循環(huán),當(dāng)拿到LayoutParams中的Behavior之后,就會(huì)調(diào)用Behavior的layoutDependsOn方法,假設(shè)此時(shí)child為DependentView,checkChild為T(mén)extView;
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,那么此時(shí)可以進(jìn)入到代碼塊中,這里會(huì)判斷type類(lèi)型:EVENT_VIEW_REMOVED和其他type,因?yàn)榇藭r(shí)的type不是REMOVE,所以就會(huì)調(diào)用BeHavior的onDependentViewChanged方法。
因?yàn)?strong>在onAttachedToWindow中,對(duì)View樹(shù)中所有的元素都設(shè)置了OnPreDrawListener的監(jiān)聽(tīng),所以只要某個(gè)View發(fā)生了變化,都會(huì)走到onChildViewsChanged方法中,進(jìn)行相應(yīng)的Behavior檢查并實(shí)現(xiàn)聯(lián)動(dòng)。
所以第2節(jié)開(kāi)頭的第一個(gè)問(wèn)題,當(dāng)DependentView發(fā)生位置變化時(shí),是如何通信到child中的,這里就是通過(guò)設(shè)置了onPreDrawListener來(lái)監(jiān)聽(tīng)。
第二個(gè)問(wèn)題,Behavior是如何被初始化的?如果自定義過(guò)XML屬性,那么大概就能了解,一般都是在布局初始化的時(shí)候,拿到layout_behavior屬性初始化,我們看下源碼。
if (mBehaviorResolved) {
mBehavior = parseBehavior(context, attrs, a.getString(
R.styleable.CoordinatorLayout_Layout_layout_behavior));
}
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);
}
}
通過(guò)源碼我們可以看到,拿到全類(lèi)名之后,通過(guò)反射的方式來(lái)創(chuàng)建Behavior,這里需要注意一點(diǎn),在自定義Behavior的時(shí)候,需要兩個(gè)構(gòu)造參數(shù)CONSTRUCTOR_PARAMS,否則在創(chuàng)建Behavior的時(shí)候會(huì)報(bào)錯(cuò),因?yàn)樵诜瓷鋭?chuàng)建Behavior的時(shí)候需要獲取這兩個(gè)構(gòu)造參數(shù)。
static final Class<?>[] CONSTRUCTOR_PARAMS = new Class<?>[] {
Context.class,
AttributeSet.class
};
報(bào)錯(cuò)類(lèi)型就是:
Could not inflate Behavior subclass com.lay.learn.asm.behavior.DependBehavior
2.3 CoordinatorLayout子控件攔截事件源碼分析
其實(shí)只要了解了其中一個(gè)功能的原理之后,其他功能都是類(lèi)似的。對(duì)于CoordinatorLayout中的子View攔截事件,我們可以先看看CoordinatorLayout中的onInterceptTouchEvent方法。
@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;
}
其中有一個(gè)核心方法performIntercept方法,這個(gè)方法中我們可以看到,同樣也是拿到了Behavior的onInterceptTouchEvent方法,來(lái)優(yōu)先判斷子View是否需要攔截這個(gè)事件,如果不攔截,那么交給父容器消費(fèi),當(dāng)前一般Behavior中也不會(huì)攔截。
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嵌套滑動(dòng)原理分析
對(duì)于嵌套滑動(dòng),其實(shí)在上一篇文章中已經(jīng)介紹的很清楚了,加上CoordinatorLayout自身的特性,我們知道當(dāng)子View(指的是實(shí)現(xiàn)了nestscrollchild接口的View)嵌套滑動(dòng)的時(shí)候,那么首先會(huì)將事件向上分發(fā)到CoordinatorLayout中,所以在parent中的onNestedPreScroll的方法中會(huì)拿到回調(diào)。
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);
}
}
我們?cè)敿?xì)看下這個(gè)方法,對(duì)于parent的onNestedPreScroll方法,當(dāng)然也是會(huì)獲取到Behavior,這里也是拿到了子View的Behavior之后,調(diào)用其onNestedPreScroll方法,會(huì)把手指滑動(dòng)的距離傳遞到子View的Behavior中。
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<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"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
所以這里我們先定義一個(gè)Behavior,這個(gè)Behavior是用來(lái)接收滑動(dòng)事件分發(fā)的。當(dāng)手指向上滑動(dòng)的時(shí)候,首先將TextView隱藏,然后才能滑動(dòng)RecyclerView。
class ScrollBehavior @JvmOverloads constructor(
val mContext: Context,
val attributeSet: AttributeSet
) : CoordinatorLayout.Behavior<TextView>(mContext, attributeSet) {
//相對(duì)于y軸滑動(dòng)的距離
private var mScrollY = 0
//總共滑動(dòng)的距離
private var totalScroll = 0
override fun onLayoutChild(
parent: CoordinatorLayout,
child: TextView,
layoutDirection: Int
): Boolean {
Log.e("TAG", "onLayoutChild----")
//實(shí)時(shí)測(cè)量
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
}
//在這里進(jìn)行事件消費(fèi),我們只需要關(guān)心豎向滑動(dòng)
ViewCompat.offsetTopAndBottom(child, -cosumedy)
//重新賦值
totalScroll += cosumedy
consumed[1] = cosumedy
}
private fun getMaxScroll(child: TextView): Int {
return child.height
}
}
對(duì)應(yīng)的布局文件,區(qū)別在于TextView設(shè)置了ScrollBehavior。
<androidx.coordinatorlayout.widget.CoordinatorLayout 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"
android:orientation="vertical">
<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"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
當(dāng)滾動(dòng)RecyclerView的時(shí)候,因?yàn)镽ecyclerView屬于nestscrollchild,所以事件先被傳遞到了CoordinatorLayout中,然后通過(guò)分發(fā)調(diào)用了TextView中的Behavior中的onNestedPreScroll,在這個(gè)方法中,我們是進(jìn)行了TextView的上下滑動(dòng)(邊界處理我這邊就不說(shuō)了,其實(shí)還蠻簡(jiǎn)單的),看下效果。

我們發(fā)現(xiàn)有個(gè)問(wèn)題,就是在TextView上滑離開(kāi)的之后,RecyclerView上方有一處空白,這個(gè)就是因?yàn)樵赥extView滑動(dòng)的時(shí)候,RecyclerView沒(méi)有跟隨TextView一起滑動(dòng)。
這個(gè)不就是我們?cè)?.1中提到的這個(gè)效果嗎,所以RecyclerView是需要依賴(lài)TextView的,我們需要再次自定義一個(gè)Behavior,完成這種聯(lián)動(dòng)效果。
class RecyclerViewBehavior @JvmOverloads constructor(
val context: Context,
val attributeSet: AttributeSet
) : CoordinatorLayout.Behavior<RecyclerView>(context, attributeSet) {
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
}
}
對(duì)應(yīng)的布局文件,區(qū)別在于RecyclerView設(shè)置了RecyclerViewBehavior。
<androidx.coordinatorlayout.widget.CoordinatorLayout 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"
android:orientation="vertical">
<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"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
這里我設(shè)置了RecyclerView依賴(lài)于TextView,當(dāng)TextView的位置發(fā)生變化的時(shí)候,就會(huì)通知RecyclerView的Behavior中的onDependentViewChanged方法,在這個(gè)方法中可以設(shè)置RecyclerView豎直方向上的偏移量。

具體的偏移量計(jì)算,可以根據(jù)上圖自行推理,因?yàn)門(mén)extView移動(dòng)的時(shí)候,會(huì)跟RecyclerView產(chǎn)生一塊位移,RecyclerView需要補(bǔ)上這塊,在onDependentViewChanged方法中。

這時(shí)候我們會(huì)發(fā)現(xiàn),即便最外層沒(méi)有使用可滑動(dòng)的布局,依然能夠完成吸頂?shù)男Ч@就顯示了CoordinatorLayout的強(qiáng)大之處,當(dāng)然除了移動(dòng)之外,控制View的顯示與隱藏、動(dòng)畫(huà)效果等等都可以完成,只要熟悉了CoordinatorLayout內(nèi)部的原理,就不怕UI跟設(shè)計(jì)老師的任意需求了。
以上就是Android進(jìn)階CoordinatorLayout協(xié)調(diào)者布局實(shí)現(xiàn)吸頂效果的詳細(xì)內(nèi)容,更多關(guān)于Android CoordinatorLayout吸頂?shù)馁Y料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android依據(jù)名字通過(guò)反射獲取在drawable中的圖片
依據(jù)圖片的名字,通過(guò)反射獲取其在drawable中的ID,在根據(jù)此ID顯示圖片,具體實(shí)現(xiàn)如下,感興趣的朋友可以參考下哈2013-06-06
Android 代碼一鍵實(shí)現(xiàn)銀行卡綁定功能
這篇文章主要介紹了Android 代碼一鍵實(shí)現(xiàn)銀行卡綁定功能,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-04-04
Android開(kāi)發(fā)筆記之Android中數(shù)據(jù)的存儲(chǔ)方式(二)
我們?cè)趯?shí)際開(kāi)發(fā)中,有的時(shí)候需要儲(chǔ)存或者備份比較復(fù)雜的數(shù)據(jù)。這些數(shù)據(jù)的特點(diǎn)是,內(nèi)容多、結(jié)構(gòu)大,比如短信備份等,通過(guò)本文給大家介紹Android開(kāi)發(fā)筆記之Android中數(shù)據(jù)的存儲(chǔ)方式(二),對(duì)android數(shù)據(jù)存儲(chǔ)方式相關(guān)知識(shí)感興趣的朋友一起學(xué)習(xí)吧2016-01-01
Android采取ContentObserver方式自動(dòng)獲取驗(yàn)證碼
這篇文章主要為大家詳細(xì)介紹了Android采取ContentObserver方式自動(dòng)獲取驗(yàn)證碼,感興趣的小伙伴們可以參考一下2016-08-08
Android sqlite設(shè)置主鍵自增長(zhǎng)的方法教程
這篇文章主要給大家介紹了關(guān)于Android sqlite設(shè)置主鍵自增長(zhǎng)的方法教程,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面跟著小編一起來(lái)學(xué)習(xí)學(xué)習(xí)吧。2017-06-06
Android評(píng)論圖片可移動(dòng)順序選擇器(推薦)
這篇文章主要介紹了 Android評(píng)論圖片可移動(dòng)順序選擇器的相關(guān)資料,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2016-12-12
詳解Android .9.png “點(diǎn)九”圖片的使用
這篇文章主要為大家詳細(xì)介紹了Android .9.png “點(diǎn)九”圖片的使用方法,感興趣的小伙伴們可以參考一下2016-09-09
Material Design系列之Behavior實(shí)現(xiàn)支付密碼彈窗和商品屬性選擇效果
這篇文章主要為大家詳細(xì)介紹了Material Design系列之Behavior實(shí)現(xiàn)支付密碼彈窗和商品屬性選擇效果,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-09-09
Android組合控件實(shí)現(xiàn)功能強(qiáng)大的自定義控件
這篇文章主要介紹了Android組合控件實(shí)現(xiàn)功能強(qiáng)大的自定義控件的相關(guān)資料,需要的朋友可以參考下2016-05-05

