Android進(jìn)階CoordinatorLayout協(xié)調(diào)者布局實(shí)現(xiàn)吸頂效果
引言
在上一節(jié)Android進(jìn)階寶典 -- NestedScroll嵌套滑動機(jī)制實(shí)現(xiàn)吸頂效果 中,我們通過自定義View的形式完成了TabBar的吸頂效果,其實(shí)除了這種方式之外,MD控件中提供了一個(gè)CoordinatorLayout,協(xié)調(diào)者布局,這種布局同樣可以實(shí)現(xiàn)吸頂效果,但是很多伙伴們對于CoordinatorLayout有點(diǎn)兒陌生,或者認(rèn)為它用起來比較麻煩,其實(shí)大多數(shù)原因是因?yàn)閷τ谒脑聿惶煜?,不知道什么時(shí)候該用什么樣的組件或者behavior,所以首先了解它的原理,就能夠?qū)oordinatorLayout駕輕就熟。
1 CoordinatorLayout功能介紹
首先我們先從源碼中能夠看到,CoordinatorLayout只實(shí)現(xiàn)了parent接口(這里如果不清楚parent接口是干什么的,建議看看前面的文章,不然根本不清楚我講的是什么),說明CoordinatorLayout只能作為父容器來使用。
public class CoordinatorLayout extends ViewGroup implements NestedScrollingParent2, NestedScrollingParent3
所以對于CoordinatorLayout來說,它的主要作用就是用來管理子View或者子View之間的聯(lián)動交互。所以在上一篇文章中,我們介紹的NestScroll嵌套滑動機(jī)制,它其實(shí)能夠?qū)崿F(xiàn)child與parent的嵌套滑動,但是是1對1的;而CoordinatorLayout是能夠管理子View之間的交互,屬于1對多的。
那么CoordinatorLayout能夠?qū)崿F(xiàn)哪些功能呢?
(1)子控件之間的交互依賴;
(2)子控件之間的嵌套滑動;
(3)子控件寬高的測量;
(4)子控件事件攔截與響應(yīng);
那么以上所有的功能實(shí)現(xiàn),全部都是依賴于CoordinatorLayout中提供的一個(gè)Behavior插件。CoordinatorLayout將所有的事件交互都扔給了Behavior,目的就是為了解耦;這樣就不需要在父容器中做太多的業(yè)務(wù)邏輯,而是通過不同的Behavior控制子View產(chǎn)生不同的行為。
1.1 CoordinatorLayout的依賴交互原理
首先我們先看第一個(gè)功能,處理子控件之間的依賴交互,這種處理方式其實(shí)在很多地方我們都能看到,例如一些小的懸浮窗,你可以拖動它到任何地方,點(diǎn)擊讓其消失的時(shí)候,跟隨這個(gè)View的其他View也會一并消失。
那么如何使用CoordinatorLayout來實(shí)現(xiàn)這個(gè)功能呢?首先我們先看一下CoordinatorLayout處理這種事件的原理。
看一下上面的圖,在協(xié)調(diào)者布局中,有3個(gè)子View:dependcy、child1、child2;當(dāng)dependcy的發(fā)生位移或者消失的時(shí)候,那么CoordinatorLayout會通知所有與dependcy依賴的控件,并且調(diào)用他們內(nèi)部聲明的Behavior,告知其依賴的dependcy發(fā)生變化了。
那么如何判斷依賴哪個(gè)控件,CoordinatorLayout-Behavior提供一個(gè)方法:layoutDependsOn,接收到的通知是什么樣的呢?onDependentViewChanged / onDependentViewRemoved 分別代表依賴的View位置發(fā)生了變化和依賴的View被移除,這些都會交給Behavior來處理。
1.2 CoordinatorLayout的嵌套滑動原理
這部分其實(shí)還是挺簡單的,如果有上一篇文章的基礎(chǔ),那么對于嵌套滑動就非常熟悉了
因?yàn)槲覀兦懊嬲f過, CoordinatorLayout只能作為父容器,因?yàn)橹粚?shí)現(xiàn)了parent接口,所以在CoordinatorLayout內(nèi)部需要有一個(gè)child,那么當(dāng)child滑動時(shí),首先會把實(shí)現(xiàn)傳遞給父容器,也就是CoordinatorLayout,再由CoordinatorLayout分發(fā)給每個(gè)child的Behavior,由Behavior來完成子控件的嵌套滑動。
這里有個(gè)問題,每個(gè)child都一定是CoordinatorLayout的直接子View嗎?
剩下的兩個(gè)功能就比較簡單了,同樣也是在Behavior中進(jìn)行處理,就不做介紹了。
2 CoordinatorLayout源碼分析
首先這里先跟大家說一下,在看源碼的時(shí)候,我們最好依托于一個(gè)實(shí)例的實(shí)現(xiàn),從而帶著問題去源碼中尋找答案,例如我們在第一節(jié)中提到過的CoordinatorLayout的四大功能,可能都會有這些問題:
(1)e.g. 控件之間的交互依賴,為什么在一個(gè)child下設(shè)置一個(gè)Behavior,就能夠跟隨DependentView的位置變化一起變化,他們是如何做依賴通信的?
(2)我們在XML中設(shè)置Behavior,是在什么時(shí)候?qū)嵗模?/p>
(3)我們既然使用了CoordinatorLayout布局,那么內(nèi)部是如何區(qū)分誰依賴誰呢?依賴關(guān)系是如何確定的?
(4)什么時(shí)候需要重新 onMeasureChild?什么時(shí)候需要重新onLayoutChild?
(5)每個(gè)設(shè)置Behavior的子View,一定要是CoordinatorLayout的直接子View嗎?
那么帶著這些問題,我們通過源碼來得到答案。
2.1 CoordinatorLayout的依賴交互實(shí)現(xiàn)
如果要實(shí)現(xiàn)依賴交互效果,首先需要兩個(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) } }
這里寫了一個(gè)很簡單的View,能夠跟隨手指滑動并一起移動,然后我們在當(dāng)前View下加一個(gè)TextView,并讓這個(gè)TextView跟著DependentView一起滑動。
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滑動的時(shí)候,通過CoordinatorLayout來通知所有的DependBehavior修飾的View。
在DependBehavior中,我們看主要有兩個(gè)方法:layoutDependsOn和onDependentViewChanged,這兩個(gè)方法之前在原理中提到過,layoutDependsOn主要是用來決定依賴關(guān)系,看child依賴的是不是DependentView;如果依賴的是DependentView,那么在DependentView滑動的時(shí)候,就會通過回調(diào)onDependentViewChanged,告知子View當(dāng)前dependency的位置信息,從而完成聯(lián)動。
2.2 CoordinatorLayout交互依賴的源碼分析
那么接下來,我們看下CoordinatorLayout是如何實(shí)現(xiàn)這個(gè)效果的。
在看CoordinatorLayout源碼之前,我們首先需要知道View的生命周期,我們知道在onCreate的時(shí)候通過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)是通過Inflate的方式解析布局文件,然后在onResume的時(shí)候顯示布局,然后隨之會調(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)聽,此監(jiān)聽在頁面發(fā)生變化(滑動、旋轉(zhuǎn)、重新獲取焦點(diǎn))會產(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)的目的就是用來判斷View之間是否存在綁定關(guān)系。
首先我們看下第二個(gè)循環(huán),當(dāng)拿到LayoutParams中的Behavior之后,就會調(diào)用Behavior的layoutDependsOn方法,假設(shè)此時(shí)child為DependentView,checkChild為TextView;
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)入到代碼塊中,這里會判斷type類型:EVENT_VIEW_REMOVED和其他type,因?yàn)榇藭r(shí)的type不是REMOVE,所以就會調(diào)用BeHavior的onDependentViewChanged方法。
因?yàn)?strong>在onAttachedToWindow中,對View樹中所有的元素都設(shè)置了OnPreDrawListener的監(jiān)聽,所以只要某個(gè)View發(fā)生了變化,都會走到onChildViewsChanged方法中,進(jìn)行相應(yīng)的Behavior檢查并實(shí)現(xiàn)聯(lián)動。
所以第2節(jié)開頭的第一個(gè)問題,當(dāng)DependentView發(fā)生位置變化時(shí),是如何通信到child中的,這里就是通過設(shè)置了onPreDrawListener來監(jiān)聽。
第二個(gè)問題,Behavior是如何被初始化的?如果自定義過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); } }
通過源碼我們可以看到,拿到全類名之后,通過反射的方式來創(chuàng)建Behavior,這里需要注意一點(diǎn),在自定義Behavior的時(shí)候,需要兩個(gè)構(gòu)造參數(shù)CONSTRUCTOR_PARAMS,否則在創(chuàng)建Behavior的時(shí)候會報(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ò)類型就是:
Could not inflate Behavior subclass com.lay.learn.asm.behavior.DependBehavior
2.3 CoordinatorLayout子控件攔截事件源碼分析
其實(shí)只要了解了其中一個(gè)功能的原理之后,其他功能都是類似的。對于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方法,來優(yōu)先判斷子View是否需要攔截這個(gè)事件,如果不攔截,那么交給父容器消費(fèi),當(dāng)前一般Behavior中也不會攔截。
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嵌套滑動原理分析
對于嵌套滑動,其實(shí)在上一篇文章中已經(jīng)介紹的很清楚了,加上CoordinatorLayout自身的特性,我們知道當(dāng)子View(指的是實(shí)現(xiàn)了nestscrollchild接口的View)嵌套滑動的時(shí)候,那么首先會將事件向上分發(fā)到CoordinatorLayout中,所以在parent中的onNestedPreScroll的方法中會拿到回調(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); } }
我們詳細(xì)看下這個(gè)方法,對于parent的onNestedPreScroll方法,當(dāng)然也是會獲取到Behavior,這里也是拿到了子View的Behavior之后,調(diào)用其onNestedPreScroll方法,會把手指滑動的距離傳遞到子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是用來接收滑動事件分發(fā)的。當(dāng)手指向上滑動的時(shí)候,首先將TextView隱藏,然后才能滑動RecyclerView。
class ScrollBehavior @JvmOverloads constructor( val mContext: Context, val attributeSet: AttributeSet ) : CoordinatorLayout.Behavior<TextView>(mContext, attributeSet) { //相對于y軸滑動的距離 private var mScrollY = 0 //總共滑動的距離 private var totalScroll = 0 override fun onLayoutChild( parent: CoordinatorLayout, child: TextView, layoutDirection: Int ): Boolean { Log.e("TAG", "onLayoutChild----") //實(shí)時(shí)測量 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)心豎向滑動 ViewCompat.offsetTopAndBottom(child, -cosumedy) //重新賦值 totalScroll += cosumedy consumed[1] = cosumedy } private fun getMaxScroll(child: TextView): Int { return child.height } }
對應(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)滾動RecyclerView的時(shí)候,因?yàn)镽ecyclerView屬于nestscrollchild,所以事件先被傳遞到了CoordinatorLayout中,然后通過分發(fā)調(diào)用了TextView中的Behavior中的onNestedPreScroll,在這個(gè)方法中,我們是進(jìn)行了TextView的上下滑動(邊界處理我這邊就不說了,其實(shí)還蠻簡單的),看下效果。
我們發(fā)現(xiàn)有個(gè)問題,就是在TextView上滑離開的之后,RecyclerView上方有一處空白,這個(gè)就是因?yàn)樵赥extView滑動的時(shí)候,RecyclerView沒有跟隨TextView一起滑動。
這個(gè)不就是我們在2.1中提到的這個(gè)效果嗎,所以RecyclerView是需要依賴TextView的,我們需要再次自定義一個(gè)Behavior,完成這種聯(lián)動效果。
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 } }
對應(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依賴于TextView,當(dāng)TextView的位置發(fā)生變化的時(shí)候,就會通知RecyclerView的Behavior中的onDependentViewChanged方法,在這個(gè)方法中可以設(shè)置RecyclerView豎直方向上的偏移量。
具體的偏移量計(jì)算,可以根據(jù)上圖自行推理,因?yàn)門extView移動的時(shí)候,會跟RecyclerView產(chǎn)生一塊位移,RecyclerView需要補(bǔ)上這塊,在onDependentViewChanged方法中。
這時(shí)候我們會發(fā)現(xiàn),即便最外層沒有使用可滑動的布局,依然能夠完成吸頂?shù)男Ч?,這就顯示了CoordinatorLayout的強(qiáng)大之處,當(dāng)然除了移動之外,控制View的顯示與隱藏、動畫效果等等都可以完成,只要熟悉了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料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android依據(jù)名字通過反射獲取在drawable中的圖片
依據(jù)圖片的名字,通過反射獲取其在drawable中的ID,在根據(jù)此ID顯示圖片,具體實(shí)現(xiàn)如下,感興趣的朋友可以參考下哈2013-06-06Android 代碼一鍵實(shí)現(xiàn)銀行卡綁定功能
這篇文章主要介紹了Android 代碼一鍵實(shí)現(xiàn)銀行卡綁定功能,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-04-04Android開發(fā)筆記之Android中數(shù)據(jù)的存儲方式(二)
我們在實(shí)際開發(fā)中,有的時(shí)候需要儲存或者備份比較復(fù)雜的數(shù)據(jù)。這些數(shù)據(jù)的特點(diǎn)是,內(nèi)容多、結(jié)構(gòu)大,比如短信備份等,通過本文給大家介紹Android開發(fā)筆記之Android中數(shù)據(jù)的存儲方式(二),對android數(shù)據(jù)存儲方式相關(guān)知識感興趣的朋友一起學(xué)習(xí)吧2016-01-01Android采取ContentObserver方式自動獲取驗(yàn)證碼
這篇文章主要為大家詳細(xì)介紹了Android采取ContentObserver方式自動獲取驗(yàn)證碼,感興趣的小伙伴們可以參考一下2016-08-08Android sqlite設(shè)置主鍵自增長的方法教程
這篇文章主要給大家介紹了關(guān)于Android sqlite設(shè)置主鍵自增長的方法教程,文中通過示例代碼介紹的非常詳細(xì),對大家具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面跟著小編一起來學(xué)習(xí)學(xué)習(xí)吧。2017-06-06詳解Android .9.png “點(diǎn)九”圖片的使用
這篇文章主要為大家詳細(xì)介紹了Android .9.png “點(diǎn)九”圖片的使用方法,感興趣的小伙伴們可以參考一下2016-09-09Material Design系列之Behavior實(shí)現(xiàn)支付密碼彈窗和商品屬性選擇效果
這篇文章主要為大家詳細(xì)介紹了Material Design系列之Behavior實(shí)現(xiàn)支付密碼彈窗和商品屬性選擇效果,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-09-09Android組合控件實(shí)現(xiàn)功能強(qiáng)大的自定義控件
這篇文章主要介紹了Android組合控件實(shí)現(xiàn)功能強(qiáng)大的自定義控件的相關(guān)資料,需要的朋友可以參考下2016-05-05