Android嵌套滑动:NestedScrollingChild使用详解

  发布日期:   2018-08-20
  最新修改:   2020-04-07
  阅读次数:   43 次

  • 如果你希望内部嵌套的View可以进行嵌套事件的分发以便与外部的父View配合,此时可以实现此接口,

  • 实现此接口的类应该创建NestedScrollingChildHelper的最终实例作为字段,并将任何View方法委托给相同签名的NestedScrollingChildHelper方法。 因为 Helper 类中已经实现好了 Child 和 Parent 交互的逻辑。原来的 View 的处理 Touch 事件,并实现滑动的逻辑大体上不需要改变

  • 调用嵌套滚动功能的视图应始终从相关的ViewCompat,ViewGroupCompat或ViewParentCompat兼容性填充静态方法执行此操作。 这确保了与Android 5.0 Lollipop和更新版本上的嵌套滚动视图的互操作性。

方法
返回值 方法名称 描述
abstract boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) 分发一个fling事件给支持嵌套滚动的父类,该方法通常被使用去标识当前的子View已经通过一定的条件检测到一个fling事件,通常情况下此时touch事件已经结束并且在滚动的方向此时存在一个符合或者超过最小fling的速度,此时子View可以将fling分发给父view处理,父View可以消费此事件,也可以观察子View的处理情况;如果子View消费了,consumed为true,反之为false,velocityX为横向的速度,velocityY为纵向的速度
abstract boolean dispatchNestedPreFling(float velocityX, float velocityY) 在当前View处理该fling事件之前将该fling事件转发给支持嵌套滚动的父View,如果返回true则父View已经消费了该fling事件,子View不应该再做处理
abstract boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) 在此View消耗任何事件之前,调度该方法先询问你的父View是否需要滑动,嵌套的预滚动事件是嵌套滚动事件触摸拦截的触摸。 dispatchNestedPreScroll为嵌套滚动操作中的父视图提供了在子视图使用之前使用部分或全部滚动操作的机会。
abstract boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) 在当前View处理完本次的滑动操作后,再次询问父View是否需要滑动,支持嵌套滚动的视图实现应调用此方法将有关正在进行的滚动的信息报告给当前嵌套滚动父级。 如果嵌套滚动当前未在进行中,或者未对此视图启用嵌套滚动,则此方法不执行任何操作。 兼容的View实现也应该在使用scroll事件的一个组件之前调用dispatchNestedPreScroll。
abstract boolean hasNestedScrollingParent() 是否存在有支持内部嵌套的父View,true表示存在
abstract boolean isNestedScrollingEnabled() 如果当前view的内部嵌套滚动式开启的,则返回true
abstract void setNestedScrollingEnabled(boolean enabled) 开启或者关闭当前view的嵌套滚动
abstract boolean startNestedScroll(int axes) 沿给定轴开始嵌套滚动操作。如果你需要进行滚动了,此时应该调用该事件告诉父View你要开始滚动了
abstract void stopNestedScroll() 停止正在进行的嵌套滚动。如果你要停止滚动了,调用该方法告诉父类
基本原理
  • 如果要准备开始滑动了,需要告诉 Parent,你要准备进入滑动状态了,调用 startNestedScroll()。你在滑动之前,先问一下你的 Parent 是否需要滑动,也就是调用dispatchNestedPreScroll()。如果父类滑动了一定距离,你需要重新计算一下父类滑动后剩下给你的滑动距离余量。然后,你自己进行余下的滑动。最后,如果滑动距离还有剩余,你就再问一下,Parent 是否需要在继续滑动你剩下的距离,也就是调用 dispatchNestedScroll()。
图解
  • A状态为刚触发滚动事件的时候
  • B状态为当前View正在处理滚动事件
  • C状态为当前View已经处理完滚动事件
sequenceDiagram participant A participant B participant C A->>A: 1、startNestedScroll(int axes)通知parent你已经进入滚动状态,axes为滑动的轴类型变量 A->>A:2、dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow)询问parent是否需要消耗当前的滚动事件,如果parent消耗了,你需要根据consumed重新计算parent给你的剩余的滑动距离 A->>B:3、获得自己可滑动的距离 B->>B:4、处理滑动操作 B->>C:5、已处理完滑动操作 C->>C:6、dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow)自己处理完滑动后再次询问parent是否需要滑动 C->>C:7、stopNestedScroll()通知parent本次滚动事件到此结束
  • fling事件类推
NestedScrollingChildHelper
  • 上面图解中我们梳理了嵌套滑动机制中child将滑动信息发送给parent的大体流程。

  • 实现过NestedScrollingChild 接口的小伙伴都知道,Google相当的贴心,大部分核心的工作都帮我们实现了,我们只需要在响应的接口调用NestedScrollingChildHelper中相同签名的函数即可,然后在监听滑动事件的回调中调用一下对应接口即可。

  • 为了更好地理解NestedScrolingChild的工作工程,接下来我们将浅析NestedScrollingChildHelper这个类的具体实现。

  • 我们的分析过程还是会根据上面的图解来进行切入。

  • 1、startNestedScroll(int axes):在child进入滑动状态时,通知parent此时的状态,startNestedScroll(int axes)内部直接调用的是NestedScrollingChildHelper响应签名的函数,我们来看下NestedScrollingChildHelper.startNestedScroll(int axes)

     public void setNestedScrollingEnabled(boolean enabled) {
           //此处不是很理解,为何原来是开启状态,后面set值时要先停止内部滚动,而不先判断enable为false时再停止呢?
           //虽然如果此时view不处于嵌套滑动状态,调用该方法也不影响
            if (this.mIsNestedScrollingEnabled) {
                ViewCompat.stopNestedScroll(this.mView);
            }
    
            this.mIsNestedScrollingEnabled = enabled;
        }
    
     public boolean isNestedScrollingEnabled() {
            return this.mIsNestedScrollingEnabled;
        }
    
     public boolean hasNestedScrollingParent() {
        return this.mNestedScrollingParent != null;
      }
    
     public boolean startNestedScroll(int axes) {
        //从上面可以看到起直接返回mNestedScrollingParent是否是null,
        //作用是保证即使你多次调用startNestedScroll也只会通知一次parent,
        //当你调用了一次startNestedScroll后,mNestedScrollingParent是不为null的
        if (this.hasNestedScrollingParent()) {
            return true;
        } else {
    
            //判断全套滚动是否开启
            if (this.isNestedScrollingEnabled()) {
    
                //获取child的parent
                ViewParent p = this.mView.getParent();
    
                //遍历child的所有parent
                for(View child = this.mView; p != null; p = p.getParent()) {
    
                     //这点很关键,如果parent中在onStartNestedScroll中返回ture,就不会继续向上寻找其他parent,
                     //同时,也只有onStartNestedScroll中返回true时,onNestedScrollAccepted才会被调用,
                     //这个在下一篇讲解NestedScrollingParent中会讲到
                    if (ViewParentCompat.onStartNestedScroll(p, child, this.mView, axes)) {
    
                        //parent确定消费此滑动事件后,就会给mnestedScrollingParent赋值,
                        //在stopNestedScroll中会将其重置为null
                        //这样就会保证了一个嵌套滑动事件的传递在startNestedScroll到stopNestedScroll期间只会与
                        //其中一个parent进行交互,避免错误多次调用startNestScroll所引发的问题
                        this.mNestedScrollingParent = p;
    
                        ViewParentCompat.onNestedScrollAccepted(p, child, this.mView, axes);
                        return true;
                    }
    
                    //若parent未消费该滑动事件,则会走这一步,然后继续调用p.getParent向上寻找新的parent,
                    //这就是嵌套滑动时并未要求其必须是直接child的原因
                    if (p instanceof View) {
                        child = (View)p;
                    }
                }
            }
    
            return false;
        }
    }
  • 之后child在滑动前便会通知parent,其调用的NestedScrollingChildHelper对应的方法是:

     public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
    
           //同样,先做一下判断,如果未开启或者在startNestedScroll中未找到符合要求的parent,后面的代码也就没必要执行了
            if (this.isNestedScrollingEnabled() && this.mNestedScrollingParent != null) {
    
                 //如果dx 或者dy其中一个不为0,也就是滑动了,则执行下面代码
                if (dx != 0 || dy != 0) {
    
                    //声明两个变量来存储当前child相对于parent的x和y的值
                    int startX = 0;
                    int startY = 0;
                    //如果你想在执行完dispatchNestedPreScroll后想知道自己x和y相对于parent的值时多少,
                    //那么可以传递一个offsetInWindow对象进来进行获取
                    if (offsetInWindow != null) {
                        //在分发滑动数据给parent前先获取一次自己相对于parent的x,y值
                        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;
    
                    //这里会调用onNestedPreScroll,将对应的参数传递给parent
                    ViewParentCompat.onNestedPreScroll(this.mNestedScrollingParent, this.mView, dx, dy, consumed);
    
                    //parent的onNestedPreScroll执行后再次获取此时的x,y的值
                    if (offsetInWindow != null) {
                        this.mView.getLocationInWindow(offsetInWindow);
    
                        //通过计算,如果值大于0,parent向上滑了,反之则parent向下滑了
                        offsetInWindow[0] -= startX;
                        offsetInWindow[1] -= startY;
                    }
                    //返回true,则代表parent消费了dx或者dy的数值
                    return consumed[0] != 0 || consumed[1] != 0;
                }
    
                if (offsetInWindow != null) {
                    offsetInWindow[0] = 0;
                    offsetInWindow[1] = 0;
                }
            }
    
            return false;
        }
  • child滑动后再次通知parent

     public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {
    
         //同样先判断
         if (this.isNestedScrollingEnabled() && this.mNestedScrollingParent != null) {
             if (dxConsumed != 0 || dyConsumed != 0 || dxUnconsumed != 0 || dyUnconsumed != 0) {
                 int startX = 0;
                 int startY = 0;
                 if (offsetInWindow != null) {
                     this.mView.getLocationInWindow(offsetInWindow);
                     startX = offsetInWindow[0];
                     startY = offsetInWindow[1];
                 }
    
                 //将自己消费了的dx 和dy以及未消费的dx 和dy传递给parent,以便parent继续消费dx 和dy
                 ViewParentCompat.onNestedScroll(this.mNestedScrollingParent, this.mView, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed);
                 if (offsetInWindow != null) {
                     this.mView.getLocationInWindow(offsetInWindow);
                     offsetInWindow[0] -= startX;
                     offsetInWindow[1] -= startY;
                 }
    
                 return true;
             }
    
             if (offsetInWindow != null) {
                 offsetInWindow[0] = 0;
                 offsetInWindow[1] = 0;
             }
         }
    
         return false;
     }
  • 最后stopNestedScroll

     public void stopNestedScroll() {
            if (this.mNestedScrollingParent != null) {
                ViewParentCompat.onStopNestedScroll(this.mNestedScrollingParent, this.mView);
                //重置为null
                this.mNestedScrollingParent = null;
            }
    
        }
总结
  • 至此,我们学习了NestedScrollingChild接口接口函数的作用以及整体的交互流程,后续将继续学习NestedScrollingParent相关知识点

   转载规则

《Android嵌套滑动:NestedScrollingChild使用详解字》GajAngels 采用 知识共享署名-非商业性使用 4.0 国际许可协议 进行许可。