Android使用CoordinatorLayout实现联动效果

时间:2024-03-13 17:11:15

在开发过程中,有时需要实现一些比较复杂的联动效果:比如在滚动列表的时候改变某个View的状态,随着滚动程度的变化,View也跟随变化等等。要想实现这些效果,用普通的方法也可以实现,不过需要设计很多的监听来控制,逻辑也比较复杂,而通过CoordinatorLayout可以更优雅的实现同样的效果。

1.1 CoordinatorLayout介绍

CoordinatorLayout 是 Google 在 Design Support 包中提供的一个十分强大的布局视图,我们先来看下官网介绍

CoordinatorLayout

public class CoordinatorLayout 
extends ViewGroup implements NestedScrollingParent2NestedScrollingParent3
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.

官网说它本质是一个 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,其具体作用介绍如下:

  1. 确定一个View(child)是否依赖于另一个View(dependency),需要在layoutDependsOn()方法中进行判断并返回一个布尔值,return true表示依赖成立,反之不成立。并且只有在layoutDependsOn()返回为true时,后面的onDependentViewChanged()onDependentViewRemoved()方法才会被调用。

  2. 当确定依赖的View(dependency)发生变化时,onDependentViewChanged()方法会被调用,我们可以在这个方法中拿到变化后的dependency,并对自己的View进行处理。

  3. View(dependency)被移除时,onDependentViewRemoved()方法会被调用。

#####为避免内容不易理解,我们来举例说明。
Android使用CoordinatorLayout实现联动效果

首先我们自定义了一个可以跟随手指滑动变化位置的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>

Android使用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" />

实现效果如下:

Android使用CoordinatorLayout实现联动效果

到此,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的实现类有:RecyclerViewNavigationMenuViewSwipeRefreshLayoutNestedScrollView

Android使用CoordinatorLayout实现联动效果

接下来,我们用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准备滑动多少距离,dxdy分别是横向和竖向的滑动位移,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滑动距离后,直接移动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", "onNestedPreScroll  dx:" + dx + " dy:" + dy);
        //加上这句,child消费掉所有dy
        consumed[1] = dy;
        ViewCompat.offsetTopAndBottom(child, dy);
    }

此时的效果就是:不论NestedScrollView如何滑动,仅能看到ImageView跟随手势动作。

上面举例说明了下Behavior响应NestedScroll的简单方式,如果你还是一头雾水,搞不清楚用法,不用担心,下面我们就来具体说明下这几个方法的调用流程和具体功能:

首先我们来看一张流程图

Android使用CoordinatorLayout实现联动效果

图中Child对应我们上面例子中的NestedScrollViewParentCoordinatorLayout,而CoordinatorLayout会将接收到的NestedScroll向各个child中的Behavior进行分发,我们可以简单理解为此处的Parent就是Behavior
(PS:流程图来自这篇文章,有兴趣的也可以看看)

Child中的DOWNMOVEUP均为childOnTouchEvent()中接收到的手势事件;

我们可以看到:

  1. child在接收到DOWN手势时,发起嵌套滚动请求,请求中携带有嵌套滑动的方向(方向为child在初始化时已经被声明过的);

  2. Parent接收到嵌套滚动请求,如果滚动方向是自己需要的则同意嵌套滚动,这时一般主动放弃拦截MOVE事件,Parent在这个过程中调用了自身的onStartNestedScroll()onNestedScrollAccepted();

  3. Child在接收到MOVE手势时,在自身准备滚动前,去询问Parent是否需要滚动(dispatchNestedPreScroll),参数中声明了本次滚动的横向和竖向距离dx,dy,并要求告知Parent消费掉的距离和窗口偏移大小

  4. ParentonNestedPreScroll()方法中接收到滚动准备请求,如果需要可以执行滑动操作,并根据需求,将消耗的距离保存到int[ ] consumed中,consumed[0]保存dx消耗,consumed[1]保存dy消耗;

  5. Child在接收到Parent的反馈后,执行自身的滚动,这个滚动是将计划滚动距离减去consumed数组中消耗的剩余距离,在滚动之后分发剩余的未消费的滚动距离 (dispatchNestedScroll),参数中声明自己已消费的xy距离和未消费的xy距离,并要求告知窗口偏移

  6. ParentonNestedScroll()方法中接收到滚动请求,此时可以根据需求,通过滑动消费掉child提供的未消费距离;

  7. Child在接收到UP手势时,如果判断当前滚动仍需要继续,那么会在自身滚动前询问Parent是否需要继续滚动,参数中会声明xy的速度;

  8. ParentonNestedPreFling()中接收到预遗留滚动请求,根据自身需要选择执行逻辑;

  9. Child在自身执行完遗留滚动后,询问Parent是否需要执行,参数中声明xy的速度已经是否已消费;

10.ParentonNestedFling()接收到child询问后,可以选择执行未消费的遗留滚动;

  1. Child滚动执行结束,通知Parent

  2. ParentonStopNestedScroll()接收到结束滚动的通知,停止滚动操作,此时可根据Parent的当前状态,作一些逻辑处理

以上,就是Nested Scroll的完整的处理流程。

了解了上面对Behavior的介绍,我们可以明白一个Behavior的运作机制。下面我们将对Android官方提供的BottomSheetBehavior进行分析,以加深理解。

2.5 BottomSheetBehavior源码分析

BottomSheetBehavior直接继承自CoordinatorLayout.Behavior


/**
 * 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 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();
    }

在构造方法中获取了设置的弹出高度,是否支持手势下拉隐藏功能以及弹出时是否支持动画的属性。

继续看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;
    }

这个方法中,主要做了几件事:

  1. 首先设置BottomView适配屏幕;

  2. 对BottomView进行摆放:先调用父类对BottomView进行布局,根据PeekHeight和State对BottomView位置进行偏移,如果PeekHeight没有设置,一般默认为屏幕高度的9/16的位置;

  3. 对mMinOffset,mMaxOffset进行计算,用来确定BottomView的偏移范围。即距离CoordinatorLayout原点Y轴 mMinOffset到mMaxOffset之间;

  4. 初始化ViewDragHelper类,用以处理拖拽和滑动事件;

  5. 存储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()中做了这几件事:

  1. 判断是否拦截事件,先使用ViewDragHelper进行拦截;

  2. 使用mVelocityTracker用以记录手指的动作,用于计算Y轴的滚动速率;

  3. 判断点击是否在NestedScrollView上,将结果保存在mTouchingScrollingChild标记位上,用于在ViewDragHelper的回调处理中判断;

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

  1. 使用mVelocityTracker用以记录手指的动作,用于计算Y轴的滚动速率;

  2. 使用ViewDragHelper处理Touch事件,产生拖动效果;

  3. 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. 总结

  1. CoordinatorLayout是一个super FrameLayout,它可以通过Behaviorchild进行交互;

  2. 我们可以通过自定义Behavior来设计child的交互规则,可以很灵活的实现比较复杂的联动效果;

  3. 自定义Behavior主要有两个大类:确定一个View和另一个View的依赖关系;指定某一个View响应Nested Scroll;

  4. Behavior是一种插件机制,如果没有 Behavior 的存在,CoordinatorLayout 和普通的 FrameLayout 无异。Behavior 的存在,可以决定 CoordinatorLayout 中对应的 childview 的测量尺寸、布局位置、触摸响应。

  5. Behavior具有解耦功能,使用Behavior可以抽象出某个模块的View的行为,而不再是依赖于特定的View。