美文网首页Android开发优秀文章自定义控件
Android使用CoordinatorLayout实现联动效果

Android使用CoordinatorLayout实现联动效果

作者: 冷江明 | 来源:发表于2019-01-19 16:45 被阅读3次

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

    1.1 CoordinatorLayout介绍

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

    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.

    官网说它本质是一个 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()方法会被调用。

    为避免内容不易理解,我们来举例说明。

    首先我们自定义了一个可以跟随手指滑动变化位置的DragView。代码很简单,如下所示:

    public class DragView extends AppCompatTextView {
    
        private final int mSlop;
        private float mLastX;
        private float mLastY;
    
        public DragView(Context context) {
            this(context,null);
        }
    
        public DragView(Context context, AttributeSet attrs) {
            this(context, attrs,0);
        }
    
        public DragView(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            setClickable(true);
            mSlop = ViewConfiguration.getTouchSlop();
        }
    
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            int action = event.getAction();
            switch (action) {
                case MotionEvent.ACTION_DOWN:
                    mLastX = event.getRawX();
                    mLastY = event.getRawY();
                    break;
    
                case MotionEvent.ACTION_MOVE:
                    int deltax = (int) (event.getRawX() - mLastX);
                    int deltay = (int) (event.getRawY() - mLastY);
                    if (Math.abs(deltax) > mSlop || Math.abs(deltay) > mSlop) {
                        ViewCompat.offsetTopAndBottom(this,deltay);
                        ViewCompat.offsetLeftAndRight(this,deltax);
                        mLastX = event.getRawX();
                        mLastY = event.getRawY();
                    }
                    break;
                case MotionEvent.ACTION_UP:
                    mLastX = event.getRawX();
                    mLastY = event.getRawY();
                    break;
                default:
                    break;
            }
            return true;
        }
    }
    

    同时,在布局文件中引入,作为CoordinatorLayout中的一个child,默认初始位置是CoordinatorLayout的中心位置,布局如下所示:

    <android.support.design.widget.CoordinatorLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context="com.hikvision.update.demo.behaivior.BehaviorTestActivity">
    
        <android.support.design.widget.AppBarLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:theme="@style/AppTheme.AppBarOverlay">
    
            <android.support.v7.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                android:background="?attr/colorPrimary"
                app:popupTheme="@style/AppTheme.PopupOverlay" />
        </android.support.design.widget.AppBarLayout>
    
        <com.update.demo.behaivior.DragView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:padding="@dimen/isms_size_10dp"
            android:layout_gravity="center"
            android:text="DragView"
            android:background="@color/colorPrimary"
            android:textColor="#fff"
            android:textSize="16sp"/>
    
    </android.support.design.widget.CoordinatorLayout>
    

    接下来,我们来自定义一个DependencyBehavior,让使用这个Behavior的View位于DragView的上方:

    public class DependencyBehavior extends CoordinatorLayout.Behavior<View> {
    
        public DependencyBehavior() {
            super();
        }
    
        public DependencyBehavior(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
    
        @Override
        public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
            //判断依赖是否为DragView
            return dependency instanceof DragView;
        }
    
        @Override
        public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
            //获取DragView的顶部,让child位于DragView的左上方
            int top = dependency.getTop();
            int childHeight = child.getHeight();
            child.setY(top - childHeight);
            child.setX(dependency.getLeft());
            return true;
        }
    }
    

    在CoordinatorLayout布局中添加一个ImageView,并使用这个Behavior:

        <ImageView
            android:id="@+id/image"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="?attr/actionBarSize"
            android:src="@mipmap/ic_launcher_round"
            app:layout_behavior="com.demo.behaivior.DependencyBehavior" />
    

    实现效果如下:

    到此,View之间的依赖如何使用已经演示明白。我们接着来看对于滑动事件的响应。

    2.4 Behavior对滑动事件的响应

    首先,我们来看下onStartNestedScroll()方法:

    /**
             * Called when a descendant of the CoordinatorLayout attempts to initiate a nested scroll.
             *
             * <p>Any Behavior associated with any direct child of the CoordinatorLayout may respond
             * to this event and return true to indicate that the CoordinatorLayout should act as
             * a nested scrolling parent for this scroll. Only Behaviors that return true from
             * this method will receive subsequent nested scroll events.</p>
             *
             * @param coordinatorLayout the CoordinatorLayout parent of the view this Behavior is
             *                          associated with
             * @param child the child view of the CoordinatorLayout this Behavior is associated with
             * @param directTargetChild the child view of the CoordinatorLayout that either is or
             *                          contains the target of the nested scroll operation
             * @param target the descendant view of the CoordinatorLayout initiating the nested scroll
             * @param axes the axes that this nested scroll applies to. See
             *                         {@link ViewCompat#SCROLL_AXIS_HORIZONTAL},
             *                         {@link ViewCompat#SCROLL_AXIS_VERTICAL}
             * @param type the type of input which cause this scroll event
             * @return true if the Behavior wishes to accept this nested scroll
             *
             * @see NestedScrollingParent2#onStartNestedScroll(View, View, int, int)
             */
            public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
                    @NonNull V child, @NonNull View directTargetChild, @NonNull View target,
                    @ScrollAxis int axes, @NestedScrollType int type) {
                if (type == ViewCompat.TYPE_TOUCH) {
                    return onStartNestedScroll(coordinatorLayout, child, directTargetChild,
                            target, axes);
                }
                return false;
            }
    

    注释中说,当一个CoordinatorLayout中的子View企图触发一个Nested scroll事件时,这个方法会被调用。并且只有在onStartNestedScroll()方法返回为true时,后续的Nested Scroll事件才会响应。

    后续的回调是这几个:

        @Override
        public void onNestedScrollAccepted(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child,
                                           @NonNull View directTargetChild, @NonNull View target, int axes, int type) {
            super.onNestedScrollAccepted(coordinatorLayout, child, directTargetChild, target, axes, type);
        }
    
        @Override
        public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child,
                                      @NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
            super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type);
          }
    
        @Override
        public void onNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child,
                                   @NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed,
                                   int dyUnconsumed, int type) {
            super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type);
        }
    
        @Override
        public void onStopNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child,
                                       @NonNull View target, int type) {
            super.onStopNestedScroll(coordinatorLayout, child, target, type);
        }
    
        @Override
        public boolean onNestedFling(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child,
                                     @NonNull View target, float velocityX, float velocityY, boolean consumed) {
            return super.onNestedFling(coordinatorLayout, child, target, velocityX, velocityY, consumed);
        }
    
        @Override
        public boolean onNestedPreFling(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child,
                                        @NonNull View target, float velocityX, float velocityY) {
            return super.onNestedPreFling(coordinatorLayout, child, target, velocityX, velocityY);
        }
    

    那么Nested Scroll又是什么呢?哪些控件可以触发Nested Scroll呢?

    通过追踪调用onStartNestedScroll()方法的源码,最终可以得到结论:如果在5.0的系统版本以上,我们需要对View.setNestedScrollingEnable(true),如果在这个版本之下,得保证这个View本身是NestedScrollingChild的实现类,只有这样,才可以触发Nested Scroll

    借助于AndroidStudio,我们可以知道NestedScrollingChild的实现类有:RecyclerViewNavigationMenuViewSwipeRefreshLayoutNestedScrollView

    接下来,我们用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的简单方式,如果你还是一头雾水,搞不清楚用法,不用担心,下面我们就来具体说明下这几个方法的调用流程和具体功能:

    首先我们来看一张流程图

    图中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<View>

    
    /**
     * An interaction behavior plugin for a child view of {@link CoordinatorLayout} to make it work as
     * a bottom sheet.
     */
    public class BottomSheetBehavior<V extends View> extends CoordinatorLayout.Behavior<V>{
      ...
    }
    

    先看下构造方法

    /**
         * Default constructor for inflating BottomSheetBehaviors from layout.
         *
         * @param context The {@link Context}.
         * @param 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。

    相关文章

      网友评论

        本文标题:Android使用CoordinatorLayout实现联动效果

        本文链接:https://www.haomeiwen.com/subject/pzuqrqtx.html