详解Android中的NestedScrolling机制带你玩转嵌套滑动
一、概述
android在support.v4包中为大家提供了两个非常神奇的类:
- nestedscrollingparent
- nestedscrollingchild
如果你从未听说过这两个类,没关系,听我慢慢介绍,你就明白这两个类可以用来干嘛了。相信大家都见识过或者使用过coordinatorlayout,通过这个类可以非常便利的帮助我们完成一些炫丽的效果,例如下面这样的:
这样的效果就非常适合使用nestedscrolling机制去完成,并且coordinatorlayout背后其实也是利用着这套机制,so,我相信你已经明白这套机制可以用来干嘛了。
但是,我相信你还有个问题
- 这个机制相比传统的自定义viewgroup事件分发处理有什么优越的地方吗?
恩,我们简单分析下:
按照上图:
假设我们按照传统的事件分发去理解,首先我们滑动的是下面的内容区域,而移动却是外部的viewgroup在移动,所以按照传统的方式,肯定是外部的parent拦截了内部的child的事件;但是,上述效果图,当parent滑动到一定程度时,child又开始滑动了,中间整个过程是没有间断的。从正常的事件分发(不手动调用分发事件,不手动去发出事件)角度去做是不可能的,因为当parent拦截之后,是没有办法再把事件交给child的,事件分发,对于拦截,相当于一锤子买卖,只要拦截了,当前手势接下来的事件都会交给parent(拦截者)来处理。
但是nestedscrolling机制来处理这个事情就很好办,所以对这个机制进行深入学习,一来有助于我们编写嵌套滑动时一些特殊的效果;二来是我为了对coordinatorlayout做分析的铺垫~~~
ps:具体在哪个v4版本中添加的,就不去深究了,如果你的v4中没有上述两个类,升级下你的v4版本。nestedscrolling机制这个词,个人称呼,不清楚官方有没有这么叫,勿深究。
二、预期效果
当然讲解这两个类,肯定要有案例的支撑,不然太过于空洞了。好在,我这里有个非常好的案例可以来描述:
很久以前,我写过这样一篇文章:
完全按照传统的方式去编写的,而且为了连续滑动,做了一些非常特殊处理,比如手动去分发down事件类的,有兴趣可以阅读下。
效果图是这样的:
今天我们就利用这个效果,作为nestedsroll机制的案例,最后我们还会简单分析一下源码,其实源码还是比较简单的~~
ps:coordinatorlayout可以很方便实现该效果,后续的文章也会对coordinatelayout做一些分析。
三、实现
上述效果图,分为3部分:顶部布局;中间的viewpager指示器;以及底部的recyclerview;
recyclerview其实就是nestedsrollingchild的实现类,所以本例主要的角色是去实现nestedscrollingparent.
(1)布局文件
首先预览下布局文件,脑子里面有个大致的布局:
<com.zhy.view.stickynavlayout xmlns:tools="http://schemas.android.com/tools" xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" > <relativelayout android:id="@id/id_stickynavlayout_topview" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="#4400ff00" > <textview android:layout_width="match_parent" android:layout_height="256dp" android:gravity="center" android:text="软件介绍" android:textsize="30sp" android:textstyle="bold" /> </relativelayout> <com.zhy.view.simpleviewpagerindicator android:id="@id/id_stickynavlayout_indicator" android:layout_width="match_parent" android:layout_height="50dp" android:background="#ffffffff" > </com.zhy.view.simpleviewpagerindicator> <android.support.v4.view.viewpager android:id="@id/id_stickynavlayout_viewpager" android:layout_width="match_parent" android:layout_height="match_parent" > </android.support.v4.view.viewpager> </com.zhy.view.stickynavlayout>
stickynavlayout是直接继承自linearlayout的,并且设置的是orientation="vertical",所以直观的就是控件按顺序纵向排列,至于测量需要做一些特殊的处理,因为不是本文的重点,可以自己查看源码,或者上面提到的文章。
(2) 实现nestedscrollingparent
nestedscrollingparent是一个接口,实现它需要实现如下方法:
public boolean onstartnestedscroll(view child, view target, int nestedscrollaxes); public void onnestedscrollaccepted(view child, view target, int nestedscrollaxes); public void onnestedscroll(view target, int dxconsumed, int dyconsumed, int dxunconsumed, int dyunconsumed); public void onnestedprescroll(view target, int dx, int dy, int[] consumed); public boolean onnestedfling(view target, float velocityx, float velocityy, boolean consumed); public boolean onnestedprefling(view target, float velocityx, float velocityy); public int getnestedscrollaxes();
在写具体的实现前,先对需要用到的上述方法做一下简单的介绍:
- onstartnestedscroll该方法,一定要按照自己的需求返回true,该方法决定了当前控件是否能接收到其内部view(非并非是直接子view)滑动时的参数;假设你只涉及到纵向滑动,这里可以根据nestedscrollaxes这个参数,进行纵向判断。
- onnestedprescroll该方法的会传入内部view移动的dx,dy,如果你需要消耗一定的dx,dy,就通过最后一个参数consumed进行指定,例如我要消耗一半的dy,就可以写consumed[1]=dy/2
- onnestedfling你可以捕获对内部view的fling事件,如果return true则表示拦截掉内部view的事件。
主要关注的就是这三个方法~
这里内部view表示不一定非要是直接子view,只要是内部view即可。
下面看一下我们具体的实现:
public class stickynavlayout extends linearlayout implements nestedscrollingparent { @override public boolean onstartnestedscroll(view child, view target, int nestedscrollaxes) { return (nestedscrollaxes & viewcompat.scroll_axis_vertical) != 0; } @override public void onnestedprescroll(view target, int dx, int dy, int[] consumed) { boolean hiddentop = dy > 0 && getscrolly() < mtopviewheight; boolean showtop = dy < 0 && getscrolly() > 0 && !viewcompat.canscrollvertically(target, -1); if (hiddentop || showtop) { scrollby(0, dy); consumed[1] = dy; } } @override public boolean onnestedprefling(view target, float velocityx, float velocityy) { if (getscrolly() >= mtopviewheight) return false; fling((int) velocityy); return true; } }
- onstartnestedscroll中,我们判断了如果是纵向返回true,这个一般是需要内部的view去传入的,你要是不确定,或者担心内部view编写的不规范,你可以直接return true;
- onnestedprescroll中,我们判断,如果是上滑且顶部控件未完全隐藏,则消耗掉dy,即consumed[1]=dy;如果是下滑且内部view已经无法继续下拉,则消耗掉dy,即consumed[1]=dy,消耗掉的意思,就是自己去执行scrollby,实际上就是我们的sticknavlayout滑动。
- 此外,这里还处理了fling,通过onnestedprefling方法,这个可以根据自己需求定了,当顶部控件显示时,fling可以让顶部控件隐藏或者显示。
以上代码就能实现下面的效果:
对于fling方法,我们利用了overscroll的fling的方法,对于边界检测,是重写了scrollto方法:
public void fling(int velocityy) { mscroller.fling(0, getscrolly(), 0, velocityy, 0, 0, 0, mtopviewheight); invalidate(); } @override public void scrollto(int x, int y) { if (y < 0) { y = 0; } if (y > mtopviewheight) { y = mtopviewheight; } if (y != getscrolly()) { super.scrollto(x, y); } }
详细的解释可以看上面提到的文章,这里就不重复了。
到这里呢,可以看到nestedscrolling机制说白了非常简单:
就是nestedscrollingparent内部的view,在滑动到时候,会首先将dx、dy传入给nestedscrollingparent,nestedscrollingparent可以决定是否对其进行消耗,一般会根据需求消耗部分或者全部(不过这里并没有实际的约束,你可以随便写消耗多少,可能会对内部view造成一定的影响)。
用白话和原本的事件分发机制作对比就是这样的(针对正常流程下一次手势):
- 事件分发是这样的:子view首先得到事件处理权,处理过程中,父view可以对其拦截,但是拦截了以后就无法再还给子view(本次手势内)。
- nestedscrolling机制是这样的:内部view在滚动的时候,首先将dx,dy交给nestedscrollingparent,nestedscrollingparent可对其进行部分消耗,剩余的部分还给内部view。
具体的源码会比本博文复杂,因为涉及到触摸非内部view区域的一些交互,非本博文重点,可以参考源码。
四、原理
原理其实就是看内部view什么时候回调nestedscrollingparent各种方法的,直接定位到内部view的ontouchevent:
@override public boolean ontouchevent(motionevent e) { switch (action) { case motionevent.action_down: { int nestedscrollaxis = viewcompat.scroll_axis_none; if (canscrollhorizontally) { nestedscrollaxis |= viewcompat.scroll_axis_horizontal; } if (canscrollvertically) { nestedscrollaxis |= viewcompat.scroll_axis_vertical; } startnestedscroll(nestedscrollaxis); } break; case motionevent.action_move: { if (dispatchnestedprescroll(dx, dy, mscrollconsumed, mscrolloffset)) { dx -= mscrollconsumed[0]; dy -= mscrollconsumed[1]; vtev.offsetlocation(mscrolloffset[0], mscrolloffset[1]); } } break; case motionevent.action_up: { fling((int) xvel, (int) yvel); resettouch(); } break; case motionevent.action_cancel: { canceltouch(); } break; } return true; }
可以看到:
action_down调用了startnestedscroll;action_move中调用了dispatchnestedprescroll;action_up可能会触发fling以调用resettouch。
startnestedscroll内部实际上:
#nestedscrollingchildhelper 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,然后回调onstartnestedscroll和onnestedscrollaccepted。
dispatchnestedprescroll中会回调onnestedprescroll方法,内部的scrollbyinternal中还会回调onnestedscroll方法。
fling中会回调onnestedprefling和onnestedfling方法。
resettouch中则会回调onstopnestedscroll。
代码其实没什么贴的,大家直接找到ontouchevent一眼就能看到,调用的方法名都是dispatchnestedxxx方法,实际内部都是通过nestedscrollingchildhelper实现的。
所以如果你需要实现和nestedscrollingparent协作的内部view,记得实现nestedscrollingchild,然后内部借助nestedscrollingchildhelper这个辅助类,核心的方法都封装好了,你只需要在恰当的实际去传入参数调用方法即可。
ok,这样的一个机制一定要去试试,很多滑动相关的效果都可以借此实现;
源码地址:
github地址:https://github.com/hongyangandroid/android-stickynavlayout
本地下载:http://xiazai.jb51.net/201705/yuanma/android-stickynavlayout(jb51.net).rar
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作能带来一定的帮助,如果有疑问大家可以留言交流,谢谢大家对的支持。
下一篇: css3学习系列之移动属性详解