在開發(fā)過程中,有時(shí)需要實(shí)現(xiàn)一些比較復(fù)雜的聯(lián)動(dòng)效果:比如在滾動(dòng)列表的時(shí)候改變某個(gè)View的狀態(tài),隨著滾動(dòng)程度的變化,View也跟隨變化等等。要想實(shí)現(xiàn)這些效果,用普通的方法也可以實(shí)現(xiàn),不過需要設(shè)計(jì)很多的監(jiān)聽來控制,邏輯也比較復(fù)雜,而通過CoordinatorLayout可以更優(yōu)雅的實(shí)現(xiàn)同樣的效果。
1.1 CoordinatorLayout介紹
CoordinatorLayout 是 Google 在 Design Support 包中提供的一個(gè)十分強(qiáng)大的布局視圖,我們先來看下官網(wǎng)介紹
CoordinatorLayout
public class CoordinatorLayout
extends ViewGroup implements NestedScrollingParent2, NestedScrollingParent3
java.lang.Object
? android.view.View
? android.view.ViewGroup
? 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.
官網(wǎng)說它本質(zhì)是一個(gè) FrameLayout,它可以作為一個(gè)容器指定與child 的一些交互規(guī)則。通過給View設(shè)置Behaviors,就可以和 child 進(jìn)行交互,或者是 child 之間互相進(jìn)行相關(guān)的交互,并且自定義 View 時(shí),可以通過DefaultBehavior這個(gè)注解來指定它關(guān)聯(lián)的 Behavior。
如此看來,我們只需要定制Behavior就可以定制我們的交互了,再來看下Behavior的內(nèi)容。
1.2 CoordinatorLayout.Behavior介紹
Behavior是CoordinatorLayout中的一個(gè)靜態(tài)內(nèi)部類。
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是針對(duì)CoordinatorLayout中child的交互插件。Behavior同時(shí)也是一個(gè)抽象類,它的實(shí)現(xiàn)類都是為了能夠讓用戶作用在一個(gè)View上進(jìn)行拖拽、滑動(dòng)、快速滑動(dòng)等手勢。
下面我們就來看下Behavior中的關(guān)鍵代碼
//類型一
@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);
}
從方法的功能側(cè)重來看,可以分為兩類,一是根據(jù)某些依賴的View的變化來實(shí)現(xiàn)效果;二是根據(jù)某些組件的滑動(dòng)事件來實(shí)現(xiàn)效果;其中第一類對(duì)應(yīng)前三個(gè)API,第二類對(duì)應(yīng)后面的API。我們先看第一類情況。
2.3 Behavior設(shè)置View之間依賴
View之間的依賴使用的是第一類API,其具體作用介紹如下:
確定一個(gè)
View(child)是否依賴于另一個(gè)View(dependency),需要在layoutDependsOn()方法中進(jìn)行判斷并返回一個(gè)布爾值,returntrue表示依賴成立,反之不成立。并且只有在layoutDependsOn()返回為true時(shí),后面的onDependentViewChanged()和onDependentViewRemoved()方法才會(huì)被調(diào)用。當(dāng)確定依賴的
View(dependency)發(fā)生變化時(shí),onDependentViewChanged()方法會(huì)被調(diào)用,我們可以在這個(gè)方法中拿到變化后的dependency,并對(duì)自己的View進(jìn)行處理。當(dāng)
View(dependency)被移除時(shí),onDependentViewRemoved()方法會(huì)被調(diào)用。
為避免內(nèi)容不易理解,我們來舉例說明。

首先我們自定義了一個(gè)可以跟隨手指滑動(dòng)變化位置的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;
}
}
同時(shí),在布局文件中引入,作為CoordinatorLayout中的一個(gè)child,默認(rèn)初始位置是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>

接下來,我們來自定義一個(gè)DependencyBehavior,讓使用這個(gè)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布局中添加一個(gè)ImageView,并使用這個(gè)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" />
實(shí)現(xiàn)效果如下:

到此,View之間的依賴如何使用已經(jīng)演示明白。我們接著來看對(duì)于滑動(dòng)事件的響應(yīng)。
2.4 Behavior對(duì)滑動(dòng)事件的響應(yīng)
首先,我們來看下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;
}
注釋中說,當(dāng)一個(gè)CoordinatorLayout中的子View企圖觸發(fā)一個(gè)Nested scroll事件時(shí),這個(gè)方法會(huì)被調(diào)用。并且只有在onStartNestedScroll()方法返回為true時(shí),后續(xù)的Nested Scroll事件才會(huì)響應(yīng)。
后續(xù)的回調(diào)是這幾個(gè):
@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又是什么呢?哪些控件可以觸發(fā)Nested Scroll呢?
通過追蹤調(diào)用onStartNestedScroll()方法的源碼,最終可以得到結(jié)論:如果在5.0的系統(tǒng)版本以上,我們需要對(duì)View.setNestedScrollingEnable(true),如果在這個(gè)版本之下,得保證這個(gè)View本身是NestedScrollingChild的實(shí)現(xiàn)類,只有這樣,才可以觸發(fā)Nested Scroll。
借助于AndroidStudio,我們可以知道NestedScrollingChild的實(shí)現(xiàn)類有:RecyclerView、NavigationMenuView、SwipeRefreshLayout、NestedScrollView

接下來,我們用NestedScrollView舉例,來實(shí)現(xiàn)一個(gè)對(duì)Nested Scroll響應(yīng)的簡單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>
我們新增了一個(gè)NestedScrollView,同時(shí)我們希望在NestedScrollView滑動(dòng)的時(shí)候,ImageView可以跟隨著一起滑動(dòng)?,F(xiàn)在我們來改造下之前的DependencyBehavior。
首先去除View的依賴關(guān)系:
@Override
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
//判斷依賴是否為DragView
// return dependency instanceof DragView;
return false;
}
然后在onStartNestedScroll()方法中作如下修改,以保證對(duì)豎直方向滑動(dòng)的接收:
@Override
public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child,
@NonNull View directTargetChild, @NonNull View target, int axes, int type) {
//child為ImageView 并且滑動(dòng)方向?yàn)樨Q直方向才響應(yīng)
return child instanceof ImageView && ViewCompat.SCROLL_AXIS_VERTICAL == axes;
}
我們繼續(xù)重寫OnNestedPreScroll()方法,這個(gè)方法會(huì)在NestedScrollView準(zhǔn)備滑動(dòng)的時(shí)候被調(diào)用,用以通知Behavior,NestedScrollView準(zhǔn)備滑動(dòng)多少距離,dx和dy分別是橫向和豎向的滑動(dòng)位移,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", "onNestedPreScroll dx:" + dx + " dy:" + dy);
ViewCompat.offsetTopAndBottom(child, dy);
}
在接收到dy滑動(dòng)距離后,直接移動(dòng)childView。這樣就可以實(shí)現(xiàn)我們預(yù)計(jì)的效果了。

如果我們想讓child消費(fèi)掉所有的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", "onNestedPreScroll dx:" + dx + " dy:" + dy);
//加上這句,child消費(fèi)掉所有dy
consumed[1] = dy;
ViewCompat.offsetTopAndBottom(child, dy);
}
此時(shí)的效果就是:不論NestedScrollView如何滑動(dòng),僅能看到ImageView跟隨手勢動(dòng)作。
上面舉例說明了下Behavior響應(yīng)NestedScroll的簡單方式,如果你還是一頭霧水,搞不清楚用法,不用擔(dān)心,下面我們就來具體說明下這幾個(gè)方法的調(diào)用流程和具體功能:
首先我們來看一張流程圖

圖中Child對(duì)應(yīng)我們上面例子中的NestedScrollView,Parent是CoordinatorLayout,而CoordinatorLayout會(huì)將接收到的NestedScroll向各個(gè)child中的Behavior進(jìn)行分發(fā),我們可以簡單理解為此處的Parent就是Behavior。
(PS:流程圖來自這篇文章,有興趣的也可以看看)
Child中的DOWN、MOVE、UP均為child在OnTouchEvent()中接收到的手勢事件;
我們可以看到:
在
child在接收到DOWN手勢時(shí),發(fā)起嵌套滾動(dòng)請(qǐng)求,請(qǐng)求中攜帶有嵌套滑動(dòng)的方向(方向?yàn)閏hild在初始化時(shí)已經(jīng)被聲明過的);Parent接收到嵌套滾動(dòng)請(qǐng)求,如果滾動(dòng)方向是自己需要的則同意嵌套滾動(dòng),這時(shí)一般主動(dòng)放棄攔截MOVE事件,Parent在這個(gè)過程中調(diào)用了自身的onStartNestedScroll()和onNestedScrollAccepted();Child在接收到MOVE手勢時(shí),在自身準(zhǔn)備滾動(dòng)前,去詢問Parent是否需要滾動(dòng)(dispatchNestedPreScroll),參數(shù)中聲明了本次滾動(dòng)的橫向和豎向距離dx,dy,并要求告知Parent消費(fèi)掉的距離和窗口偏移大小Parent在onNestedPreScroll()方法中接收到滾動(dòng)準(zhǔn)備請(qǐng)求,如果需要可以執(zhí)行滑動(dòng)操作,并根據(jù)需求,將消耗的距離保存到int[ ] consumed中,consumed[0]保存dx消耗,consumed[1]保存dy消耗;Child在接收到Parent的反饋后,執(zhí)行自身的滾動(dòng),這個(gè)滾動(dòng)是將計(jì)劃滾動(dòng)距離減去consumed數(shù)組中消耗的剩余距離,在滾動(dòng)之后分發(fā)剩余的未消費(fèi)的滾動(dòng)距離 (dispatchNestedScroll),參數(shù)中聲明自己已消費(fèi)的x、y距離和未消費(fèi)的x、y距離,并要求告知窗口偏移Parent在onNestedScroll()方法中接收到滾動(dòng)請(qǐng)求,此時(shí)可以根據(jù)需求,通過滑動(dòng)消費(fèi)掉child提供的未消費(fèi)距離;Child在接收到UP手勢時(shí),如果判斷當(dāng)前滾動(dòng)仍需要繼續(xù),那么會(huì)在自身滾動(dòng)前詢問Parent是否需要繼續(xù)滾動(dòng),參數(shù)中會(huì)聲明x、y的速度;Parent在onNestedPreFling()中接收到預(yù)遺留滾動(dòng)請(qǐng)求,根據(jù)自身需要選擇執(zhí)行邏輯;Child在自身執(zhí)行完遺留滾動(dòng)后,詢問Parent是否需要執(zhí)行,參數(shù)中聲明x、y的速度已經(jīng)是否已消費(fèi);
10.Parent在onNestedFling()接收到child詢問后,可以選擇執(zhí)行未消費(fèi)的遺留滾動(dòng);
Child滾動(dòng)執(zhí)行結(jié)束,通知Parent;Parent在onStopNestedScroll()接收到結(jié)束滾動(dòng)的通知,停止?jié)L動(dòng)操作,此時(shí)可根據(jù)Parent的當(dāng)前狀態(tài),作一些邏輯處理
以上,就是Nested Scroll的完整的處理流程。
了解了上面對(duì)Behavior的介紹,我們可以明白一個(gè)Behavior的運(yùn)作機(jī)制。下面我們將對(duì)Android官方提供的BottomSheetBehavior進(jìn)行分析,以加深理解。
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>{
...
}
先看下構(gòu)造方法
/**
* Default constructor for inflating BottomSheetBehaviors from layout.
*
* @param context The {@link Context}.
* @param attrs The {@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();
}
在構(gòu)造方法中獲取了設(shè)置的彈出高度,是否支持手勢下拉隱藏功能以及彈出時(shí)是否支持動(dòng)畫的屬性。
繼續(xù)看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;
}
這個(gè)方法中,主要做了幾件事:
首先設(shè)置BottomView適配屏幕;
對(duì)BottomView進(jìn)行擺放:先調(diào)用父類對(duì)BottomView進(jìn)行布局,根據(jù)PeekHeight和State對(duì)BottomView位置進(jìn)行偏移,如果PeekHeight沒有設(shè)置,一般默認(rèn)為屏幕高度的9/16的位置;
對(duì)mMinOffset,mMaxOffset進(jìn)行計(jì)算,用來確定BottomView的偏移范圍。即距離CoordinatorLayout原點(diǎn)Y軸 mMinOffset到mMaxOffset之間;
初始化ViewDragHelper類,用以處理拖拽和滑動(dòng)事件;
存儲(chǔ)BottomView的軟引用并遞歸尋找到BottomView中的第一個(gè)NestedScrollingChild組件;
說明一下:由于Android中屏幕的坐標(biāo)軸是向下為y軸正方向,因此在計(jì)算PeekHeight時(shí),會(huì)讓ParentHeight-mPeekHeight,此時(shí)顯示的高度才是設(shè)置的高度。
對(duì)于事件攔截的處理
@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進(jìn)行攔截;
使用mVelocityTracker用以記錄手指的動(dòng)作,用于計(jì)算Y軸的滾動(dòng)速率;
判斷點(diǎn)擊是否在NestedScrollView上,將結(jié)果保存在mTouchingScrollingChild標(biāo)記位上,用于在ViewDragHelper的回調(diào)處理中判斷;
在ACTION_UP和ACTION_CANCEL對(duì)標(biāo)記為進(jìn)行復(fù)位,為下一次Touch準(zhǔn)備;
對(duì)事件的處理
@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用以記錄手指的動(dòng)作,用于計(jì)算Y軸的滾動(dòng)速率;
使用ViewDragHelper處理Touch事件,產(chǎn)生拖動(dòng)效果;
ViewDragHelper在滑動(dòng)的時(shí)候?qū)ottomView的再次捕獲。再次明確告訴ViewDragHelper我需要移動(dòng)的是BottomView。在如下場景中需要做這個(gè)處理:當(dāng)你點(diǎn)擊在BottomView的區(qū)域,但是BottomView的視圖層級(jí)不是最高的,或者你點(diǎn)擊的區(qū)域不在BottomView上,ViewDragHelper在處理滑動(dòng)的時(shí)候找不到BottomView,這個(gè)時(shí)候你需要主動(dòng)告知ViewDragHelper現(xiàn)在要移動(dòng)的是BottomView。
對(duì)NestedScroll的處理
onStartNestedScroll中聲明接收Y軸方向的滑動(dòng)
@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中判斷發(fā)起NestedScroll的 View 是否是我們?cè)趏nLayoutChild 找到的那個(gè)控件.不是的話,不做處理。不處理就是不消耗y 軸,把所有的Scroll 交給發(fā)起的 View 自己消耗。如果處理,則根據(jù)dy判斷滑動(dòng)方向,根據(jù)之前計(jì)算出的偏移量,使用ViewCompat.offsetTopAndBottom()方法對(duì)BottomView進(jìn)行偏移操作,并將消耗的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中,根據(jù)當(dāng)前BottomView所處的狀態(tài)確定它的最終位置,有必要的話,還會(huì)調(diào)用ViewDragHelper.smoothSlideViewTo進(jìn)行滑動(dòng)。
@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;
}
當(dāng)向下滑動(dòng)且Hideable為true時(shí),會(huì)根據(jù)記錄的Y軸上的速率進(jìn)行判斷,是否應(yīng)該切換到Hideable狀態(tài)
在onNestedPreFling中處理快速滑動(dòng)觸發(fā),判斷邏輯是當(dāng)前觸發(fā)滑動(dòng)的控件為onLayoutChild中找到的那個(gè)并且當(dāng)前BottomView的狀態(tài)不是完全展開的,此時(shí)會(huì)消耗快速滑動(dòng)事件,其他情況下不處理,交給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));
}
最后我們總結(jié)一下:在BottomSheetBehavior中,對(duì)事件的攔截和處理通過ViewDragHelper來輔助處理拖拽滑動(dòng)操作,對(duì)于NestedScroll,則是通過對(duì)滑動(dòng)方向的判斷結(jié)合ViewCompat對(duì)BottomView進(jìn)行處理。
3. 總結(jié)
CoordinatorLayout是一個(gè)
super FrameLayout,它可以通過Behavior與child進(jìn)行交互;我們可以通過自定義Behavior來設(shè)計(jì)child的交互規(guī)則,可以很靈活的實(shí)現(xiàn)比較復(fù)雜的聯(lián)動(dòng)效果;
自定義Behavior主要有兩個(gè)大類:確定一個(gè)View和另一個(gè)View的依賴關(guān)系;指定某一個(gè)View響應(yīng)Nested Scroll;
Behavior是一種插件機(jī)制,如果沒有 Behavior 的存在,CoordinatorLayout 和普通的 FrameLayout 無異。Behavior 的存在,可以決定 CoordinatorLayout 中對(duì)應(yīng)的 childview 的測量尺寸、布局位置、觸摸響應(yīng)。
Behavior具有解耦功能,使用Behavior可以抽象出某個(gè)模塊的View的行為,而不再是依賴于特定的View。