NestedScrolling:文章详情页的实现

如果你的APP和新闻媒体相关,那么你肯定有类似于头条文章页那样,上面是网页显示的文章,下面是文章相关的列表和评论列表,NestedWebViewRecyclerViewGroup 见名知意就是为这种需求而生,NestedWebViewRecyclerViewGroup 基于NestedScrolling机制而实现,是 WebView 和 RecyclerView 的嵌套控件,用于经典的上面是 WebView 显示的文章,下面是 RecyclerView 显示的列表的文章详情页结构中,可以实现 WebView 和 RecyclerView 的无缝滑动切换,也提供 WebView 显示区域和 RecyclerView 显示区域的上下显示切换。NestedWebViewRecyclerViewGroup 的高度为当前屏幕上该控件的可见高度,滑动流畅,占用内存小。本篇学习下NestedScrolling机制,顺便简单讲下此控件的实现原理,相信学完本篇以后,你对各种嵌套滑动也会有自己的实现思路!

Github地址:NestedWebViewRecyclerViewGroup

image

NestedScrolling介绍

NestedScrolling机制是Android 5.0 以后添加的嵌套滑动机制,用于实现各种复杂的嵌套滑动。嵌套滑动归根到底需要解决的是三个问题:

  1. 子View需要滑动时,需要告知父View,当前的滑动由子View完成,父View不动
  2. 父View需要滑动时,需要告知子View,当前的滑动由父View完成,子View不动
  3. 父View或子View自己的滑动完成后,将未结束的滑动事件交给子View或父View继续处理

以文章页的控件 NestedWebViewRecyclerViewGroup 为例,如上图所示,当WebView滑动到底部时,将滑动事件传递给父View让父继续滑动,当父View滑动到RecyclerView区域显示时,将滑动事件传递给RecyclerView,让RecyclerView继续滑动,反向滑动一样的逻辑,如此便完成了连续的滑动。

那么我们怎么用NestedScrolling实现一套自己的嵌套滑动呢,我们首先需要了解一下NestedScrolling机制相关的一些类:

  1. NestedScrollingParent2:父View需要实现的接口,用于接收子View的滑动事件和滑动时的参数
  2. NestedScrollingParentHelper:处理父View滑动的辅助类
  3. NestedScrollingChild2:子View需要实现的接口,用于向父View回调各种滑动事件和滑动参数
  4. NestedScrollingChildHelper:处理子View滑动的辅助类

PS: NestedScrollingParent2 继承 NestedScrollingParent 接口,主要为了解决连续滑动的问题 参考链接1参考链接2

NestedScrolling滑动机制

NestedScrolling滑动机制中,子View的方法和父View的方法有一一对应的回调关系,比如:

子View的方法父View的方法方法说明
startNestedScrollonStartNestedScroll onNestedScrollAccepted当前方向滑动是否可用的判断与设置
stopNestedScrollonStopNestedScroll停止滑动的回调
dispatchNestedScrollonNestedScroll滑动时的回调
dispatchNestedPreScrollonNestedPreScroll滑动之前的回调
dispatchNestedFlingonNestedFlingfling时的回调
dispatchNestedPreFlingonNestedPreFlingfling之前的回调

虽然NestedScrolling能为我们解决嵌套滑动中各种复杂的触摸回调,但是对于触摸事件的消费和嵌套滑动中的各个View的滑动规则需要我们自己完成,这里引用 这篇英文文章中对嵌套滑动的回顾:

  1. ? User touches screen. 当触发 ACTION_DOWN 事件的时候,view调用每一个父view的startNestedScroll(),直到其中一个返回true。返回true表示那个parent对这个滚动感兴趣。如果没有parent返回true,那么嵌套滚动被取消,view执行它自己的操作。后面我们都假设有一个parent返回了true。

  2. ? User moves finger. 当 ACTION_MOVE 事件触发的时候,view将调用dispatchNestedPreScroll() 把事件发送给parent,让它消费部分/全部(或者不消费)用户手指滑动的距离。如果parent没有消费完所有的移动,view将自己接着消费并发送 dispatchNestedScroll()告知消费了多少。

  3. ? User lifts finger. 当触发 ACTION_UP 的时候,view计算是否需要继续移动。如果余下的速度足够大,它将调用 dispatchNestedPreFling() 让parent继续消费velocity。如果parent返回true并且消费了它,view的工作就完成了。否则view将开始划动并立即调用 dispatchNestedFling()。view将立即调用 stopNestedScroll() 来将嵌套滚动标记为结束,即使view自己实际上还处于fling中。

了解了嵌套的基本原理后,我们以嵌套滑动需要解决的三个问题为例,了解一下基于NestedScrolling的嵌套滑动机制,先来看一个例子:

子View的滑动处理:

    private final int[] mScrollConsumed = new int[2];
    private final int[] mScrollOffset = new int[2];
    
    //子View的onTouchEvent方法的ACTION_MOVE事件
    case MotionEvent.ACTION_MOVE:
        if (startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL) &&
                dispatchNestedPreScroll(0, -dy, mScrollConsumed, mScrollOffset)) {
            //父View已处理滑动
        }else{
            //需要子View自己处理滑动
        }

父View的滑动处理:

    //父View的方法,设置在垂直方向上启用
    @Override
    public boolean **onStartNestedScroll**(@NonNull View child, @NonNull View target, int axes, int type) {
        return (axes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
    }
    
    //父View的滑动前来自子View的回调,父View根据条件判断是否需要处理滑动
    @Override
    public void onNestedPreScroll(@NonNull View view, int dx, int dy, @NonNull int[] ints, int type) {
        //如果父View需要处理滑动,那么可以将该滑动方向上的偏移量赋值给方法中的数组参数
        if(父View需要在垂直方向上滑动){
            ints[1] = dy;
        }else{
            ...
        }
    }

以上的代码中,我们在子View手势滑动时,调用startNestedScroll方法开始垂直方向的滑动,调用dispatchNestedPreScroll方法处理滑动相关的方法和参数,各个参数的含义如下:

  1. dy:子View在垂直方向上的滑动偏移
  2. mScrollConsumed:子View传递给父View用于接收本次滑动时父View的滑动偏移,如果父View需要自己处理此刻此方向的滑动,则可以将滑动偏移赋值给父View的onNestedPreScroll方法中的consumed参数,
  3. mScrollOffset:父View的onNestedPreScroll执行方法前后,子View在Window上的偏移量

从以下的dispatchNestedPreScroll源码中也可以了解到父View中为consumed参数赋值后,则该方法返回false,表示此次滑动由父View完成

    public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed, @Nullable int[] offsetInWindow, int type) {
        if (this.isNestedScrollingEnabled()) {
            ViewParent parent = this.getNestedScrollingParentForType(type);
            if (parent == null) {
                return false;
            }

            if (dx != 0 || dy != 0) {
                int startX = 0;
                int startY = 0;
                if (offsetInWindow != null) {
                    this.mView.getLocationInWindow(offsetInWindow);
                    startX = offsetInWindow[0];
                    startY = offsetInWindow[1];
                }

                if (consumed == null) {
                    if (this.mTempNestedScrollConsumed == null) {
                        this.mTempNestedScrollConsumed = new int[2];
                    }

                    consumed = this.mTempNestedScrollConsumed;
                }

                consumed[0] = 0;
                consumed[1] = 0;
                //调用View的onNestedPreScroll方法在滑动之前处理判断条件
                ViewParentCompat.onNestedPreScroll(parent, this.mView, dx, dy, consumed, type);
                if (offsetInWindow != null) {
                    this.mView.getLocationInWindow(offsetInWindow);
                    offsetInWindow[0] -= startX;
                    offsetInWindow[1] -= startY;
                }
                //注意这里当父View为consumed赋值后,表示此刻的滑动由父View完成,返回false
                return consumed[0] != 0 || consumed[1] != 0;
            }

            if (offsetInWindow != null) {
                offsetInWindow[0] = 0;
                offsetInWindow[1] = 0;
            }
        }

        return false;
    }

我们结合上面的例子以及dispatchNestedPreScroll的源码可知

  1. 当子View滑动时需要同时调用startNestedScroll方法和dispatchNestedPreScroll方法
  2. 当本次滑动需要父View处理时,则可以为父View中onNestedPreScroll的参数consumed中对应的方向赋值,此时dispatchNestedPreScroll方法返回false
  3. 当本次滑动需要子View处理时,不对consumed参数赋值,此时dispatchNestedPreScroll方法返回true,子View自己处理本地滑动
  4. 不对consumed赋值时,表示父View不处理本次滑动,相应的也不会调用父View的onNestedScroll方法
  5. 相应的,如果我们想处理fling事件,子View也必须同时调用startNestedScroll和dispatchNestedPreFling方法,这里可能有人不理解为什么fling事件还需要调用startNestedScroll方法,其实在startNestedScroll方法中会遍历寻找支持NestedScrolling滑动的父控件,可以看对应的源码了解下

总结

以上就是NestedScrolling滑动机制的原理,写个例子看过源码之后,觉得还是相对比较简单的,其实NestedScrolling滑动机制对于嵌套滑动的处理更加类似于滑动辅助工具,更多的滑动处理和边界条件的判断需要我们自己完成。NestedWebViewRecyclerViewGroup的使用可以看github的相关介绍,欢迎提issue和建议!

参考
  1. [译]对design库中AppBarLayout嵌套滚动问题的修复
  2. Android8.0对于CoordinatorLayout、RecyclerView 精准fling的优化
  3. Material Design系列教程(5) - NestedScrollView

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值