NestedScrolling机制之CoordinatorLayout.Behavior实战

1,419 阅读6分钟
原文链接: www.jianshu.com

在上一讲中我们讲了NestedScrolling机制,其实android很多有些常用的控件都是支持NestedScrolling机制的,如RecyclerView,NestedScrollView等,

public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild2{}

public class NestedScrollView extends FrameLayout implements NestedScrollingParent2,NestedScrollingChild2, ScrollingView{}

这些控件内部用的就是我们上一讲的东西,通过上一讲的内容其实我们已经可以实现很复杂的ui效果了,那个这一讲讲什么呢,就是CoordinatorLayout,CoordinatorLayout.Behavior这个相当于NestedScrolling机制的运用和封装。

简单来说CoordinatorLayout像一个容易,包含所有子View,协调其子View之间的动作的一个父View,而Behavior是用来给CoordinatorLayout里的子View实现交互的。

单单说概念可能大家都理解不深,接下来就讲我写的类似美团外卖骨架的demo吧。 看效果图先吧:

waimaidetails.gif

这种效果假如不用CoordinatorLayout其实还是有点难麻烦的,不过有了CoordinatorLayout就简单了,首先我们看一下布局文件:

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.jack.meituangoodsdetails.view.GoodDetailsView
        android:id="@+id/goods_details_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    </com.jack.meituangoodsdetails.view.GoodDetailsView>

    <com.jack.meituangoodsdetails.view.GoodsListView
        android:id="@+id/goods_list_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/goods_list_behavior">
    </com.jack.meituangoodsdetails.view.GoodsListView>

    <com.jack.meituangoodsdetails.view.GoodsTitleView
        android:id="@+id/goods_title_view"
        android:layout_width="match_parent"
        android:layout_height="50dp">
    </com.jack.meituangoodsdetails.view.GoodsTitleView>

</androidx.coordinatorlayout.widget.CoordinatorLayout>

从上面的布局文件可以看出,CoordinatorLayout包含着3个自定义的Viewr然后就没了,其中GoodDetailsView是图片和下面商品详情的View,GoodsTitleView如其名字那样是界面的头部的View, GoodsListView就是给我们滑动的View了。在这布局里,我们看到一个比较特殊的东西app:layout_behavior="@string/goods_list_behavior",这是什么呢?

其实这是CoordinatorLayout父View绑定一个叫goods_list_behavior的子View,有个这个就完成了父View和子View的关联,那么goods_list_behavior又指向那个类呢?看字符串资源文件

<string name="goods_list_behavior">com.jack.meituangoodsdetails.hehavior.GoodsListBehavior</string>

可以是指向一个叫GoodsListBehavior的类,这也是这个UI交互的核心,所有的UI交互都在这个类完成,代码如下:

public class GoodsListBehavior extends CoordinatorLayout.Behavior<GoodsListView> {
    private CoordinatorLayout parentView;
    private GoodDetailsView detailsView;
    private GoodsTitleView titleView;
    private GoodsListView goodView;
    private Context context;
    private Scroller scroller;
    private int duration=1000;
    private Handler handler;

    private int pagingTouchSlop;
    private int verticalPagingTouch;

    //商品界面的中心
    int centerGoodView;
    //商品界面离顶部的间隔
    int goodViewTop;

    public GoodsListBehavior(Context context, AttributeSet attrs){
        super(context,attrs);
        this.context=context;
        this.pagingTouchSlop=DensityUtils.dp2px(context,5);
        this.scroller=new Scroller(context);
        this.handler=new Handler();
    }

    @Override
    public boolean layoutDependsOn(@NonNull CoordinatorLayout parent, @NonNull GoodsListView child, @NonNull View dependency) {
        this.goodView=child;
        this.parentView=parent;
        if(dependency instanceof GoodsTitleView){
            titleView=(GoodsTitleView) dependency;
            return true;
        }
        if(dependency instanceof GoodDetailsView){
            detailsView=(GoodDetailsView) dependency;
            detailsView.expandBtn.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    startScroll((int)goodView.getTranslationY(),goodViewTop-parentView.getHeight());
                }
            });
            return true;
        }
        return false;
    }

    @Override
    public boolean onLayoutChild(@NonNull CoordinatorLayout parent, @NonNull GoodsListView child, int layoutDirection) {
        CoordinatorLayout.LayoutParams layoutParams=(CoordinatorLayout.LayoutParams)child.getLayoutParams();
        if(layoutParams.height==CoordinatorLayout.LayoutParams.MATCH_PARENT){
            layoutParams.height=parent.getHeight()-titleView.getHeight();
            child.setLayoutParams(layoutParams);
            goodViewTop=titleView.getHeight()+ DensityUtils.dp2px(context,160);
            child.setTranslationY(goodViewTop);
            return true;
        }
        return super.onLayoutChild(parent, child, layoutDirection);
    }

    @Override
    public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
                                       @NonNull GoodsListView child,
                                       @NonNull View directTargetChild,
                                       @NonNull View target, int axes, int type) {
        handler.removeCallbacks(flingRunnable);
        return (axes & ViewCompat.SCROLL_AXIS_VERTICAL)!=0;
    }

    @Override
    public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull GoodsListView child,
                                  @NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
        //防止左右误滑
        verticalPagingTouch+=dy;
        if(goodView.viewPager.isScrollable()&&Math.abs(verticalPagingTouch)>pagingTouchSlop){
            goodView.viewPager.setScrollable(false);
        }

        if(dy>0){
            //向上滑
            if(child.getTranslationY()<=titleView.getHeight()){
                child.setTranslationY(titleView.getHeight());
            }else{
                child.setTranslationY(child.getTranslationY()-dy);
                consumed[1]=dy;
            }
        }else{
            //向下滑
            if(((GoodsListFragment) child.getFragment().get(child.viewPager.getCurrentItem())).isScrollAble()){
                child.setTranslationY(child.getTranslationY()-dy);
            }
        }

        if(child.getTranslationY()>=goodViewTop){
            detailsView.updateView(dy);
            titleView.checkView();
        } else{
            titleView.updateView(dy);
        }

    }

    @Override
    public void onStopNestedScroll(@NonNull CoordinatorLayout parent,
                                   @NonNull GoodsListView child,
                                   @NonNull View target, int type) {
        verticalPagingTouch = 0;
        goodView.viewPager.setScrollable(true);
        centerGoodView=(parent.getHeight()+goodViewTop)/2;

        if(child.getTranslationY()>goodViewTop&&child.getTranslationY()<centerGoodView){
            //恢复
            startScroll((int)child.getTranslationY(),(int)(goodViewTop-child.getTranslationY()));
        }else if(child.getTranslationY()>centerGoodView){
            //隐藏
            startScroll((int)child.getTranslationY(),(int)(parent.getHeight()-child.getTranslationY()));
        }

    }

    @Override
    public boolean onNestedFling(@NonNull CoordinatorLayout coordinatorLayout, @NonNull GoodsListView child, @NonNull View target, float velocityX, float velocityY, boolean consumed) {
        if(velocityY<0){
            //向下
            startScroll((int)child.getTranslationY(),(int)(coordinatorLayout.getHeight()-child.getTranslationY()));
        }else{
            //向上
            if(goodView.getTranslationY()<goodViewTop){
                startScroll((int)child.getTranslationY(),(int)(titleView.getHeight()-child.getTranslationY()));
            }else{
                startScroll((int)child.getTranslationY(),(int)(goodViewTop-child.getTranslationY()));
            }
        }
        return true;
    }

    public void startScroll(int startY,int dy){
        scroller.startScroll(0,startY,0,dy,duration);
        this.handler.post(flingRunnable);
    }

    Runnable flingRunnable=new Runnable() {
        @Override
        public void run() {
            if(scroller.computeScrollOffset()){
                goodView.setTranslationY(scroller.getCurrY());
                if(goodView.getTranslationY()>=goodViewTop){
                    detailsView.updateView(scroller.getStartY()-scroller.getFinalY());
                }else{
                    titleView.updateView(scroller.getStartY()-scroller.getFinalY());
                }
                handler.post(flingRunnable);
            }
        }
    };

}

看上去代码还是有点多,首先要形成与父View的关联GoodsListBehavior必须继承GoodsListBehavior,这样子View一滑动才可以回调相应的NestedScrolling机制的一些方法,在这里我们看几个方法:

/**
* 开始滑动的时候调用一次,手松开的时候调用一次
* 返回true代表获取滑动事件,其他的scroll事件就会被触发
* coordinatorLayout
* child 使用此Behavior的View
* directTargetChild 是target或是target的parent
* target 处理滑动事件的view
* axes  垂直滚动2 横向滚动1
* type  滑动类型touch 0手指按下 1手指松开
*/
public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
                                       @NonNull GoodsListView child,
                                       @NonNull View directTargetChild,
                                       @NonNull View target, int axes, int type);
/**
* 页面滑动的时候调用
* coordinatorLayout 同上
* child 同上
* target 同上
* dxConsumed 水平滑动的实时距离
* dyConsumed 竖直滑动的实时距离
* dxUnconsumed view处于滚动状态,但是并不是由target消耗的滚动时候触发,这个是水平滚动的实时距离
* dyUnconsumed view处于滚动状态,但是并不是由target消耗的滚动时候触发,这个是竖直滚动的实时距离
* type 同上
*/
public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull GoodsListView child,@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type);


//手指松开时,调用一次,滑动停止时调用一次
public void onStopNestedScroll(@NonNull CoordinatorLayout parent,
                                   @NonNull GoodsListView child,
                                   @NonNull View target, int type);

/**
* 滑动时手指松开如果还继续滑动的时候调用一次
* coordinatorLayout 同上
* child 同上
* target 同上
* velocityX 水平加速度
* velocityY 竖直加速度
* consumed 同上 false不拦截 true则不会有惯性滑动,需要自己处理
*/
public boolean onNestedFling(@NonNull CoordinatorLayout coordinatorLayout, @NonNull GoodsListView child, @NonNull View target, float velocityX, float velocityY, boolean consumed);

是不是和我们上一讲中的NestedScrollingParent回调方法很像,其实说白了CoordinatorLayout内部还是用NestedScrolling机制实现的。因为这个方法比较常用,所以我就讲这几个方法,m没出现的暂时不讲。除了上面几个,还有如下:

/**
 * 指定依赖的View,在这里指定依赖的View之后,
 * @param parent
 * @param child      使用该Behavior的View
 * @param dependency 依赖的View
 * @return 当指定的View是我们需要的View时,返回true
 */

boolean layoutDependsOn(@NonNull CoordinatorLayout parent, @NonNull GoodsListView child, @NonNull View dependency);

确定使用Behavior的View要依赖的View的类型,在这里,我做的最多的是初始化各个View,如GoodDetailsView,GoodsTitleView,GoodsListView,CoordinatorLayout分别对应detailsView,titleView,goodView,parentView。

/**
* CoordinatorLayout绘制child的时候调用
* parent 同上
* child 同上
* CoordinatorLayout布局解析的方法 0=ltr 1=rtl,因为有些国家是从左向右显示的
**/

boolean onLayoutChild(@NonNull CoordinatorLayout parent, @NonNull GoodsListView child, int layoutDirection); 确定使用Behavior的View位置,这一步确定各个子View的初始位置,具体无非通过计算得到各个View的位置再移动,代码很简单已给。

onStartNestedScroll():当(axes & ViewCompat.SCROLL_AXIS_VERTICAL)!=0既表示竖直滑动嵌套滑动就开始了,最主要的作用就是确定滑动的方向。

onNestedPreScroll():当我们滑动时候就会不断的调用这个方法,这也是我们实现各种效果的关键,我在这里做的最主要的就是各种滑动动画效果的实现,而效果无非就是放大,缩小,透明度,View的移动等。

onStopNestedScroll():看名字就知道了,当停止滑动时调用的方法,主要是执行当滑到一般停止时要怎么恢复还是隐藏商品列表的判断

onNestedFling(): 当手指快速一划时所触发的方法,在代码中结合着Scroller,onNestedFling赋一个结束值给Scroller,Scroller会不断产生中间值直到结束为止。而我们拿到这些中间中间值进行动画处理。

这个就是各个方法的功能和职责,也是整个整个功能的骨架,共同支撑了整个交互的执行,而具体的细节请看源码。

github.com/jack921/Mei…