优雅的嵌套滑动解决方式-NestedScroll
Posted zuguorui
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了优雅的嵌套滑动解决方式-NestedScroll相关的知识,希望对你有一定的参考价值。
优雅的嵌套滑动解决方式-NestedScroll
嵌套滑动相信大家一定经常遇到,最烦人的就是我们有两层view,它们都能在同一个方向上滑动,这时候滑动的判断就是个头疼的问题。不过这也还好说,毕竟只要根据业务需要来决定上层layout相对于下层的layout滚动的优先级,然后决定是否拦截滑动事件即可。最最让人绝望的,就是在同一个事件流中要分别让两层view滑动!!!比如滑动时,前半段上层滑动,上层滑不动之后,下层再接着开始滑动。
这怎么办?以前我是这么办的,上层判断自己能滑动时,就拦截这个事件,然后滑动,滑不动之后呢?就把接下来的事件再按照正常流程分发下去。但这步经常会出问题,如果你下层的是自定义的layout,你知道其中的事件消耗规则,那还好办。但对于系统的那些view,它们会更加复杂。比如这个地方,如果你是直接把接下来的ACTION_MOVE事件分发下去,那可能因为没有ACTION_DOWN事件导致View的某些状态没有被激活,然后它就会忽略掉之后的事件。当然你也可以再自己伪造一个ACTION_DOWN事件骗骗它们。
那如果是要求下层先滑动呢?这可就真让人扎心了,因为:首先,上层view没法判断下层view什么时候会滑不动。尽管View类中有OnScrollChangeListener
,但并不是所有的view都乖乖地遵循这个规范,比如我们的AbsListView
(它是ListView和ScrollView的父类),它就是用的自己的OnScrollListener
。还有RecyclerView
,它也有自己的OnScrollListener
,并且和AbsListView
的那个还不一样。你真的愿意为了这么多View都分别实现它们的监听器吗?
其次就是下层View先滑动,那就意味着事件先交给下层View。我们之前的文章讲过,子view可以告诉上层view不要拦截事件,但找来找去都没有找到子view能把事件还给上层view的方法。。。。
当然上面那些问题都可以通过自定义view去继承一个通用的接口啊啥的来解决,但一来代码侵入性太强,二来工作量会大很多。而今天我们要介绍的这个方式,其实就是和这个想法一样,让view继承一套接口来解决这个问题,只是它是官方支持的,意味着那些能滚动的View都支持它。这两个接口就是NestedScrollingParent
和NestedScrollingChild
。其中NestedScrollingChild
扮演的角色就是我们这里说的下层view,而NestedScrollingParent
就是上层view。这两个接口是android5.0之后在所有控件中就默认支持了,也就是集成了它们,不必再去显式地继承它们。而如果你的程序要跑在更老一点的系统上,那还是显式地继承并重写比较好。
让我们先看个demo
首先看看这两个接口具体的方法。
public interface NestedScrollingParent
/**
* 开始NestedScroll时调用,返回true就意味着后面可以接受到NestedScroll事件,否则就无法接收。
* @param child 该view的直接子view
* @param target 发出NestedScroll事件的子view,和child不一定是同一个
* @param nestedScrollAxes 滑动的方向,为ViewCompat#SCROLL_AXIS_HORIZONTAL或者ViewCompat#SCROLL_AXIS_VERTICAL,亦或两者都有。
* @return 返回true代表要消耗这个NestedScroll事件,否则就是false。
* */
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);
/**
*在onStartNestedScroll之后调用,参数意义同上,可什么都不做
* */
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);
/**
* 结束NestedScroll事件时调用,可什么都不做
* */
public void onStopNestedScroll(View target);
/**
*在target滑不动的时候会调用这个方法,这时就通知本view可以进行滑动。如果目标view可以一直滑动,那么这个方法就不会被调用
* @param target 发出NestedScroll事件的子view
* @param dxConsumed target在x方向上已经消耗的滑动距离
* @param dxUnconsumed 这次滑动事件在x方向除去target已经消耗的还剩下的距离,通常如果我们需要滑动的话就使用这个值。
* @param dyConsumed 同上
* @param dyUnconsumed 同上
*
* */
public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed);
/**
*在target每次滑动之前会调用这个方法,。
* @param target 发出NestedScroll事件的子view
* @param dx 这次滑动事件在x方向上滑动的距离
* @param dy 这次滑动事件在y方向上滑动的距离
* @param consumed 一个长度为2的数组。第0位时我们在x方向消耗的滑动距离,第1位是我们在y方向上消耗的滑动距离。子view会根据这个和dx/dy来计算余下的滑动量,来决定自己是否还要进行剩下的滑动。
* 比如我们使consumed[1] = dy,那么子view在y方向上就不会滑动。
*
* */
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed);
/**
* 在target进行fling后调用。注意这个方法并不是像onNestedScroll在子view滑不动之后调用,而是紧跟着onNestedPreFling后会被调用。因此对于它的使用场景一般比较少。
*
* @param target 目标view
* @param velocityX 在x方向的速度,注意这是fling的起始速度,并不是目标在滑不动时停止时刻的速度,它和onNestedPreFling中的velocityX是一样的。
* @param velocityY 在y方向的速度,注意这是fling的起始速度,并不是目标在滑不动时停止时刻的速度,它和onNestedPreFling中的velocityY是一样的。
* @param consumed 目标view是否消耗了此次fling
* @return 本view是否消耗了这次fling
* */
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);
/**
* 在target判断为fling并且执行fling之前调用,我们可以通过返回true来拦截目标的fling,这样它就不会执行滑动。
* @param target 目标view
* @param velocityX 在x方向的起始速度
* @param velocityY 在y方向的起始速度
* @return 我们是否消耗此次fling,返回true代表拦截,返回false,目标view就进行正常的fling
* */
public boolean onNestedPreFling(View target, float velocityX, float velocityY);
public int getNestedScrollAxes();
然后是NestedScrollingChild
public interface NestedScrollingChild
/**
* 设置使该view可以进行NestedScroll,一般都是设为true
* */
public void setNestedScrollingEnabled(boolean enabled);
public boolean isNestedScrollingEnabled();
public boolean startNestedScroll(int axes);
public void stopNestedScroll();
public boolean hasNestedScrollingParent();
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);
public boolean dispatchNestedPreFling(float velocityX, float velocityY);
我修改了注释,使其更加易懂。同时也加入了一些原来注释中未曾提到的注意事项。我相信看了它之后,基本心中也就有点数了。而NestedScrollingChild
基本就没写注释,是因为它基本和NestedScrollingParent
中的方法是一一对应的。看了前面的,后面相信不难理解。另外一个原因则是根据官方推荐,使用者两个接口都需要两个Helper类:NestedScrollingChildHelper
和NestedScrollingParentHelper
,而NestedScrollingChild
中的方法在NestedScrollingChildHelper
都有对应的实现,只要在NestedScrollingChild
的方法中调用NestedScrollingChildHelper
的方法即可。
可能看了不是很明白,接下来我们就按流程写一个实际的用法。
首先,我们的自定义View需要实现NestedScrollingParent
和NestedScrollingChild
接口。
public class DemoLayout extends ViewGroup implements NestedScrollingParent, NestedScrollingChild
然后,根据官方推荐,我们需要两个final的NestedScrollingChildHelper
和NestedScrollingParentHelper
对象来进行一些辅助性的工作。
private final NestedScrollingChildHelper mNestedChildHelper;
private final NestedScrollingParentHelper mNestedParentHelper;
然后在View的构造函数中初始化它们。
public HideHeadLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)
super(context, attrs, defStyleAttr, defStyleRes);
mNestedChildHelper = new NestedScrollingChildHelper(this);
mNestedParentHelper = new NestedScrollingParentHelper(this);
setNestedScrollingEnabled(true);
//其它工作
//....
接着,主要是实现这两个接口的方法,需要注意的是,如果你的targetSdk大于等于Android5.0,那么你不实现这些接口也不会报错,但如果为了兼容性,那还是全都实现了比较好。首先是NestedScrollingChild
,因为它的实现很简单。
/*NestedScrollingChild APIs*/
@Override
public void setNestedScrollingEnabled(boolean enabled)
mNestedChildHelper.setNestedScrollingEnabled(enabled);
@Override
public boolean isNestedScrollingEnabled()
return mNestedChildHelper.isNestedScrollingEnabled();
@Override
public boolean startNestedScroll(int axes)
return mNestedChildHelper.startNestedScroll(axes);
@Override
public void stopNestedScroll()
mNestedChildHelper.stopNestedScroll();
@Override
public boolean hasNestedScrollingParent()
return mNestedChildHelper.hasNestedScrollingParent();
@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow)
return mNestedChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow);
@Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow)
return mNestedChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
@Override
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed)
return mNestedChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);
@Override
public boolean dispatchNestedPreFling(float velocityX, float velocityY)
return mNestedChildHelper.dispatchNestedPreFling(velocityX, velocityY);
对,你没看错,这些接口方法的实现就是调用NestedScrollingChildHelper
对应的方法。
接着,就是实现NestedScrollingParent
的方法。这里NestedScrollingParentHelper
不像NestedScrollingChildHelper
会给我们全部方法的对应实现,而是关键方法需要我们自己去实现,毕竟这部分逻辑只有开发者自己清楚。
/*NestedScrollingParent APIs*/
@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes)
//检查滑动方向是不是我们需要的,如果是就返回true,不是就返回false。
if((ViewCompat.SCROLL_AXIS_VERTICAL & nestedScrollAxes) == ViewCompat.SCROLL_AXIS_VERTICAL)
return true;
else
return false;
@Override
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes)
//这个方法只是状态通知,没有多大的作用,因此NestedScrollingParentHelper为我们提供了对应方法,直接调用即可。当然也可以不处理。
mNestedParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes);
//同上,不过你可以在这里做一些收尾的工作
@Override
public void onStopNestedScroll(View target)
mNestedParentHelper.onStopNestedScroll(target);
//这个方法是真正重要的方法,执行滑动。
@Override
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed)
//根据自己的方向,使用dxUnconsumed或者dyUnconsumed进行滑动。
offsetChild(dyUnconsumed);
//在target滑动之前调用。
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed)
//根据自己的方向,使用dx或者dy进行滑动,并且要将自己消耗的距离放入consumde数组里,这里以y方向举例。
int realOffset = computeOffsetDis(dy);
offsetChild(realOffset);
consumed[1] = realOffset;
consumed[0] = 0;
//这个方法基本就没什么用了,因为它并不能反映在target停止滑动时的状态。也就只能监测一下状态而已。直接返回false即可。
//当然,如果consumed = false时你需要滑动,也可以进行滑动然后返回true。
@Override
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed)
return false;
//这个方法就是target在fling之前问你要不要fling,你fling了它就不fling了,要不然它就上了。
@Override
public boolean onNestedPreFling(View target, float velocityX, float velocityY)
//可以判断一下自己的状态是不是需要fling,然后进行操作。
if((velocityY > 0 && canScrollY(1)) || (velocityY < 0 && calScrollY(-1)))
//进行你自己的fling
return true;
else
return false;
//获取当前滚动方向。可以推测NestedScrollingParentHelper内部肯定维护了一些状态,因此尽管某些方法看起来无足轻重,但我
//们还是应该按照规定实现它,以确保它内部的状态是正确的。对于NestedScrollingChildHelper也是如此。
@Override
public int getNestedScrollAxes()
return mNestedParentHelper.getNestedScrollAxes();
好了,以上就是基本的用法了,都是些伪代码。当然,这并不完全标准,因为我们只是作为一个Parent来消耗事件,但是并没有作为一个合格的Child去分发事件。如果我们的上层View也需要我们的NestedScroll事件,那可就惨了。
下一篇文章我会实现本篇开头的那个demo,这是目前已经非常常用的一种布局。掌握它还是很有用的。
本人目前在找Android开发的工作,地点在深圳南山科技园附近,有伯乐可以留言或者发邮件给我:zu_guorui@126.com.
以上是关于优雅的嵌套滑动解决方式-NestedScroll的主要内容,如果未能解决你的问题,请参考以下文章
Android NestedScrolling解决滑动冲突问题 - fling问题与NestedScroll++
Android中不同方向嵌套滑动的解决方式(ListView为样例)
解决ScrollView嵌套RecyclerView的显示及滑动问题
(转)ViewPager,ScrollView 嵌套ViewPager滑动冲突解决