NestedScrolling机制之CoordinatorLayout.Behavior实战

在上一讲中我们讲了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会不断产生中间值直到结束为止。而我们拿到这些中间中间值进行动画处理。

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

https://github.com/jack921/MeiTuanGoodsDetails

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343

推荐阅读更多精彩内容