Android使用CoordinatorLayout實現聯動效果
在開發過程中,有時需要實現一些比較複雜的聯動效果:比如在滾動列表的時候改變某個View的狀態,隨著滾動程度的變化,View也跟隨變化等等。要想實現這些效果,用普通的方法也可以實現,不過需要設計很多的監聽來控制,邏輯也比較複雜,而通過CoordinatorLayout可以更優雅的實現同樣的效果。
1.1 CoordinatorLayout介紹
CoordinatorLayout 是 Google 在 Design Support 包中提供的一個十分強大的佈局檢視,我們先來看下官網介紹
CoordinatorLayout
public class CoordinatorLayout
extends ViewGroup implements NestedScrollingParent2 , NestedScrollingParent3
java.lang.Object
↳ androidx.coordinatorlayout.widget.CoordinatorLayout
CoordinatorLayout is a super-powered FrameLayout .
CoordinatorLayout is intended for two primary use cases:
- As a top-level application decor or chrome layout
- As a container for a specific interaction with one or more child views
By specifying Behaviors for child views of a CoordinatorLayout you can provide many different interactions within a single parent and those views can also interact with one another. View classes can specify a default behavior when used as a child of a CoordinatorLayout using the CoordinatorLayout.DefaultBehavior annotation.
官網說它本質是一個 FrameLayout
,它可以作為一個容器指定與child 的一些互動規則。通過給 View
設定 Behaviors
,就可以和 child 進行互動,或者是 child 之間互相進行相關的互動,並且自定義 View 時,可以通過 DefaultBehavior
這個註解來指定它關聯的 Behavior。
如此看來,我們只需要定製Behavior就可以定製我們的互動了,再來看下Behavior的內容。
1.2 CoordinatorLayout.Behavior介紹
Behavior是CoordinatorLayout中的一個靜態內部類。
CoordinatorLayout.Behavior
public static abstract class CoordinatorLayout.Behavior extends Object
java.lang.Object
↳androidx.coordinatorlayout.widget.CoordinatorLayout.Behavior<V extends android.view.View >
Interaction behavior plugin for child views of CoordinatorLayout .
A Behavior implements one or more interactions that a user can take on a child view. These interactions may include drags, swipes, flings, or any other gestures.
Behavior是針對CoordinatorLayout中child的互動外掛。Behavior同時也是一個抽象類,它的實現類都是為了能夠讓使用者作用在一個View上進行拖拽、滑動、快速滑動等手勢。
下面我們就來看下Behavior中的關鍵程式碼
//型別一 @Override public boolean layoutDependsOn(CoordinatorLayout parent, V child, View dependency) { return false; } @Override public boolean onDependentViewChanged(CoordinatorLayout parent, V child, View dependency){ return false; } @Override public void onDependentViewRemoved(CoordinatorLayout parent, V child, View dependency) { }
//型別二 @Override public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View directTargetChild, @NonNull View target, int axes, int type) { return false; } @Override public void onNestedScrollAccepted(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View directTargetChild, @NonNull View target, int axes, int type) { super.onNestedScrollAccepted(coordinatorLayout, child, directTargetChild, target, axes, type); } @Override public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) { super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type); } @Override public void onNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) { super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type); } @Override public void onStopNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View target, int type) { super.onStopNestedScroll(coordinatorLayout, child, target, type); } @Override public boolean onNestedFling(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View target, float velocityX, float velocityY, boolean consumed) { return super.onNestedFling(coordinatorLayout, child, target, velocityX, velocityY, consumed); } @Override public boolean onNestedPreFling(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View target, float velocityX, float velocityY) { return super.onNestedPreFling(coordinatorLayout, child, target, velocityX, velocityY); }
從方法的功能側重來看,可以分為兩類,一是根據某些依賴的View的變化來實現效果;二是根據某些元件的滑動事件來實現效果;其中第一類對應前三個API,第二類對應後面的API。我們先看第一類情況。
2.3 Behavior設定View之間依賴
View之間的依賴使用的是第一類API,其具體作用介紹如下:
-
確定一個
View(child)
是否依賴於另一個View(dependency)
,需要在layoutDependsOn()
方法中進行判斷並返回一個布林值,returntrue
表示依賴成立,反之不成立。並且只有在layoutDependsOn()
返回為true時,後面的onDependentViewChanged()
和onDependentViewRemoved()
方法才會被呼叫。 -
當確定依賴的
View(dependency)
發生變化時,onDependentViewChanged()
方法會被呼叫,我們可以在這個方法中拿到變化後的dependency,並對自己的View進行處理。 -
當
View(dependency)
被移除時,onDependentViewRemoved()
方法會被呼叫。
為避免內容不易理解,我們來舉例說明。

首先我們自定義了一個可以跟隨手指滑動變化位置的DragView。程式碼很簡單,如下所示:
public class DragView extends AppCompatTextView { private final int mSlop; private float mLastX; private float mLastY; public DragView(Context context) { this(context,null); } public DragView(Context context, AttributeSet attrs) { this(context, attrs,0); } public DragView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); setClickable(true); mSlop = ViewConfiguration.getTouchSlop(); } @Override public boolean onTouchEvent(MotionEvent event) { int action = event.getAction(); switch (action) { case MotionEvent.ACTION_DOWN: mLastX = event.getRawX(); mLastY = event.getRawY(); break; case MotionEvent.ACTION_MOVE: int deltax = (int) (event.getRawX() - mLastX); int deltay = (int) (event.getRawY() - mLastY); if (Math.abs(deltax) > mSlop || Math.abs(deltay) > mSlop) { ViewCompat.offsetTopAndBottom(this,deltay); ViewCompat.offsetLeftAndRight(this,deltax); mLastX = event.getRawX(); mLastY = event.getRawY(); } break; case MotionEvent.ACTION_UP: mLastX = event.getRawX(); mLastY = event.getRawY(); break; default: break; } return true; } }
同時,在佈局檔案中引入,作為CoordinatorLayout中的一個child,預設初始位置是CoordinatorLayout的中心位置,佈局如下所示:
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="com.hikvision.update.demo.behaivior.BehaviorTestActivity"> <android.support.design.widget.AppBarLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:theme="@style/AppTheme.AppBarOverlay"> <android.support.v7.widget.Toolbar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" android:background="?attr/colorPrimary" app:popupTheme="@style/AppTheme.PopupOverlay" /> </android.support.design.widget.AppBarLayout> <com.update.demo.behaivior.DragView android:layout_width="wrap_content" android:layout_height="wrap_content" android:padding="@dimen/isms_size_10dp" android:layout_gravity="center" android:text="DragView" android:background="@color/colorPrimary" android:textColor="#fff" android:textSize="16sp"/> </android.support.design.widget.CoordinatorLayout>

接下來,我們來自定義一個DependencyBehavior,讓使用這個Behavior的View位於DragView的上方:
public class DependencyBehavior extends CoordinatorLayout.Behavior<View> { public DependencyBehavior() { super(); } public DependencyBehavior(Context context, AttributeSet attrs) { super(context, attrs); } @Override public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) { //判斷依賴是否為DragView return dependency instanceof DragView; } @Override public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) { //獲取DragView的頂部,讓child位於DragView的左上方 int top = dependency.getTop(); int childHeight = child.getHeight(); child.setY(top - childHeight); child.setX(dependency.getLeft()); return true; } }
在CoordinatorLayout佈局中新增一個ImageView,並使用這個Behavior:
<ImageView android:id="@+id/image" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="?attr/actionBarSize" android:src="@mipmap/ic_launcher_round" app:layout_behavior="com.demo.behaivior.DependencyBehavior" />
實現效果如下:

到此,View之間的依賴如何使用已經演示明白。我們接著來看對於滑動事件的響應。
2.4 Behavior對滑動事件的響應
首先,我們來看下 onStartNestedScroll()
方法:
/** * Called when a descendant of the CoordinatorLayout attempts to initiate a nested scroll. * * <p>Any Behavior associated with any direct child of the CoordinatorLayout may respond * to this event and return true to indicate that the CoordinatorLayout should act as * a nested scrolling parent for this scroll. Only Behaviors that return true from * this method will receive subsequent nested scroll events.</p> * * @param coordinatorLayout the CoordinatorLayout parent of the view this Behavior is *associated with * @param child the child view of the CoordinatorLayout this Behavior is associated with * @param directTargetChild the child view of the CoordinatorLayout that either is or *contains the target of the nested scroll operation * @param target the descendant view of the CoordinatorLayout initiating the nested scroll * @param axes the axes that this nested scroll applies to. See *{@link ViewCompat#SCROLL_AXIS_HORIZONTAL}, *{@link ViewCompat#SCROLL_AXIS_VERTICAL} * @param type the type of input which cause this scroll event * @return true if the Behavior wishes to accept this nested scroll * * @see NestedScrollingParent2#onStartNestedScroll(View, View, int, int) */ public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull V child, @NonNull View directTargetChild, @NonNull View target, @ScrollAxis int axes, @NestedScrollType int type) { if (type == ViewCompat.TYPE_TOUCH) { return onStartNestedScroll(coordinatorLayout, child, directTargetChild, target, axes); } return false; }
註釋中說,當一個CoordinatorLayout中的 子View
企圖觸發一個 Nested scroll
事件時,這個方法會被呼叫。並且只有在 onStartNestedScroll()
方法返回為 true
時,後續的 Nested Scroll
事件才會響應。
後續的回撥是這幾個:
@Override public void onNestedScrollAccepted(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View directTargetChild, @NonNull View target, int axes, int type) { super.onNestedScrollAccepted(coordinatorLayout, child, directTargetChild, target, axes, type); } @Override public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) { super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type); } @Override public void onNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) { super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type); } @Override public void onStopNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View target, int type) { super.onStopNestedScroll(coordinatorLayout, child, target, type); } @Override public boolean onNestedFling(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View target, float velocityX, float velocityY, boolean consumed) { return super.onNestedFling(coordinatorLayout, child, target, velocityX, velocityY, consumed); } @Override public boolean onNestedPreFling(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View target, float velocityX, float velocityY) { return super.onNestedPreFling(coordinatorLayout, child, target, velocityX, velocityY); }
那麼 Nested Scroll
又是什麼呢?哪些控制元件可以觸發 Nested Scroll
呢?
通過追蹤呼叫 onStartNestedScroll()
方法的原始碼,最終可以得到結論: 如果在5.0的系統版本以上,我們需要對 View.setNestedScrollingEnable(true)
,如果在這個版本之下,得保證這個View本身是 NestedScrollingChild
的實現類,只有這樣,才可以觸發 Nested Scroll
。
藉助於AndroidStudio,我們可以知道NestedScrollingChild的實現類有: RecyclerView
、 NavigationMenuView
、 SwipeRefreshLayout
、 NestedScrollView

接下來,我們用 NestedScrollView
舉例,來實現一個對 Nested Scroll
響應的簡單 Behavior
,佈局如下所示:
<?xml version="1.0" encoding="utf-8"?> <android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="com.demo.behaivior.BehaviorTestActivity"> <android.support.design.widget.AppBarLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:theme="@style/AppTheme.AppBarOverlay"> <android.support.v7.widget.Toolbar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" android:background="?attr/colorPrimary" app:popupTheme="@style/AppTheme.PopupOverlay" /> </android.support.design.widget.AppBarLayout> <android.support.v4.widget.NestedScrollView android:layout_width="match_parent" android:layout_height="match_parent" android:layout_marginTop="?attr/actionBarSize"> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/a_lot_of_text" android:textSize="@dimen/isms_text_size_16sp"/> </android.support.v4.widget.NestedScrollView> <ImageView android:id="@+id/image" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="?attr/actionBarSize" android:src="@mipmap/ic_launcher_round" app:layout_behavior="com.demo.behaivior.DependencyBehavior" /> </android.support.design.widget.CoordinatorLayout>
我們新增了一個 NestedScrollView
,同時我們希望在 NestedScrollView
滑動的時候, ImageView
可以跟隨著一起滑動。現在我們來改造下之前的 DependencyBehavior
。
首先去除View的依賴關係:
@Override public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) { //判斷依賴是否為DragView //return dependency instanceof DragView; return false; }
然後在onStartNestedScroll()方法中作如下修改,以保證對豎直方向滑動的接收:
@Override public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View directTargetChild, @NonNull View target, int axes, int type) { //child為ImageView 並且滑動方向為豎直方向才響應 return child instanceof ImageView && ViewCompat.SCROLL_AXIS_VERTICAL == axes; }
我們繼續重寫 OnNestedPreScroll()
方法,這個方法會在 NestedScrollView
準備滑動的時候被呼叫,用以通知 Behavior,NestedScrollView
準備滑動多少距離, dx
和 dy
分別是橫向和豎向的滑動位移, int[ ] consumed
用以記錄 Behavior
消耗的 dx
和 dy
;
@Override public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) { super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type); Log.d("DependencyBehavior", "onNestedPreScrolldx:" + dx + " dy:" + dy); ViewCompat.offsetTopAndBottom(child, dy); }
在接收到 dy
滑動距離後,直接移動 childView
。這樣就可以實現我們預計的效果了。
//TODO:動圖一張待傳
如果我們想讓 child
消費掉所有的 dy
偏移量,只需要再加上一行程式碼 :
@Override public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) { super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type); Log.d("DependencyBehavior", "onNestedPreScrolldx:" + dx + " dy:" + dy); //加上這句,child消費掉所有dy consumed[1] = dy; ViewCompat.offsetTopAndBottom(child, dy); }
此時的效果就是:不論 NestedScrollView
如何滑動,僅能看到 ImageView
跟隨手勢動作。
上面舉例說明了下 Behavior
響應 NestedScroll
的簡單方式,如果你還是一頭霧水,搞不清楚用法,不用擔心,下面我們就來具體說明下這幾個方法的呼叫流程和具體功能:
首先我們來看一張流程圖

圖中 Child
對應我們上面例子中的 NestedScrollView
, Parent
是 CoordinatorLayout
,而 CoordinatorLayout
會將接收到的 NestedScroll
向各個 child
中的 Behavior
進行分發,我們可以簡單理解為此處的 Parent
就是 Behavior
。
(PS:流程圖來自這篇文章,有興趣的也可以看看)
Child
中的 DOWN
、 MOVE
、 UP
均為 child
在 OnTouchEvent()
中接收到的手勢事件;
我們可以看到:
-
在
child
在接收到DOWN
手勢時,發起巢狀滾動請求,請求中攜帶有巢狀滑動的方向(方向為child在初始化時已經被宣告過的); -
Parent
接收到巢狀滾動請求,如果滾動方向是自己需要的則同意巢狀滾動,這時一般主動放棄攔截MOVE
事件,Parent
在這個過程中呼叫了自身的onStartNestedScroll()
和onNestedScrollAccepted()
; -
Child
在接收到MOVE
手勢時,在自身準備滾動前,去詢問Parent
是否需要滾動(dispatchNestedPreScroll
),引數中聲明瞭本次滾動的橫向和豎向距離dx
,dy
,並要求告知Parent
消費掉的距離和視窗偏移大小 -
Parent
在onNestedPreScroll()
方法中接收到滾動準備請求,如果需要可以執行滑動操作,並根據需求,將消耗的距離儲存到int[ ] consumed
中,consumed[0]
儲存dx
消耗,consumed[1]
儲存dy
消耗; -
Child
在接收到Parent
的反饋後,執行自身的滾動,這個滾動是將計劃滾動距離減去consumed
陣列中消耗的剩餘距離,在滾動之後分發剩餘的未消費的滾動距離 (dispatchNestedScroll
),引數中宣告自己已消費的x
、y
距離和未消費的x
、y
距離,並要求告知視窗偏移 -
Parent
在onNestedScroll()
方法中接收到滾動請求,此時可以根據需求,通過滑動消費掉child
提供的未消費距離; -
Child
在接收到UP
手勢時,如果判斷當前滾動仍需要繼續,那麼會在自身滾動前詢問Parent
是否需要繼續滾動,引數中會宣告x
、y
的速度; -
Parent
在onNestedPreFling()
中接收到預遺留滾動請求,根據自身需要選擇執行邏輯; -
Child
在自身執行完遺留滾動後,詢問Parent
是否需要執行,引數中宣告x
、y
的速度已經是否已消費;
10. Parent
在 onNestedFling()
接收到 child
詢問後,可以選擇執行未消費的遺留滾動;
-
Child
滾動執行結束,通知Parent
; -
Parent
在onStopNestedScroll()
接收到結束滾動的通知,停止滾動操作,此時可根據Parent
的當前狀態,作一些邏輯處理
以上,就是 Nested Scroll
的完整的處理流程。
瞭解了上面對 Behavior
的介紹,我們可以明白一個 Behavior
的運作機制。下面我們將對Android官方提供的 BottomSheetBehavior
進行分析,以加深理解。
2.5 BottomSheetBehavior原始碼分析
BottomSheetBehavior直接繼承自CoordinatorLayout.Behavior<View>
/** * An interaction behavior plugin for a child view of {@link CoordinatorLayout} to make it work as * a bottom sheet. */ public class BottomSheetBehavior<V extends View> extends CoordinatorLayout.Behavior<V>{ ... }
先看下構造方法
/** * Default constructor for inflating BottomSheetBehaviors from layout. * * @param context The {@link Context}. * @param attrsThe {@link AttributeSet}. */ public BottomSheetBehavior(Context context, AttributeSet attrs) { super(context, attrs); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.BottomSheetBehavior_Layout); TypedValue value = a.peekValue(R.styleable.BottomSheetBehavior_Layout_behavior_peekHeight); if (value != null && value.data == PEEK_HEIGHT_AUTO) { setPeekHeight(value.data); } else { setPeekHeight(a.getDimensionPixelSize( R.styleable.BottomSheetBehavior_Layout_behavior_peekHeight, PEEK_HEIGHT_AUTO)); } setHideable(a.getBoolean(R.styleable.BottomSheetBehavior_Layout_behavior_hideable, false)); setSkipCollapsed(a.getBoolean(R.styleable.BottomSheetBehavior_Layout_behavior_skipCollapsed, false)); a.recycle(); ViewConfiguration configuration = ViewConfiguration.get(context); mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); }
在構造方法中獲取了設定的彈出高度,是否支援手勢下拉隱藏功能以及彈出時是否支援動畫的屬性。
繼續看onLayoutChild****的原始碼(我們稱使用了BottomSheetBehavior的View為BottomView)
@Override public boolean onLayoutChild(CoordinatorLayout parent, V child, int layoutDirection) { if (ViewCompat.getFitsSystemWindows(parent) && !ViewCompat.getFitsSystemWindows(child)) { ViewCompat.setFitsSystemWindows(child, true);//1 } int savedTop = child.getTop(); // First let the parent lay it out parent.onLayoutChild(child, layoutDirection);//2 // Offset the bottom sheet mParentHeight = parent.getHeight(); int peekHeight; if (mPeekHeightAuto) { if (mPeekHeightMin == 0) { mPeekHeightMin = parent.getResources().getDimensionPixelSize( R.dimen.design_bottom_sheet_peek_height_min); } peekHeight = Math.max(mPeekHeightMin, mParentHeight - parent.getWidth() * 9 / 16);//2 } else { peekHeight = mPeekHeight; } mMinOffset = Math.max(0, mParentHeight - child.getHeight());//3 mMaxOffset = Math.max(mParentHeight - peekHeight, mMinOffset);//3 if (mState == STATE_EXPANDED) { ViewCompat.offsetTopAndBottom(child, mMinOffset); } else if (mHideable && mState == STATE_HIDDEN) { ViewCompat.offsetTopAndBottom(child, mParentHeight); } else if (mState == STATE_COLLAPSED) { ViewCompat.offsetTopAndBottom(child, mMaxOffset); } else if (mState == STATE_DRAGGING || mState == STATE_SETTLING) { ViewCompat.offsetTopAndBottom(child, savedTop - child.getTop()); } if (mViewDragHelper == null) { mViewDragHelper = ViewDragHelper.create(parent, mDragCallback);//4 } mViewRef = new WeakReference<>(child); mNestedScrollingChildRef = new WeakReference<>(findScrollingChild(child));//5 return true; }
這個方法中,主要做了幾件事:
-
首先設定BottomView適配螢幕;
-
對BottomView進行擺放:先呼叫父類對BottomView進行佈局,根據PeekHeight和State對BottomView位置進行偏移,如果PeekHeight沒有設定,一般預設為螢幕高度的9/16的位置;
-
對mMinOffset,mMaxOffset進行計算,用來確定BottomView的偏移範圍。即距離CoordinatorLayout原點Y軸 mMinOffset到mMaxOffset之間;
-
初始化ViewDragHelper類,用以處理拖拽和滑動事件;
-
儲存BottomView的軟引用並遞迴尋找到BottomView中的第一個NestedScrollingChild元件;
說明一下:由於Android中螢幕的座標軸是向下為y軸正方向,因此在計算PeekHeight時,會讓ParentHeight-mPeekHeight,此時顯示的高度才是設定的高度。
對於事件攔截的處理
@Override public boolean onInterceptTouchEvent(CoordinatorLayout parent, V child, MotionEvent event) { if (!child.isShown()) { mIgnoreEvents = true; return false; } int action = event.getActionMasked(); // Record the velocity if (action == MotionEvent.ACTION_DOWN) { reset(); } if (mVelocityTracker == null) { mVelocityTracker = VelocityTracker.obtain(); } mVelocityTracker.addMovement(event); // 2 switch (action) { case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: mTouchingScrollingChild = false; mActivePointerId = MotionEvent.INVALID_POINTER_ID; // Reset the ignore flag if (mIgnoreEvents) {//4 mIgnoreEvents = false; return false; } break; case MotionEvent.ACTION_DOWN: int initialX = (int) event.getX(); mInitialY = (int) event.getY(); View scroll = mNestedScrollingChildRef != null ? mNestedScrollingChildRef.get() : null; if (scroll != null && parent.isPointInChildBounds(scroll, initialX, mInitialY)) { mActivePointerId = event.getPointerId(event.getActionIndex()); mTouchingScrollingChild = true; } mIgnoreEvents = mActivePointerId == MotionEvent.INVALID_POINTER_ID && !parent.isPointInChildBounds(child, initialX, mInitialY); break; } // 1 if (!mIgnoreEvents && mViewDragHelper.shouldInterceptTouchEvent(event)) { return true; } // We have to handle cases that the ViewDragHelper does not capture the bottom sheet because // it is not the top most view of its parent. This is not necessary when the touch event is // happening over the scrolling content as nested scrolling logic handles that case. View scroll = mNestedScrollingChildRef.get(); //3 return action == MotionEvent.ACTION_MOVE && scroll != null && !mIgnoreEvents && mState != STATE_DRAGGING && !parent.isPointInChildBounds(scroll, (int) event.getX(), (int) event.getY()) && Math.abs(mInitialY - event.getY()) > mViewDragHelper.getTouchSlop(); }
onInterceptTouchEvent()中做了這幾件事:
-
判斷是否攔截事件,先使用ViewDragHelper進行攔截;
-
使用mVelocityTracker用以記錄手指的動作,用於計算Y軸的滾動速率;
-
判斷點選是否在NestedScrollView上,將結果儲存在mTouchingScrollingChild標記位上,用於在ViewDragHelper的回撥處理中判斷;
-
在ACTION_UP和ACTION_CANCEL對標記為進行復位,為下一次Touch準備;
對事件的處理
@Override public boolean onTouchEvent(CoordinatorLayout parent, V child, MotionEvent event) { if (!child.isShown()) { return false; } int action = event.getActionMasked(); if (mState == STATE_DRAGGING && action == MotionEvent.ACTION_DOWN) { return true; } if (mViewDragHelper != null) { mViewDragHelper.processTouchEvent(event);//2 } // Record the velocity if (action == MotionEvent.ACTION_DOWN) { reset(); } if (mVelocityTracker == null) { mVelocityTracker = VelocityTracker.obtain(); } mVelocityTracker.addMovement(event);//1 // The ViewDragHelper tries to capture only the top-most View. We have to explicitly tell it // to capture the bottom sheet in case it is not captured and the touch slop is passed. if (action == MotionEvent.ACTION_MOVE && !mIgnoreEvents) { if (Math.abs(mInitialY - event.getY()) > mViewDragHelper.getTouchSlop()) { mViewDragHelper.captureChildView(child, event.getPointerId(event.getActionIndex()));//3 } } return !mIgnoreEvents; }
OnTouchEvnet中做了如下處理:
-
使用mVelocityTracker用以記錄手指的動作,用於計算Y軸的滾動速率;
-
使用ViewDragHelper處理Touch事件,產生拖動效果;
-
ViewDragHelper在滑動的時候對BottomView的再次捕獲。再次明確告訴ViewDragHelper我需要移動的是BottomView。在如下場景中需要做這個處理:當你點選在BottomView的區域,但是BottomView的檢視層級不是最高的,或者你點選的區域不在BottomView上,ViewDragHelper在處理滑動的時候找不到BottomView,這個時候你需要主動告知ViewDragHelper現在要移動的是BottomView。、
對NestedScroll****的處理
onStartNestedScroll中宣告接收Y軸方向的滑動
@Override public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, V child, View directTargetChild, View target, int nestedScrollAxes) { mLastNestedScrollDy = 0; mNestedScrolled = false; return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0; }
在onNestedPreScroll中判斷髮起NestedScroll的 View 是否是我們在onLayoutChild 找到的那個控制元件.不是的話,不做處理。不處理就是不消耗y 軸,把所有的Scroll 交給發起的 View 自己消耗。如果處理,則根據dy判斷滑動方向,根據之前計算出的偏移量,使用ViewCompat.offsetTopAndBottom()方法對BottomView進行偏移操作,並將消耗的dy值記錄。
@Override public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, V child, View target, int dx, int dy, int[] consumed) { View scrollingChild = mNestedScrollingChildRef.get(); if (target != scrollingChild) { return; } int currentTop = child.getTop(); int newTop = currentTop - dy; if (dy > 0) { // Upward if (newTop < mMinOffset) { consumed[1] = currentTop - mMinOffset; ViewCompat.offsetTopAndBottom(child, -consumed[1]); setStateInternal(STATE_EXPANDED); } else { consumed[1] = dy; ViewCompat.offsetTopAndBottom(child, -dy); setStateInternal(STATE_DRAGGING); } } else if (dy < 0) { // Downward if (!target.canScrollVertically(-1)) { if (newTop <= mMaxOffset || mHideable) { consumed[1] = dy; ViewCompat.offsetTopAndBottom(child, -dy); setStateInternal(STATE_DRAGGING); } else { consumed[1] = currentTop - mMaxOffset; ViewCompat.offsetTopAndBottom(child, -consumed[1]); setStateInternal(STATE_COLLAPSED); } } } dispatchOnSlide(child.getTop()); mLastNestedScrollDy = dy; mNestedScrolled = true; }
在onStopNestedScroll中,根據當前BottomView所處的狀態確定它的最終位置,有必要的話,還會呼叫ViewDragHelper.smoothSlideViewTo進行滑動。
@Override public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target) { if (child.getTop() == mMinOffset) { setStateInternal(STATE_EXPANDED); return; } if (mNestedScrollingChildRef == null || target != mNestedScrollingChildRef.get() || !mNestedScrolled) { return; } int top; int targetState; if (mLastNestedScrollDy > 0) { top = mMinOffset; targetState = STATE_EXPANDED; } else if (mHideable && shouldHide(child, getYVelocity())) { top = mParentHeight; targetState = STATE_HIDDEN; } else if (mLastNestedScrollDy == 0) { int currentTop = child.getTop(); if (Math.abs(currentTop - mMinOffset) < Math.abs(currentTop - mMaxOffset)) { top = mMinOffset; targetState = STATE_EXPANDED; } else { top = mMaxOffset; targetState = STATE_COLLAPSED; } } else { top = mMaxOffset; targetState = STATE_COLLAPSED; } if (mViewDragHelper.smoothSlideViewTo(child, child.getLeft(), top)) { setStateInternal(STATE_SETTLING); ViewCompat.postOnAnimation(child, new SettleRunnable(child, targetState)); } else { setStateInternal(targetState); } mNestedScrolled = false; }
當向下滑動且Hideable為true時,會根據記錄的Y軸上的速率進行判斷,是否應該切換到Hideable狀態
在onNestedPreFling中處理快速滑動觸發,判斷邏輯是當前觸發滑動的控制元件為onLayoutChild中找到的那個並且當前BottomView的狀態不是完全展開的,此時會消耗快速滑動事件,其他情況下不處理,交給child自己處理。
@Override public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, V child, View target, float velocityX, float velocityY) { return target == mNestedScrollingChildRef.get() && (mState != STATE_EXPANDED || super.onNestedPreFling(coordinatorLayout, child, target, velocityX, velocityY)); }
最後我們總結一下:在BottomSheetBehavior中,對事件的攔截和處理通過ViewDragHelper來輔助處理拖拽滑動操作,對於NestedScroll,則是通過對滑動方向的判斷結合ViewCompat對BottomView進行處理。
3. 總結
-
CoordinatorLayout是一個
super FrameLayout
,它可以通過Behavior
與child
進行互動; -
我們可以通過自定義Behavior來設計child的互動規則,可以很靈活的實現比較複雜的聯動效果;
-
自定義Behavior主要有兩個大類:確定一個View和另一個View的依賴關係;指定某一個View響應Nested Scroll;
-
Behavior是一種外掛機制,如果沒有 Behavior 的存在,CoordinatorLayout 和普通的 FrameLayout 無異。Behavior 的存在,可以決定 CoordinatorLayout 中對應的 childview 的測量尺寸、佈局位置、觸控響應。
-
Behavior具有解耦功能,使用Behavior可以抽象出某個模組的View的行為,而不再是依賴於特定的View。