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

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

效果如上图【别嫌弃我】

在这之前,我们知道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

java    @Override
    public void setNestedScrollingEnabled(boolean enabled) {
        super.setNestedScrollingEnabled(enabled);
        mChildHelper.setNestedScrollingEnabled(enabled);
    }

    @Override
    public boolean isNestedScrollingEnabled() {
        return mChildHelper.isNestedScrollingEnabled();
    }

    @Override
    public boolean startNestedScroll(int axes) {
        return mChildHelper.startNestedScroll(axes);
    }

    @Override
    public void stopNestedScroll() {
        mChildHelper.stopNestedScroll();
    }

    @Override
    public boolean hasNestedScrollingParent() {
        return mChildHelper.hasNestedScrollingParent();
    }

    @Override
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {
        return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow);
    }

    @Override
    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
        return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
    }

    @Override
    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
        return mChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);
    }

    @Override
    public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
        return mChildHelper.dispatchNestedPreFling(velocityX, velocityY);
    }

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

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

java    /**
     * Start a new nested scroll for this view.
     *
     * <p>This is a delegate method. Call it from your {@link android.view.View View} subclass
     * method/{@link NestedScrollingChild} interface method with the same signature to implement
     * the standard policy.</p>
     *
     * @param axes Supported nested scroll axes.
     *             See {@link NestedScrollingChild#startNestedScroll(int)}.
     * @return true if a cooperating parent view was found and nested scrolling started successfully
     */
    public boolean startNestedScroll(int axes) {
        if (hasNestedScrollingParent()) {
            // Already in progress
            return true;
        }
        if (isNestedScrollingEnabled()) {
            ViewParent p = mView.getParent();
            View child = mView;
            while (p != null) {
                if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) {
                    mNestedScrollingParent = p;
                    ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes);
                    return true;
                }
                if (p instanceof View) {
                    child = (View) p;
                }
                p = p.getParent();
            }
        }
        return false;
    }

可以看到这里是帮你实现一些跟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 父view
startNestedScroll onStartNestedScroll、onNestedScrollAccepted
dispatchNestedPreScroll onNestedPreScroll
dispatchNestedScroll onNestedScroll
stopNestedScroll onStopNestedScroll

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

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

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

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


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

你可能感兴趣的文章


本文采用 署名-相同方式共享 3.0 中国大陆许可协议,分享、演绎需署名且使用相同方式共享。

讨论区

+0

如果子view获得处理touch事件机会的时候,父view就再也没有机会去处理这个touch事件了,直到下一次手指再按下。 这是漏洞吗?父view本来就是有机会处理触摸事件的,看不出来这算什么漏洞

菠萝菠萝蜜 · 2015年06月03日

+0
回复 菠萝菠萝蜜

对,但是子view在处理的时候 父view已经没有机会了。除非onInterceptTouchEvent处理掉或者onTouchEvent处理掉(这是在子view之前,并不能描述子view中处理的情况)

Gemini · 2015年06月03日

+0
回复 Gemini

好吧,你这个能给个demo出来吗?

菠萝菠萝蜜 · 2015年06月03日

+0

这个控件我暂时是没敢用,有时候会无法滑动,很怪异。暂时又没有时间去查就弃用了

廖子燊App · 2015年06月08日

+0
回复 廖子燊App

无法滑动?在啥情况下?我看看

Gemini · 2015年06月08日

+0
回复 Gemini

额。我是在viewpage+tablayout中的一个fragment中使用了NestedScrolling,然后就遇到有时候会划不动了

廖子燊App · 2015年06月08日

+0
回复 廖子燊App

使用情况跟我是一样吧? 我是CoordinatorLayout作为父容器,我把SwipeRefreshLayout作为NestedScrollingChild,写了一些方法用来支持。 只能建议你检查下了,我这没有你说的划不动的问题。。。

Gemini · 2015年06月08日

+0
回复 菠萝菠萝蜜

https://gist.github.com/geminiwen/9fda4ff6b84ffd3691e7#
这是可以嵌套滑动版本的SwipeRefreshLayout,搭配AppBarLayout和CoordinatorLayout使用,里面套入ListView或者其他均可。

Gemini · 2015年06月09日

+1
回复 Gemini

https://gist.github.com/geminiwen/9fda4ff6b84ffd3691e7# 无效哦
CoordinatorLayout+viewpager 在viewpager的fragment中SwipeRefreshLayout+recyclerview

yabin · 2015年06月17日

+0
回复 yabin

recycle view本身支持NestedScroll
你尝试下listview. 我没有试过用RecycleView进行组装~ 可以调试下看哪里出问题啦

Gemini · 2015年06月17日

+0
回复 Gemini

多谢,通过http://stackoverflow.com/questions/30779667/android-collapsingtoolbarl... 解决了我的冲突问题。但是滑动的顺序和我要的不一样。

期望如下:
在AppBarLayout收起的情况下,下拉时应该先滑动recyclerview 再滑动显示AppBarLayout,最后显示刷新swiperefreshlayout,还没有实现上述效果。有什么思路提醒一下。

呵呵,还有看了你的这篇分析,我也看了源码但没你理解的细,多谢分享。期待你的CoordinatorLayout分析

yabin · 2015年06月17日

+0
回复 yabin

可以直接改我代码里 SwipeRefreshLayout的onInterceptTouchEvent方法:
1、判断canScrollVertically(child) 如果为true 使用super.onInterceptTouchEvent。
2、如果child不能滑动,则使用dispatchNestedPreScroll
3、如果dispatchNestedPreScroll返回false,则使用super.onInterceptTouchEvent,否则直接返回false

Gemini · 2015年06月17日

+0
回复 yabin

CoordinatorLayout的分析在这~ http://segmentfault.com/a/1190000002888109

Gemini · 2015年06月17日

+0

你的github打不开了?

yabin · 2015年06月18日

+0
回复 yabin

https://gist.github.com/geminiwen/9fda4ff6b84ffd3691e7 我统一了下用户名,所以地址改掉了。。。

Gemini · 2015年06月18日

+0
回复 Gemini

您好,请问您github上共享的这个NestedScrollSwipeRefreshLayout怎么才能修改为不需要刷新功能,且能直接将listview嵌套进去使用呢?我试着将它直接继承ViewGroup却没有了下拉时隐藏上面TitleBar的效果

Vanish136 · 2015年06月27日

+0
回复 Vanish136

最简单的方式是使用RecycleView,ListView系统默认不支持NestedScrolling 特性,不过要自己写的话也要实现NestedScrollingChild接口 当然 你可以研究下onTouchEvent事件 尝试写写看

Gemini · 2015年06月28日

+0
回复 Gemini

谢谢您的指导,我试着直接去自定义了下listview实现NestedScrollingChild接口以及onTouchEvent触摸事件的分发,可以做到需要的效果

Vanish136 · 2015年06月28日

+0
回复 Vanish136

cool

Gemini · 2015年06月28日

+0

求教下拉刷新的样式。已给你发过gmail邮件,大神指导

Shonim · 2015年07月07日

+0
回复 Shonim

QQ: 14291050

Gemini · 2015年07月07日

+0
回复 Vanish136

求listView源码,一直弄不出来,对这些东西很头疼。。。谢谢了

wswsl · 2015年08月23日

+0
回复 wswsl

其实大部分和博主github上下拉刷新的一样,照搬都行,当时写了下觉得可以,后面发现还是有一些瑕疵不会弄,最后换掉了,建议还是使用RecycleView

Vanish136 · 2015年08月23日

+0
回复 Vanish136

嗯嗯,我觉得也有换recyclerview的必要了

wswsl · 2015年08月23日

+0

我使用 design support的时候, 一直报错
Error:(1) Attribute "title" has already been defined

dependencies {

compile 'com.android.support:support-v4:21.0.3'
compile 'com.android.support:design:22.2.1'
compile fileTree(dir: 'libs', exclude: 'android-support-*.jar', include: '*.jar')

}
请问你们有遇到过这种问题码

tlrk · 2015年09月06日

+0
回复 tlrk

没有遇到过~但是你使用了Design Support之后就不需要compile 'com.android.support:support-v4:21.0.3'

Gemini · 2015年09月06日

+0
回复 Gemini

目前改到哪个地址了,我使用appBar作标题。用了SwipeRefrsh +NestedScrollView 嵌套ViewPager 然后嵌套RecyclerView。滑动控制就混乱了,不太会做。

andyboyce · 2015年11月30日

+0
回复 Gemini

onInterceptTouchEvent也是可以处理子View中的情况的。
子View处理onTouchEvent的过程中,其父View的onInterceptTouchEvent仍然会被调用。可以在合适的情况下拦截掉。

passerbywhu · 2015年12月07日

+0

Gemini 请问效果图的demo有源码吗;或者描述一下Toolbar的隐藏的处理方法

liuyansp · 2月4日

+0

为什么我在NestedScrollView中放webview控件时,webview的内容却不能显示出来,上滑动画也没有,webview换成其他控件可以正常显示

David_Jerry · 2月22日

+0

测试新接口

Gemini · 2月22日

+0

@Gemini 麻烦问下,我在使用appbarlayout时候会报一个这样的错java.lang.ClassNotFoundException: Didn't find class "android.support.design.widget.AppBarLayout" on path: DexPathList[[zip file "/system/framework/cloud-common.jar", zip file "/system/framework/zxing.jar", zip file "/system/framework/android-support-v13.jar", zip file "/system/framework/protobuf.jar", zip file "/system/priv-app/Settings/Settings.apk"],nativeLibraryDirectories=[/system/priv-app/Settings/lib/arm, /vendor/lib, /system/lib]]
能帮我看下十怎么回事吗?

gaochong · 5月2日

+0

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

关于这句话,我有点疑问。
当子View子view获得处理touch事件机会的时候,父View在onInterceptTouchEvent将其中断,这样会给子View发送一个Cancel类型的事件,接下来父View不就可以继续处理Touch事件了吗?

慢慢学 · 6月12日

+0

如果子view获得处理touch事件机会的时候,父view就再也没有机会去处理这个touch事件了,直到下一次手指再按下.
这句话说的不对,如果子View的dispatchTouchEvent返回false,那么父view的mFirstTouchTarget会为空,然后会调用父view的dispatchTouchEvent

chefish12306 · 8月3日

+0
回复 chefish12306

不应该吧,父view的dispatchTouchEvent是先调用的呀

Gemini · 8月3日

+0
回复 Gemini

父view不拦截,那就尝试给子view处理,子view如果不处理,还是会抛给父view,如果父view还不处理,那继续往上抛,直到ViewRootImpl。可以拿个例子试一下,child的dispatchTouchEvent返回false,那父view的onTouchEvent会被调用

chefish12306 · 8月4日

+0

我补充一下,如果是down事件,子不处理,会丢给父的onTouchEvent处理。而如果子处理了down事件,下一个move事件到来子不处理,那父也不会处理,此事件丢弃

chefish12306 · 8月4日

+0

不嫌弃,作为菜鸟希望你能把完整的例子放出来

sandeda · 9月18日

展开评论

本文隶属于专栏

Gemini @ SegmentFault

Gemini's Blog

Gemini Gemini

作者

SegmentFault

一起探索更多未知

下载 App