Android嵌套滑动机制(NestedScrolling)

来源:互联网 发布:淘宝pc 编辑:程序博客网 时间:2024/05/09 00:23

来源 http://segmentfault.com/a/1190000002873657

编辑推荐:稀土掘金,这是一个针对技术开发者的一个应用,你可以在掘金上获取最新最优质的技术干货,不仅仅是Android知识、前端、后端以至于产品和设计都有涉猎,想成为全栈工程师的朋友不要错过!

转载这篇文章是因为NestedScrolling的文档太少,虽然本文也是轻描淡写,但是也是最详细的了。不过文档少也证明了NestedScrolling的功能很容易用其他方法替代,所以大家看看就好,不必太细究。

Android 在发布 Lollipop版本之后,为了更好的用户体验,Google为Android的滑动机制提供了NestedScrolling特性

NestedScrolling的特性可以体现在哪里呢?
比如你使用了Toolbar,下面一个ScrollView,向上滚动隐藏Toolbar,向下滚动显示Toolbar,这里在逻辑上就是一个NestedScrolling —— 因为你在滚动整个Toolbar在内的View的过程中,又嵌套滚动了里面的ScrollView

bVmdFl.gif

效果如上图【别嫌弃我】

在这之前,我们知道AndroidTouch事件的分发是有自己一套机制的。主要是有是三个函数:
dispatchTouchEventonInterceptTouchEventonTouchEvent

这种分发机制有一个漏洞:

如果子view获得处理touch事件机会的时候,父view就再也没有机会去处理这个touch事件了,直到下一次手指再按下。

也就是说,我们在滑动子View的时候,如果子View对这个滑动事件不想要处理的时候,只能抛弃这个touch事件,而不会把这些传给父view去处理。

  注:本站这种观点值得商量。

但是Google新的NestedScrolling机制就很好的解决了这个问题。
我们看看如何实现这个NestedScrolling,首先有几个类(接口)我们需要关注一下

NestedScrollingChild
NestedScrollingParent
NestedScrollingChildHelper
NestedScrollingParentHelper

以上四个类都在support-v4包中提供,Lollipop的View默认实现了几种方法。
实现接口很简单,这边我暂时用到了NestedScrollingChild系列的方法(因为Parent是support-design提供的CoordinatorLayout

  1.    @Override
  2.     public void setNestedScrollingEnabled(boolean enabled) {
  3.         super.setNestedScrollingEnabled(enabled);
  4.         mChildHelper.setNestedScrollingEnabled(enabled);
  5.     }
  6.  
  7.     @Override
  8.     public boolean isNestedScrollingEnabled() {
  9.         return mChildHelper.isNestedScrollingEnabled();
  10.     }
  11.  
  12.     @Override
  13.     public boolean startNestedScroll(int axes) {
  14.         return mChildHelper.startNestedScroll(axes);
  15.     }
  16.  
  17.     @Override
  18.     public void stopNestedScroll() {
  19.         mChildHelper.stopNestedScroll();
  20.     }
  21.  
  22.     @Override
  23.     public boolean hasNestedScrollingParent() {
  24.         return mChildHelper.hasNestedScrollingParent();
  25.     }
  26.  
  27.     @Override
  28.     public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {
  29.         return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow);
  30.     }
  31.  
  32.     @Override
  33.     public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
  34.         return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
  35.     }
  36.  
  37.     @Override
  38.     public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
  39.         return mChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);
  40.     }
  41.  
  42.     @Override
  43.     public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
  44.         return mChildHelper.dispatchNestedPreFling(velocityX, velocityY);
  45.     }

对,简单的话你就这么实现就好了。

这些接口都是我们在需要的时候自己调用的。childHelper干了些什么事呢?,看一下startNestedScroll方法

  1.     /**
  2.      * Start a new nested scroll for this view.
  3.      *
  4.      * <p>This is a delegate method. Call it from your {@link android.view.View View} subclass
  5.      * method/{@link NestedScrollingChild} interface method with the same signature to implement
  6.      * the standard policy.</p>
  7.      *
  8.      * @param axes Supported nested scroll axes.
  9.      *             See {@link NestedScrollingChild#startNestedScroll(int)}.
  10.      * @return true if a cooperating parent view was found and nested scrolling started successfully
  11.      */
  12.     public boolean startNestedScroll(int axes) {
  13.         if (hasNestedScrollingParent()) {
  14.             // Already in progress
  15.             return true;
  16.         }
  17.         if (isNestedScrollingEnabled()) {
  18.             ViewParent p = mView.getParent();
  19.             View child = mView;
  20.             while (!= null) {
  21.                 if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) {
  22.                     mNestedScrollingParent = p;
  23.                     ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes);
  24.                     return true;
  25.                 }
  26.                 if (instanceof View) {
  27.                     child = (View) p;
  28.                 }
  29.                 p = p.getParent();
  30.             }
  31.         }
  32.         return false;
  33.     }

可以看到这里是帮你实现一些跟NestedScrollingParent交互的一些方法。
ViewParentCompat是一个和父view交互的兼容类,它会判断api version,如果在Lollipop以上,就是用view自带的方法,否则判断是否实现了NestedScrollingParent接口,去调用接口的方法。

那么具体我们怎么使用这一套机制呢?比如子View这时候我需要通知父view告诉它我有一个嵌套的touch事件需要我们共同处理。那么针对一个只包含scroll交互,它整个工作流是这样的:

一、startNestedScroll

首先子view需要开启整个流程(内部主要是找到合适的能接受nestedScroll的parent),通知父View,我要和你配合处理TouchEvent

二、dispatchNestedPreScroll

在子View的onInterceptTouchEvent或者onTouch中(一般在 MontionEvent.ACTION_MOVE事件里),调用该方法通知父View滑动的距离。该方法的第三第四个参数返回父view消费掉的 scroll长度和子View的窗体偏移量。如果这个scroll没有被消费完,则子view进行处理剩下的一些距离,由于窗体进行了移动,如果你记录了手指最后的位置,需要根据第四个参数offsetInWindow计算偏移量,才能保证下一次的touch事件的计算是正确的。
如果父view接受了它的滚动参数,进行了部分消费,则这个函数返回true,否则为false。
这个函数一般在子view处理scroll前调用。

三、dispatchNestedScroll

向父view汇报滚动情况,包括子view消费的部分和子view没有消费的部分。
如果父view接受了它的滚动参数,进行了部分消费,则这个函数返回true,否则为false。
这个函数一般在子view处理scroll后调用。

四、stopNestedScroll

结束整个流程。

整个对应流程是这样

子view父viewstartNestedScrollonStartNestedScroll、onNestedScrollAccepteddispatchNestedPreScrollonNestedPreScrolldispatchNestedScrollonNestedScrollstopNestedScrollonStopNestedScroll

一般是子view发起调用,父view接受回调。

我们最需要关注的是dispatchNestedPreScroll中的consumed参数。

  1. public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) ;

它是一个int型的数组,长度为2,第一个元素是父view消费的x方向的滚动距离;第二个元素是父view消费的y方向的滚动距离,如果这两个值不为0,则子view需要对滚动的量进行一些修正。正因为有了这个参数,使得我们处理滚动事件的时候,思路更加清晰,不会像以前一样被一堆的滚动参数搞混。


对NestedScroll的介绍暂时到这里,下一次将讲一下CoordinatorLayout的使用(其中让人较难理解的Behavior对象),以及在SegmentFault Android客户端中的实践。谢谢支持。

阅读全文
0 0
原创粉丝点击