OverScroll弹性滚动和惯性滚动效果的实现原理——CoordinatorLayout+Behavior

5,415 阅读4分钟

前面介绍了OverScroll的使用,没看过文章的同学可以先了解下《类似微信首页弹性滚动和惯性滚动效果的实现——OverScroll》

接下来介绍OverScroll的实现原理。

CoordinatorLayout

CoordinatorLayout是在Support 包中功能强大的布局容器,它本质是一个 FrameLayout,然而它允许开发者通过自定义Behavior协调各个子view,实现各种复杂酷炫的UI交互效果。

使用CoordinatorLayout需要在 build.gradle 加入:

implementation 'com.android.support:design:26.1.0'

网上很多关于CoordinatorLayout的入门文章,这里笔者不再赘述,所谓实践大于理论,本文讲述如何利用CoordinatorLayout+Behavior实现弹性滑动和惯性滑动,从侧面去理解它的使用原理.

本文实现类似微信首页的弹性滑动和惯性滑动效果,支持水平和垂直方向上的滚动,如下图所示:

vertical over-scroll
horizontal over-scroll

Behavior

CoordinatorLayout主要是通过Behavior来协调子view,这里涉及到的Behavior的关键方法如下:

方法 描述
boolean onStartNestedScroll(CoordinatorLayout parent, View child, View directTargetChild, View target, int nestedScrollAxes, int type) 根据返回值判断是否要处理target发生的滑动,一般用于判断是否要处理某个方向上的滑动.
例如return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;表示处理垂直方向方向上的滑动.接下来的滑动事件将回调给下面的方法处理.
void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dx, int dy, int[] consumed, int type) target即将发生滑动时调用,在这里可以做拦截处理.可以修改参数consumed表示消耗(拦截)了多少像素。例如target控件本身想要垂直方向上滑动100px,而我们需要拦截掉80px,则要设置 consumed[1] = 80,(consumed[0]consumed[1] 分别对应x轴和y轴),最后target控件实际只滑动了20px.
void onNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) target控件发生滑动后调用, dyConsumed为实际消耗的距离,dyUnconsumed为未消耗的距离.例如上面的滑动,此时dyConsumed = 20,dxUnconsumed = 0,如果dyConsumed = 15,dxUnconsumed = 5则表示target在滑动15px距离时到达了边界,我们可以利用dxUnconsumed处理一些越界后的滑动.
boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, View child, View target, float velocityX, float velocityY) 用户快速滑动target并松开手指发生惯性滑动之前调用,返回true表示拦截该惯性滑动事件.
void onStopNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target, int type) 所有的滑动停止后调用.

原理分析

弹性滑动和惯性滑动都属于过度滑动(Over scroll),即在达到正常滑动范围的边界后继续滑动.因此,我们只需要处理在达到边界时的越界滑动效果.关键的处理逻辑如下:

overscoll

上图描述了向下滑动过程中需要处理的关键逻辑,同理,向上滑动也采取类似的处理.

另外我们还要处理惯性滑动,当快速滑动产生fling事件时,让列表滑到边界时仍能够惯性滑动一点距离.这里需要借助系统提供的工具类OverScroller, 主要在onNestedPreFling里相关的惯性滑动的参数传入OverScroller


public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, View child, View target, float velocityX, float velocityY) {
    if (child == target) {
        mOverScroller.fling(0, 0, 0, (int) velocityY, 0, 0, Integer.MIN_VALUE, Integer.MAX_VALUE);
    }
    return super.onNestedPreFling(coordinatorLayout, child, target, velocityX, velocityY);
}

后面便可以在滑动过程中通过mOverScroller.getCurrVelocity()获取当前时间惯性滑动的速度,当速度小于某个值时则停止滑动.

Behavior中的关键方法的参数中基本上最后都有个type值,文档解释为the type of input which cause this scroll event,即表示产生当前滑动的事件来源,当type == ViewCompat.TYPE_TOUCH时表示由用户触摸控件产生的滑动,type == ViewCompat.TYPE_NON_TOUCH时表示由非触摸产生的滑动,比如惯性造成的滑动就是非触摸产生的.

因此我们在滑动过程中可以通过type判断当前滑动是否为惯性滑动.

public void onNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed,
                           int type) {
    if (type == ViewCompat.TYPE_TOUCH) { // scroll
        
    } else { // fling
        
    }
}

最后,我们需要在停滑动时,如果列表发生了越界偏移,则需要把列表弹回原位,这里通过ValueAnimator动画实现即可.

代码实现

  • 弹性滑动和惯性滑动过程中需要一些参数控制滑动效果,如最大的滑动距离,惯性滑动的最小速度,滑动的阻尼因子等,因此我们需要定义一个接口,和'Behavior`绑定的子View必须实现该接口,接口定义请查看IOverScrollCallback,默认实现为SimpleOverScrollCallback.

  • 自定义Behavior弹性滑动和惯性滑动,基类为BaseOverScrollBehavior,控制垂直滚动OverScrollVerticalBehavior,控制水平滚动OverScrollHorizontalBehavior

  • 让NestedScrolling滑动控件(如RecyclerView,NestedScrollView等)实现IOverScrollCallback,提供相关滑动参数,这里以OverScrollScrollView控件为例,代码请查看OverScrollScrollView.

效果(布局相关:NestedScrollFragmentlayout_scrollview.xml):

nested over-scroll

项目地址OverScroll

多谢支持我的github项目>>>OverScroll