Android使用CoordinatorLayout實(shí)現(xiàn)聯(lián)動(dòng)效果

在開發(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:

  1. As a top-level application decor or chrome layout
  2. 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,其具體作用介紹如下:

  1. 確定一個(gè)View(child)是否依賴于另一個(gè)View(dependency),需要在layoutDependsOn()方法中進(jìn)行判斷并返回一個(gè)布爾值,return true表示依賴成立,反之不成立。并且只有在layoutDependsOn()返回為true時(shí),后面的onDependentViewChanged()onDependentViewRemoved()方法才會(huì)被調(diào)用。

  2. 當(dāng)確定依賴的View(dependency)發(fā)生變化時(shí),onDependentViewChanged()方法會(huì)被調(diào)用,我們可以在這個(gè)方法中拿到變化后的dependency,并對(duì)自己的View進(jìn)行處理。

  3. 當(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、SwipeRefreshLayoutNestedScrollView

接下來,我們用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)多少距離,dxdy分別是橫向和豎向的滑動(dòng)位移,int[ ] consumed 用以記錄Behavior消耗的dxdy;

 @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,ParentCoordinatorLayout,而CoordinatorLayout會(huì)將接收到的NestedScroll向各個(gè)child中的Behavior進(jìn)行分發(fā),我們可以簡單理解為此處的Parent就是Behavior
(PS:流程圖來自這篇文章,有興趣的也可以看看)

Child中的DOWN、MOVEUP均為childOnTouchEvent()中接收到的手勢事件;

我們可以看到:

  1. child在接收到DOWN手勢時(shí),發(fā)起嵌套滾動(dòng)請(qǐng)求,請(qǐng)求中攜帶有嵌套滑動(dòng)的方向(方向?yàn)閏hild在初始化時(shí)已經(jīng)被聲明過的);

  2. Parent接收到嵌套滾動(dòng)請(qǐng)求,如果滾動(dòng)方向是自己需要的則同意嵌套滾動(dòng),這時(shí)一般主動(dòng)放棄攔截MOVE事件,Parent在這個(gè)過程中調(diào)用了自身的onStartNestedScroll()onNestedScrollAccepted();

  3. Child在接收到MOVE手勢時(shí),在自身準(zhǔn)備滾動(dòng)前,去詢問Parent是否需要滾動(dòng)(dispatchNestedPreScroll),參數(shù)中聲明了本次滾動(dòng)的橫向和豎向距離dx,dy,并要求告知Parent消費(fèi)掉的距離和窗口偏移大小

  4. ParentonNestedPreScroll()方法中接收到滾動(dòng)準(zhǔn)備請(qǐng)求,如果需要可以執(zhí)行滑動(dòng)操作,并根據(jù)需求,將消耗的距離保存到int[ ] consumed中,consumed[0]保存dx消耗,consumed[1]保存dy消耗;

  5. 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)的xy距離,并要求告知窗口偏移

  6. ParentonNestedScroll()方法中接收到滾動(dòng)請(qǐng)求,此時(shí)可以根據(jù)需求,通過滑動(dòng)消費(fèi)掉child提供的未消費(fèi)距離;

  7. Child在接收到UP手勢時(shí),如果判斷當(dāng)前滾動(dòng)仍需要繼續(xù),那么會(huì)在自身滾動(dòng)前詢問Parent是否需要繼續(xù)滾動(dòng),參數(shù)中會(huì)聲明x、y的速度;

  8. ParentonNestedPreFling()中接收到預(yù)遺留滾動(dòng)請(qǐng)求,根據(jù)自身需要選擇執(zhí)行邏輯;

  9. Child在自身執(zhí)行完遺留滾動(dòng)后,詢問Parent是否需要執(zhí)行,參數(shù)中聲明x、y的速度已經(jīng)是否已消費(fèi);

10.ParentonNestedFling()接收到child詢問后,可以選擇執(zhí)行未消費(fèi)的遺留滾動(dòng);

  1. Child滾動(dòng)執(zhí)行結(jié)束,通知Parent

  2. ParentonStopNestedScroll()接收到結(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è)方法中,主要做了幾件事:

  1. 首先設(shè)置BottomView適配屏幕;

  2. 對(duì)BottomView進(jìn)行擺放:先調(diào)用父類對(duì)BottomView進(jìn)行布局,根據(jù)PeekHeight和State對(duì)BottomView位置進(jìn)行偏移,如果PeekHeight沒有設(shè)置,一般默認(rèn)為屏幕高度的9/16的位置;

  3. 對(duì)mMinOffset,mMaxOffset進(jìn)行計(jì)算,用來確定BottomView的偏移范圍。即距離CoordinatorLayout原點(diǎn)Y軸 mMinOffset到mMaxOffset之間;

  4. 初始化ViewDragHelper類,用以處理拖拽和滑動(dòng)事件;

  5. 存儲(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()中做了這幾件事:

  1. 判斷是否攔截事件,先使用ViewDragHelper進(jìn)行攔截;

  2. 使用mVelocityTracker用以記錄手指的動(dòng)作,用于計(jì)算Y軸的滾動(dòng)速率;

  3. 判斷點(diǎn)擊是否在NestedScrollView上,將結(jié)果保存在mTouchingScrollingChild標(biāo)記位上,用于在ViewDragHelper的回調(diào)處理中判斷;

  4. 在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中做了如下處理:

  1. 使用mVelocityTracker用以記錄手指的動(dòng)作,用于計(jì)算Y軸的滾動(dòng)速率;

  2. 使用ViewDragHelper處理Touch事件,產(chǎn)生拖動(dòng)效果;

  3. 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é)

  1. CoordinatorLayout是一個(gè)super FrameLayout,它可以通過Behaviorchild進(jìn)行交互;

  2. 我們可以通過自定義Behavior來設(shè)計(jì)child的交互規(guī)則,可以很靈活的實(shí)現(xiàn)比較復(fù)雜的聯(lián)動(dòng)效果;

  3. 自定義Behavior主要有兩個(gè)大類:確定一個(gè)View和另一個(gè)View的依賴關(guān)系;指定某一個(gè)View響應(yīng)Nested Scroll;

  4. Behavior是一種插件機(jī)制,如果沒有 Behavior 的存在,CoordinatorLayout 和普通的 FrameLayout 無異。Behavior 的存在,可以決定 CoordinatorLayout 中對(duì)應(yīng)的 childview 的測量尺寸、布局位置、觸摸響應(yīng)。

  5. Behavior具有解耦功能,使用Behavior可以抽象出某個(gè)模塊的View的行為,而不再是依賴于特定的View。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容