SmartRefreshLayout dispatchTouchEvent 解读

2021-04-12 10:20:54 浏览数 (1)

Github SmartRefreshLayout 地址

SmartRefreshLayout 以打造一个强大,稳定,成熟的下拉刷新框架为目标,并集成各种的炫酷、多样、实用、美观的Header和Footer。

·支持多点触摸

·支持淘宝二楼和二级刷新

·支持嵌套多层的视图结构 Layout (LinearLayout,FrameLayout...)

·支持所有的 View(AbsListView、RecyclerView、WebView....View)

·支持和 ListView 的无缝同步滚动 和 CoordinatorLayout 的嵌套滚动 .

·支持自动刷新、自动上拉加载(自动检测列表惯性滚动到底部,而不用手动上拉).

·支持自定义回弹动画的插值器,实现各种炫酷的动画效果.

·支持设置主题来适配任何场景的 App,不会出现炫酷但很尴尬的情况.

·支持设多种滑动方式:平移、拉伸、背后固定、顶层固定、全屏

·支持所有可滚动视图的越界回弹

高版本还支持

·支持 Header 和 Footer 交换混用

·支持 AndroidX

·支持自定义并且已经集成了很多炫酷的 Header 和 Footer.

更多效果可见:https://github.com/scwang90/SmartRefreshLayout/tree/master/art

开发者对API极致的运用,产生如此炫酷的视觉效果。

接下来自底向上,解读SmartRefreshLayout的核心实现。

SmartRefreshLayout 集成

集成SmartRefreshLayout

https://github.com/scwang90/SmartRefreshLayout

APP 目前基于2018年有钱花需求引入的版本,进行源码集成。

代码语言:javascript复制
 implementation project(':refresh')

在XML中声明该布局

代码语言:javascript复制
<?xml version="1.0" encoding="utf-8"?><FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/default_bg">

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

    <com.duxiaoman.ui.smartrefresh.SmartRefreshLayout
            android:id="@ id/refreshLayout"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:srlEnableLoadMore="false"
            app:srlEnableOverScrollBounce="false"
            app:srlEnableOverScrollDrag="false"
            app:srlFixedHeaderViewId="@id/refresh_bg_image"
            app:srlHeaderTranslationY="@dimen/brand_area_height"
            app:srlHeaderInsetStart="0dp"
            app:srlHeaderMaxDragRate="1"
            app:srlDragRate="0.8"
            app:srlReboundDuration="980">


            <ImageView
                android:id="@ id/refresh_bg_image"
                android:layout_width="match_parent"
                android:layout_height="150dp" />

            <com.duxiaoman.wallet.ui.header.TextCircleHeader
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                app:color_type="white" />


            <android.support.v7.widget.RecyclerView
                android:id="@ id/refresh_recycler_view"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:paddingBottom="@dimen/brand_area_height"
                android:descendantFocusability="blocksDescendants"
                android:nestedScrollingEnabled="false"
                android:overScrollMode="never"
                android:scrollbars="none" />
        </com.duxiaoman.ui.smartrefresh.SmartRefreshLayout>

        <include
            android:id="@ id/life_topbar"
            layout="@layout/fragment_life_service_topbar"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />
    </FrameLayout>
...

</FrameLayout>

控件类型

作用

名称

备注

RefreshLayout

下拉刷新框架

SmartRefreshLayout

RefreshContent

滑动内容

RecyclerView

RefreshHeader

默认下拉后刷新的头部

TextCircleHeader

FixedHeaderView

随RecyclerView一起滑动的背景

ImageView

换肤等业务诉求

在 Activity 或者 Fragment 中添加代码

代码语言:javascript复制
// 示例样板代码
RefreshLayout refreshLayout = (RefreshLayout)findViewById(R.id.refreshLayout);
// 设置下拉刷新header
refreshLayout.setRefreshHeader(new ClassicsHeader(this)); 
// 设置上推加载更多footor
refreshLayout.setRefreshFooter(new ClassicsFooter(this));
// 监听下拉刷新
refreshLayout.setOnRefreshListener(new OnRefreshListener() {
    @Override
    public void onRefresh(RefreshLayout refreshlayout) {
        refreshlayout.finishRefresh(2000/*,false*/);//传入false表示刷新失败
    }
});
// 监听上推加载更多
refreshLayout.setOnLoadMoreListener(new OnLoadMoreListener() {
    @Override
    public void onLoadMore(RefreshLayout refreshlayout) {
        refreshlayout.finishLoadMore(2000/*,false*/);//传入false表示加载失败
    }
});

SmartRefreshLayout被设计为一个刷新框架,具有非常高的自定性和可扩展性,可以应付 项目中的各种情况和场景。

通过SmartRefreshLayout框架,你可以在一个稳定强大的下拉布局中实现自己项目需求的 Header ,不用去关心滑动事件处理,不用关心子控件的回弹和滚动边界,只需关注自己真 正的项目需求Header的样子和动画。

SmartRefreshLayout体系结构

复杂的问题,往往通过拆解为子问题、子模块的思路解决。

在学习使用框架的自定义功能之前,我们还是有必要来了解一下框架的体系和结构:

·RefreshLayout 下拉的基本功能,包括布局测量、滑动事件处理、参数设定等等

·RefreshHeader 下拉头部的事件处理和显示接口

·RefreshFooter 上拉底部的事件处理和显示接口

·RefreshContent 对不同内容的统一封装,包括判断是否可滚动、回弹判断、智能识别

·RefreshKernel 刷新布局核心功能接口、为功能复杂的 Header 或者 Footer 开放的接口

·RefreshInternal 刷新内部组件,传递下拉或者上拉等事件

类图

开放接口

支持外部设置的属性,可通过xml 或者set方法设置,详情见https://github.com/scwang90/SmartRefreshLayout/blob/master/art/md_property.md

属性

类型

含义

业务实现

srlReboundDuration

integer

释放后回弹动画时长(默认250毫秒)

980

srlHeaderHeight

dimension

Header的标准高度(dp)

默认425,二楼210

srlDragRate

float

显示拖动高度/真实拖动高度(默认0.5,阻尼效果)

默认0.4,二楼0.8

srlHeaderMaxDragRate

float

Header最大拖动高度/Header标准高度(默认2,要求>=1)

1

srlFooterMaxDragRate

float

Footer最大拖动高度/Footer标准高度(默认2,要求>=1)

1

srlHeaderTriggerRate

float

Header触发刷新距离 与 HeaderHeight 的比率(默认1)

默认 70/350 二楼 140/210

srlEnableRefresh

boolean

是否开启下拉刷新功能(默认true)

默认

srlEnableHeaderTranslationContent

boolean

拖动Header的时候是否同时拖动内容(默认true)

默认

srlEnableFooterTranslationContent

boolean

拖动Footer的时候是否同时拖动内容(默认true)

默认

srlEnableOverScrollDrag

boolean

是否启用越界拖动(仿苹果效果)V1.0.4

false

srlEnableOverScrollBounce

boolean

设置是否开启越界回弹功能(默认true)

默认

srlEnableNestedScrolling

boolean

是否开启嵌套滚动NestedScrolling(默认false-智能开启)

默认

srlDisableContentWhenRefresh

boolean

是否在刷新的时候禁止内容的一切手势操作(默认false)

默认

srlDisableContentWhenLoading

boolean

是否在加载的时候禁止内容的一切手势操作(默认false)

默认

srlFixedHeaderViewId

id

指定固定顶部的视图Id

设置品牌简介 资产区整图背景

srlFixedFooterViewId

id

指定固定底部的视图Id

自定义ViewGroup

属性变量

https://github.com/scwang90/SmartRefreshLayout/blob/master/art/md_property.md

同其他自定义View一样。SmartRefreshLayout提供了丰富的自定义xml属性,实现丰富的定制效果。

智能识别 - onFinishInflate()

智能识别,识别的是什么呢?RefreshHeader,RefreshContent,RefreshFooter

1.限制子View不能超过3个

2.RefreshContentWrapper.isScrollableView(即View类型为任意可滑动组件的一种)判断RefreshContent。并记录index。

3.识别RefreshHeader/RefreshFooter,即实现此接口的View或者第0个或者最后一个。

4.使用bringChildToFront() 接口,按照设置的SpinningStyle样式,进行排序。

默认填充 - onAttachedToWindow()

假如上个阶段的三个组件未获取到,则根据enableRefresh,enableLoadMore 等属性,填充默认值。

onMeasure() - onLayout()

这两个阶段,所见即所写,大家都比较熟悉。

主要讲一下SpinnerStyle.Translate 状态的的布局。在mHeaderTranslationY = 0 时

RefreshHeader布局完全出离屏幕。

OverDraw 优化 - drawChild()

SpinnerStyle.FixedBehind 状态(类似微信header效果),在绘制时,使用clipPath裁剪Header真正需要的位置,减少过度绘制。

代码语言:javascript复制
   if (mEnableClipHeaderWhenFixedBehind && mRefreshHeader.getSpinnerStyle() == SpinnerStyle.FixedBehind) {
                    canvas.save();
                    canvas.clipRect(child.getLeft(), child.getTop(), child.getRight(), bottom);
                    boolean ret = super.drawChild(canvas, child, drawingTime);
                    canvas.restore();
                    return ret;
                }

防止内存泄漏 - onDetachedFromWindow()

代码语言:javascript复制
 @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        moveSpinner(0, false);
        notifyStateChanged(RefreshState.None);
        mHandler.removeCallbacksAndMessages(null);
        mHandler = null;
        mManualLoadMore = true;
        mManualNestedScrolling = true;
        animationRunnable = null;
        if (reboundAnimator != null) {
            reboundAnimator.removeAllListeners();
            reboundAnimator.removeAllUpdateListeners();
            reboundAnimator.cancel();
            reboundAnimator = null;
        }
    }

除了上述基本定制内容,还有嵌套滑动,越界回弹效果等等。

事件分发核心 - 从dispatchTouchEvent开始...

代码语言:javascript复制
True if the event was handled by the view, false otherwise.

SmartRefreshLayout是个自定义View,它内部的事件分发的重心是处理当前 Group 和子 View 之间的逻辑关系:

·是否需要拦截 touch 事件;

·是否需要将 touch 事件继续分发给子 View;

·如何将 touch 事件分发给子 View。

·根据Event 下拉,或者上推,或者滑动。

step1:多点触控

假如不处理多点触摸事件,会发生什么?

我们写支持手指滑动操作的控件时,当你一根手指操作你发现没有问题,但是当多根手指的时候,会有一些问题。

很简单,注释该段代码, 会产生如下恶劣的效果(效果图我就不放了)

1. 多点触摸上推效果不连贯

2. 双指切换,页面跳动。示例场景:多手指情况下,一手指不变,另一手指上推二楼至不可见后松手,二楼突然变换至下拉状态。

问题原因

event.getY() 返回的可能是任意的一个手指的位置。观察下列日志,可以发现ACTION_POINTER_DOWN事件之后,ACTION_MOVE对应的X,Y值均有一个落差性的变化

所以你在onTouchEvent 里面 ,如果你是按照getY() 和 LastY 做差值去移动页面,ACTION_MOVE 的时候会有两个手指的落差 ,造成双指切换的时候 页面会来回跳动

代码语言:javascript复制

在讲如何解决此类问题之前,可以先了解下列API。

MotionEvent.getActionMasked() 和 MotionEvent.getAction 有什么区别

MotionEvent.getAction() 识别不了 MotionEvent.ACTION_POINTER_DOWN 和 ACTION_POINTER_UP 事件。所以如果自定义View 用到了多点触控,要使用getActionMasked() 方法。

MotionEvent.getY() 和 MotionEvent.getRawY() 的区别

·getY 表示触摸事件在当前的View内的Y 坐标

·getRawY表示触摸事件在整个屏幕上面的Y 坐标

MotionEvent.getActionIndex()

·使用场景:event.getActionIndex() 表示当前触摸手指的index, 用于多点触控。

·使用范围: 仅在 ACTION_POINTER_DOWN 和 ACTION_POINTER_UP 的时候用到。

·作用: 返回当前ACTION_POINTER_DOWN 或者 ACTION_POINTER_UP 对应的手指Index。其他事件返回0。

·通过id标示手指 我们拿到当前的触摸手指的Index 之后,就可以拿到当前触摸手指的Id:event.getPointerId(event.getActionIndex()).

·在多点触控过程中,Index 可能会变,但是Id 不会变。 我们也可以根据Id 拿到 index,

代码语言:javascript复制
2020-12-10 16:40:35.497 1795-1795/? E/action: action=ACTION_DOWN touchX=868.0 touchY=589.0 index=0 id=0
2020-12-10 16:40:35.541 1795-1795/? E/action: action=ACTION_MOVE touchX=854.65155 touchY=632.39374 index=0 id=0
...
2020-12-10 16:40:36.507 1795-1795/? E/action: action=ACTION_MOVE touchX=712.0 touchY=1843.0 index=0 id=0
2020-12-10 16:40:37.464 1795-1795/? E/action: action=ACTION_POINTER_DOWN touchX=712.0 touchY=1843.0 index=1 id=1
2020-12-10 16:40:37.538 1795-1795/? E/action: action=ACTION_POINTER_UP touchX=712.0 touchY=1843.0 index=0 id=0
2020-12-10 16:40:37.591 1795-1795/? E/action: action=ACTION_MOVE touchX=286.6251 touchY=1891.3749 index=0 id=1
..
2020-12-10 16:40:37.808 1795-1795/? E/action: action=ACTION_MOVE touchX=275.5766 touchY=1905.0 index=0 id=1
2020-12-10 16:40:37.909 1795-1795/? E/action: action=ACTION_MOVE touchX=275.0 touchY=1905.0 index=0 id=1
2020-12-10 16:40:38.230 1795-1795/? E/action: action=ACTION_POINTER_DOWN touchX=830.0 touchY=1423.0 index=0 id=0
2020-12-10 16:40:38.275 1795-1795/? E/action: action=ACTION_MOVE touchX=830.0 touchY=1423.0 index=0 id=0
...
2020-12-10 16:40:38.345 1795-1795/? E/action: action=ACTION_MOVE touchX=829.0 touchY=1425.0 index=0 id=0
2020-12-10 16:40:38.345 1795-1795/? E/action: action=ACTION_POINTER_UP touchX=829.0 touchY=1425.0 index=1 id=1
2020-12-10 16:40:38.358 1795-1795/? E/action: action=ACTION_MOVE touchX=827.619 touchY=1426.381 index=0 id=0
...
2020-12-10 16:40:38.492 1795-1795/? E/action: action=ACTION_MOVE touchX=820.0 touchY=1426.0 index=0 id=0
2020-12-10 16:40:38.821 1795-1795/? E/action: action=ACTION_MOVE touchX=820.0 touchY=1427.0 index=0 id=0
2020-12-10 16:40:38.821 1795-1795/? E/action: action=ACTION_POINTER_DOWN touchX=820.0 touchY=1427.0 index=1 id=1
2020-12-10 16:40:38.842 1795-1795/? E/action: action=ACTION_MOVE touchX=821.3011 touchY=1427.0 index=0 id=0
2020-12-10 16:40:38.850 1795-1795/? E/action: action=ACTION_POINTER_UP touchX=821.0 touchY=1427.0 index=0 id=0
2020-12-10 16:40:38.942 1795-1795/? E/action: action=ACTION_MOVE touchX=301.0 touchY=1922.0 index=0 id=1
...
2020-12-10 16:40:39.392 1795-1795/? E/action: action=ACTION_MOVE touchX=297.0 touchY=1971.0 index=0 id=1
2020-12-10 16:40:39.605 1795-1795/? E/action: action=ACTION_POINTER_DOWN touchX=845.0 touchY=914.0 index=0 id=0
2020-12-10 16:40:39.626 1795-1795/? E/action: action=ACTION_MOVE touchX=845.0 touchY=914.0 index=0 id=0
...
2020-12-10 16:40:39.721 1795-1795/? E/action: action=ACTION_MOVE touchX=843.0 touchY=914.0 index=0 id=0
2020-12-10 16:40:39.721 1795-1795/? E/action: action=ACTION_POINTER_UP touchX=843.0 touchY=914.0 index=1 id=1
...
2020-12-10 16:40:40.251 1795-1795/? E/action: action=ACTION_MOVE touchX=837.0 touchY=915.0 index=0 id=0
2020-12-10 16:40:40.252 1795-1795/? E/action: action=ACTION_POINTER_DOWN touchX=837.0 touchY=915.0 index=1 id=1
2020-12-10 16:40:40.297 1795-1795/? E/action: action=ACTION_POINTER_UP touchX=837.0 touchY=915.0 index=0 id=0
2020-12-10 16:40:40.377 1795-1795/? E/action: action=ACTION_MOVE touchX=274.34717 touchY=1916.3472 index=0 id=1
...
2020-12-10 16:40:40.843 1795-1795/? E/action: action=ACTION_MOVE touchX=305.0 touchY=1978.0 index=0 id=1
2020-12-10 16:40:41.041 1795-1795/? E/action: action=ACTION_POINTER_DOWN touchX=899.0 touchY=930.0 index=0 id=0
2020-12-10 16:40:41.061 1795-1795/? E/action: action=ACTION_MOVE touchX=899.0 touchY=930.0 index=0 id=0
2020-12-10 16:40:41.076 1795-1795/? E/action: action=ACTION_MOVE touchX=899.0 touchY=930.0 index=0 id=0
2020-12-10 16:40:41.093 1795-1795/? E/action: action=ACTION_MOVE touchX=899.0 touchY=930.0 index=0 id=0
2020-12-10 16:40:41.110 1795-1795/? E/action: action=ACTION_MOVE touchX=899.0 touchY=930.0 index=0 id=0
2020-12-10 16:40:41.116 1795-1795/? E/action: action=ACTION_POINTER_UP touchX=899.0 touchY=930.0 index=1 id=1
2020-12-10 16:40:41.160 1795-1795/? E/action: action=ACTION_MOVE touchX=893.0 touchY=928.0 index=0 id=0
2020-12-10 16:40:41.177 1795-1795/? E/action: action=ACTION_MOVE touchX=882.8128 touchY=925.9532 index=0 id=0
...
2020-12-10 16:40:41.344 1795-1795/? E/action: action=ACTION_MOVE touchX=869.0 touchY=923.0 index=0 id=0
2020-12-10 16:40:41.704 1795-1795/? E/action: action=ACTION_MOVE touchX=869.0 touchY=923.0 index=0 id=0
2020-12-10 16:40:41.705 1795-1795/? E/action: action=ACTION_POINTER_DOWN touchX=869.0 touchY=923.0 index=1 id=1
2020-12-10 16:40:41.737 1795-1795/? E/action: action=ACTION_POINTER_UP touchX=869.0 touchY=923.0 index=0 id=0
2020-12-10 16:40:41.827 1795-1795/? E/action: action=ACTION_MOVE touchX=302.05124 touchY=2012.8463 index=0 id=1
...
2020-12-10 16:40:42.411 1795-1795/? E/action: action=ACTION_MOVE touchX=297.0 touchY=2026.0 index=0 id=1
2020-12-10 16:40:42.456 1795-1795/? E/action: action=ACTION_POINTER_DOWN touchX=922.0 touchY=976.0 index=0 id=0
2020-12-10 16:40:42.495 1795-1795/? E/action: action=ACTION_MOVE touchX=922.0 touchY=976.0 index=0 id=0
2020-12-10 16:40:42.510 1795-1795/? E/action: action=ACTION_MOVE touchX=922.0 touchY=976.0 index=0 id=0
2020-12-10 16:40:42.528 1795-1795/? E/action: action=ACTION_MOVE touchX=922.0 touchY=976.0 index=0 id=0
2020-12-10 16:40:42.540 1795-1795/? E/action: action=ACTION_POINTER_UP touchX=922.0 touchY=976.0 index=1 id=1
2020-12-10 16:40:42.578 1795-1795/? E/action: action=ACTION_MOVE touchX=920.0 touchY=979.0 index=0 id=0
...
2020-12-10 16:40:42.745 1795-1795/? E/action: action=ACTION_MOVE touchX=907.0 touchY=982.0 index=0 id=0
2020-12-10 16:40:43.260 1795-1795/? E/action: action=ACTION_POINTER_DOWN touchX=907.0 touchY=982.0 index=1 id=1
2020-12-10 16:40:43.315 1795-1795/? E/action: action=ACTION_POINTER_UP touchX=907.0 touchY=982.0 index=0 id=0
2020-12-10 16:40:43.362 1795-1795/? E/action: action=ACTION_MOVE touchX=277.1105 touchY=1934.0737 index=0 id=1
...
2020-12-10 16:40:43.957 1795-1795/? E/action: action=ACTION_MOVE touchX=302.0 touchY=1963.0 index=0 id=1
2020-12-10 16:40:43.957 1795-1795/? E/action: action=ACTION_POINTER_DOWN touchX=962.0 touchY=1044.0 index=0 id=0
2020-12-10 16:40:43.962 1795-1795/? E/action: action=ACTION_MOVE touchX=962.0 touchY=1044.0 index=0 id=0
2020-12-10 16:40:43.978 1795-1795/? E/action: action=ACTION_MOVE touchX=962.0 touchY=1044.0 index=0 id=0
2020-12-10 16:40:43.993 1795-1795/? E/action: action=ACTION_POINTER_UP touchX=962.0 touchY=1044.0 index=1 id=1
2020-12-10 16:40:44.079 1795-1795/? E/action: action=ACTION_MOVE touchX=959.0 touchY=1043.0 index=0 id=0
...
2020-12-10 16:40:44.621 1795-1795/? E/action: action=ACTION_MOVE touchX=950.0 touchY=1041.0 index=0 id=0
2020-12-10 16:40:44.622 1795-1795/? E/action: action=ACTION_POINTER_DOWN touchX=950.0 touchY=1041.0 index=1 id=1
2020-12-10 16:40:44.652 1795-1795/? E/action: action=ACTION_POINTER_UP touchX=950.0 touchY=1041.0 index=0 id=0
2020-12-10 16:40:44.746 1795-1795/? E/action: action=ACTION_MOVE touchX=276.37662 touchY=1953.2511 index=0 id=1
...
2020-12-10 16:40:45.246 1795-1795/? E/action: action=ACTION_MOVE touchX=288.89142 touchY=1968.0 index=0 id=1
2020-12-10 16:40:45.262 1795-1795/? E/action: action=ACTION_MOVE touchX=287.5 touchY=1968.0 index=0 id=1
2020-12-10 16:40:45.268 1795-1795/? E/action: action=ACTION_POINTER_DOWN touchX=1050.0 touchY=1217.0 index=0 id=0
2020-12-10 16:40:45.268 1795-1795/? E/action: action=ACTION_POINTER_UP touchX=1050.0 touchY=1217.0 index=1 id=1
2020-12-10 16:40:45.336 1795-1795/? E/action: action=ACTION_UP touchX=1050.0 touchY=1217.0 index=0 id=0

·从而计算触摸手指Id 对应的Y 坐标:event.findPointerIndex(mActivePointerId)

支持多点触控
1.获取多手指触点均值
2.使用mLastTouchY记录此均值,用于下拉状态下,多手指触摸时,坐标计算

多点触摸相关代码

代码语言:javascript复制
      // <editor-fold desc="多点触摸计算代码">
        // ---------------------------------------------------------------------------
        // 多点触摸计算代码
        // ---------------------------------------------------------------------------
        final int action = e.getActionMasked();
        // step 1 多个手指时,判断手指抬起事件
        final boolean pointerUp = action == MotionEvent.ACTION_POINTER_UP;
        // step 2 获取抬起手指的index
        final int skipIndex = pointerUp ? e.getActionIndex() : -1;

        //  Determine focal point
        // step 3 计算 X,Y 坐标的sum值
        float sumX = 0, sumY = 0;
        final int count = e.getPointerCount();
        for (int i = 0; i < count; i  ) {
            if (skipIndex == i) {
                continue;
            }
            sumX  = e.getX(i);
            sumY  = e.getY(i);
        }
        // step 4 计算坐标均值
        final int div = pointerUp ? count - 1 : count;
        final float touchX = sumX / div;
        final float touchY = sumY / div;
        // step 5 在发生多指触控   滑动 条件下,当前的mTouchY = 本次滑动均值 - 上次滑动均值
        if ((action == MotionEvent.ACTION_POINTER_UP || action == MotionEvent.ACTION_POINTER_DOWN)
                && mIsBeingDragged) {
            mTouchY  = touchY - mLastTouchY;
            // 即 mTouchY = mTouchY   (touchY - mLastTouchY);
        }
        // step 6消费Touch均值后,将其赋值为mLastTouchY。
        mLastTouchX = touchX;
        mLastTouchY = touchY;
        // ---------------------------------------------------------------------------
        // </editor-fold>

Step5 基于状态模式的下拉刷新及视图位移

下拉刷新与上推加载更多,是对称的操作,这里仅详细介绍下拉刷新和对应的视图位移。

除了上一节的多点触控,其他常用的ACTION包含

·ACTION_DOWN TouchEvent事件的起点,一般ACTION_DOWN 事件被谁handled,后续的事件,均由其接收。

·ACTION_MOVE 手指Touch过程中,会持续收到的事件。

·ACTION_UP 事件结束,即松手

·ACTION_CANCEL 即事件在move过程中,被父View拦截,则会收到ACTION_CANCEL事件

ACTION_DOWN

ACTION_DOWN事件主要进行状态重置,详见上图代码注释。

ACTION_MOVE

ACTION_MOVE 事件完成spinner计算,overScroll滑动,下拉刷新(即视图位移)等操作。

step1 计算dx、dy
step3 内容滚动,还是下拉刷新 —— 子View 是否消费此次move事件?

举个例子:

代码语言:javascript复制
   if (mEnableClipHeaderWhenFixedBehind && mRefreshHeader.getSpinnerStyle() == SpinnerStyle.FixedBehind) {
                    canvas.save();
                    canvas.clipRect(child.getLeft(), child.getTop(), child.getRight(), bottom);
                    boolean ret = super.drawChild(canvas, child, drawingTime);
                    canvas.restore();
                    return ret;
                }

防止内存泄漏 - onDetachedFromWindow()

代码语言:javascript复制
 @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        moveSpinner(0, false);
        notifyStateChanged(RefreshState.None);
        mHandler.removeCallbacksAndMessages(null);
        mHandler = null;
        mManualLoadMore = true;
        mManualNestedScrolling = true;
        animationRunnable = null;
        if (reboundAnimator != null) {
            reboundAnimator.removeAllListeners();
            reboundAnimator.removeAllUpdateListeners();
            reboundAnimator.cancel();
            reboundAnimator = null;
        }
    }

除了上述基本定制内容,还有嵌套滑动,越界回弹效果等等。

事件分发核心 - 从dispatchTouchEvent开始...

代码语言:javascript复制
True if the event was handled by the view, false otherwise.

SmartRefreshLayout是个自定义View,它内部的事件分发的重心是处理当前 Group 和子 View 之间的逻辑关系:

·是否需要拦截 touch 事件;

·是否需要将 touch 事件继续分发给子 View;

·如何将 touch 事件分发给子 View。

·根据Event 下拉,或者上推,或者滑动。

step1:多点触控

假如不处理多点触摸事件,会发生什么?

我们写支持手指滑动操作的控件时,当你一根手指操作你发现没有问题,但是当多根手指的时候,会有一些问题。

step 5 滑动状态,通知父View不要拦截事件
代码语言:javascript复制
dy = touchY - mTouchY;// 调整 mTouchSlop 偏差 重新计算 dy
if (mSuperDispatchTouchEvent) {// 如果父类拦截了事件,发送一个取消事件通知
    e.setAction(MotionEvent.ACTION_CANCEL);
    super.dispatchTouchEvent(e);
}
...
   getParent().requestDisallowInterceptTouchEvent(true);// 通知父控件不要拦截事件
step 6 移动视图 moveSpinnerInfinitely
滑动阻尼 - 实现丝滑的下拉效果

spinner :根据持续产生的move事件的dy值,累加产生的下拉偏移

那么手指偏移1px,Header就偏移1px吗?

SmartRefresh的阻尼相关参数有两个

DragRate = 显示拖动距离 / 手指真实拖动距离 (要求<= 1,越小阻尼越大) MaxDragRate = 最大拖动距离 / Header或者Footer的高度 (要求>=1,越大阻尼越小)

相关方法

name

format

description

setDragRate

dimension

设置拖动比率

setHeaderMaxDragRate

float

Header最大拖动距离与HeaderHeight的比率(默认2.5)

相关属性

name

format

description

srlDragRate

dimension

设置拖动比率

srlHeaderMaxDragRate

float

Header最大拖动距离与HeaderHeight的比率(默认2.5)

阻尼公式 y = M(1-100^(-x/H))

value

含义

M

指当前允许下拉的最大高度

H

控件高度

x

物理偏移值spinner * 拖动比率

y

y = M(1-100^(-x/H))

很显然,随着spinner 增大,100^(-x/H) 这个幂次函数无限接近于0。则y值,无限趋近于M,也就是当前case允许下拉的最大值。其他case大同小异。

moveSpinnerInfinitely

代码语言:javascript复制
else if (spinner >= 0) {
            final double M = mHeaderExtendHeight   mHeaderHeight - mHeaderTranslationY;
            final double H = Math.max(mScreenHeightPixels / 2, getHeight());
            final double x = Math.max(0, spinner * mDragRate);
            final double y = Math.min(M * (1 - Math.pow(100, -x / (H == 0 ? 1 : H))), x);//  公式 y = M(1-100^(-x/H))
            moveSpinner((int) y, false);
        }
视图移动 - mRefreshHeader.getView().setTranslationY()

根据上述代码,可以发现进行视图移送的是moveSpinner方法,moveSpinnerInifitely 仅是 按照 物理偏移值 当前状态 区分case,计算真正拖动值。

以业务使用的 SpinnerStyle.Translate 变换方式来看:

moveSpinner

代码语言:javascript复制
            // 启用下拉刷新   松手后进入Refreshing状态前的回弹动画ing
            if (isEnableRefresh() || (mState == RefreshState.RefreshFinish && isAnimator)) {
                if (oldSpinner != mSpinner) {
                    if (mRefreshHeader.getSpinnerStyle() == SpinnerStyle.Translate) {
                        // Header 位移
                        mRefreshHeader.getView().setTranslationY(mSpinner  mHeaderTranslationY);
                    } else if (mRefreshHeader.getSpinnerStyle() == SpinnerStyle.Scale) {
                        mRefreshHeader.getView().requestLayout();
                    }
                    if (isAnimator) {
                        // 通知Refreshheader,正在回弹
                        mRefreshHeader.onReleasing(percent, offset, headerHeight, extendHeight);
                    }
                }
                if (!isAnimator) {
                    if (mRefreshHeader.isSupportHorizontalDrag()) {
                        final int offsetX = (int) mLastTouchX;
                        final int offsetMax = getWidth();
                        final float percentX = mLastTouchX / (offsetMax == 0 ? 1 : offsetMax);
                        mRefreshHeader.onHorizontalDrag(percentX, offsetX, offsetMax);
                        mRefreshHeader.onPulling(percent, offset, headerHeight, extendHeight);
                    } else if (oldSpinner != mSpinner) {
                        // 通知Refreshheader,正在拖动
                        mRefreshHeader.onPulling(percent, offset, headerHeight, extendHeight);
                    }
                }
            }
状态切换 moveSpinner

影响状态切换的主要因素为 当前状态 下拉偏移值 (松手或者拖动)。

代码语言:javascript复制
    if (!isAnimator && mViceState.dragging) {
            if (mSpinner > mHeaderHeight * mHeaderTriggerRate) {
                if (mState != RefreshState.ReleaseToTwoLevel) {
                    mKernel.setState(RefreshState.ReleaseToRefresh);
                }
            } else if (-mSpinner > mFooterHeight * mFooterTriggerRate && !mFooterNoMoreData) {
                mKernel.setState(RefreshState.ReleaseToLoad);
            } else if (mSpinner < 0 && !mFooterNoMoreData) {
                mKernel.setState(RefreshState.PullUpToLoad);
            } else if (mSpinner > 0) {
                mKernel.setState(RefreshState.PullDownToRefresh);
            }
        }

下拉过程中,回调RefreshHeader.onPulling(),松手后,回调RefreshHeader.onReleasing()。

当前业务的Header动画,即是基于状态切换及其偏移值 分阶段进行的动画。

视图位移调用链 dispatchTouchEvent()→ RefreshContentWrapper.canRefresh() → moveSpinnerInfinitely → moveSpinner → (RefreshHeader.onPulling()RefreshHeader.onReleasing()...)
状态切换调用链 dispatchTouchEvent()→ RefreshContentWrapper.canRefresh() → moveSpinnerInfinitely → moveSpinner → RefreshKernel.setState → notifyStateChange→ RefreshHeader.onStateChanged
ACTION_UP & ACTION_CANCEL
overScroll滑动放到下个小节
overSpinner()- 松手后如何进行下一步?

overSpinner

代码语言:javascript复制
    /*
     * 手势拖动结束
     * 开始执行回弹动画
     */
    protected void overSpinner() {
        if (mState == RefreshState.TwoLevel) {
            if (mVelocityTracker.getYVelocity() > -1000 && mSpinner > getMeasuredHeight() / 2) {
                ValueAnimator animator = animSpinner(getMeasuredHeight());
                if (animator != null) {
                    animator.setDuration(mFloorDuration);
                }

            } else if (mIsBeingDragged) {
                mKernel.finishTwoLevel();
            }
        } else if (mState == RefreshState.Loading
                //                 || (mEnableAutoLoadMore && !mFooterNoMoreData && mSpinner < 0 && isEnableLoadMore()
                //  && mState != RefreshState.Refreshing)
                || (mEnableFooterFollowWhenLoadFinished && mFooterNoMoreData && mSpinner < 0 && isEnableLoadMore())) {
            if (mSpinner < -mFooterHeight) {
                //                 mTotalUnconsumed = -mFooterHeight;
                animSpinner(-mFooterHeight);
            } else if (mSpinner > 0) {
                //                 mTotalUnconsumed = 0;
                animSpinner(0);
            }
        } else if (mState == RefreshState.Refreshing) {
            if (mSpinner > mHeaderHeight) {
                //                 mTotalUnconsumed = mHeaderHeight;
                animSpinner(mHeaderHeight);
            } else if (mSpinner < 0) {
                //                 mTotalUnconsumed = 0;
                animSpinner(0);
            }
        } else if (mState == RefreshState.PullDownToRefresh) {
            mKernel.setState(RefreshState.PullDownCanceled);
        } else if (mState == RefreshState.PullUpToLoad) {
            mKernel.setState(RefreshState.PullDownCanceled);
        } else if (mState == RefreshState.ReleaseToRefresh) {
            setStateRefreshing();
        } else if (mState == RefreshState.ReleaseToLoad) {
            setStateLoading();
        } else if (mState == RefreshState.ReleaseToTwoLevel) {
            mKernel.setState(RefreshState.TwoLevelReleased);
        } else if (mSpinner != 0) { // 预期外的异常case
            animSpinner(0);
        }
    }
状态切换

状态切换路径见上图。

描述

State

role

role = 1 代表Header的状态 、role = 2 代表Footer的状态

正在拖动状态

PullDownToRefresh PullUpToLoad ReleaseToRefresh ReleaseToLoad ReleaseToTwoLevel

正在刷新状态

Refreshing Loading TwoLevel

正在完成状态

RefreshFinish LoadFinish TwoLevelFinish

·根据上述的讲解,我们发现下拉刷新组件下一步的行为,依赖 (ATCTION 事件 RefreshContent的滑动状态 下拉偏移值 spinner)的变化。

·在不同的条件下,下拉刷新组件可能作出 视图偏移 、刷新并执行刷新动画、进入二楼、回弹动画 等视觉操作。

·而状态模式就是解决“对象的行为依赖于它的状态(属性),并且可以根据它的状态改变而改变它的相关行为” 这类问题。

·因此,SmartRefreshLayout基于状态模式的思想,封装了RefreshState,主要由RefreshKernal.setState()完成条件转换。

·在经典状态模式里,后续操作由State控制,但是此处通过RefreshInternal接口,将State切换事件,传递给RefreshInternal接口,也就是RefreshHeader,完成对应状态下的操作。后续操作即业务实现。

处理Refreshing状态

setStateRefreshing() - 当Header进入刷新状态

1.notifyStateChange进入RefreshReleased状态

2.调用Header的onAnimatorStart回调,通知Header做刷新时的loading动画。

3.执行Header的onRelease回调,通知Header 发生了松手操作

4.通过animSpinner函数,执行属性动画,将Header高度移动到mReboundHeight(即回弹高度)

5.动画结束之后,才通过notifyStateChange进入Refreshing状态,此处调用RefreshListener.onRefresh(),即业务做下拉刷新的地方(注意:假如未设置RefreshListener,则使用默认,即3S后结束动画)

回弹动画 - animSpinner() - 根据属性动画差值器的计算mSpinner位移,并使用moveSpinner做位移。

回弹动画在两个场景下发生,分别是

·进入Refreshing状态,回弹到mReboundHeight
·结束刷新,回弹到0
代码语言:javascript复制
             reboundAnimator = ValueAnimator.ofInt(mSpinner, endSpinner); // 属性动画,修改spinner值
              ...
            reboundAnimator.addListener(new AnimatorListenerAdapter() {
                ...
                @Override
                public void onAnimationEnd(Animator animation) {
                    reboundAnimator = null;
                    // 假如动画结束后,Spinner = 0,则回到none状态
                    if (mSpinner == 0) { 
                        if (mState != RefreshState.None && !mState.opening) {
                            notifyStateChanged(RefreshState.None);
                        }
                    } else if (mState != mViceState) {
                        setViceState(mState);
                    }
                }
            });
            reboundAnimator.addUpdateListener(new AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    //根据差值器计算的结果,做视图位移
                    moveSpinner((int) animation.getAnimatedValue(), true);
                }
            });

·ACTION_DOWN TouchEvent事件的起点,一般ACTION_DOWN 事件被谁handled,后续的事件,均由其接收。

·ACTION_MOVE 手指Touch过程中,会持续收到的事件。

·ACTION_UP 事件结束,即松手

·ACTION_CANCEL 即事件在move过程中,被父View拦截,则会收到ACTION_CANCEL事件

ACTION_DOWN

ACTION_DOWN事件主要进行状态重置,详见上图代码注释。

ACTION_MOVE

ACTION_MOVE 事件完成spinner计算,overScroll滑动,下拉刷新(即视图位移)等操作。

step1 计算dx、dy
step3 内容滚动,还是下拉刷新 —— 子View 是否消费此次move事件?

举个例子,当页面处于初始状态,此时上推,会发现RecyclerView向上滑动。

持续下拉,回到初始状态 → 进入下拉刷新状态 ...

那么,如何判断当前事件是交给子View处理滑动,还是SmartRefreshLayout处理下拉刷新呢?

根据上图ACTION_MOVE step 2 可以知道,ACTION_MOVE 会判断,当前是否应该进入滑动状态。

代码语言:javascript复制
  case MotionEvent.ACTION_MOVE:
                float dx = touchX - mTouchX;
                float dy = touchY - mTouchY; // step1 获取两个ACTION之间的滑动间距
                if (!mIsBeingDragged && mDragDirection != 'h') {// 没有拖动之前,检测  canRefresh canLoadMore 来开启拖动
                    if (mDragDirection == 'v' || (Math.abs(dy) >= mTouchSlop && Math.abs(dx) < Math
                            .abs(dy))) {// 滑动允许最大角度为45度 // // step 2 当前未进入滑动状态且为纵向滑动
                        mDragDirection = 'v';
                        // step 3 RefreshContentWrapper.canRefresh() 将判断父控件可否滑动的权利,交给子View去判断。
                        if (dy > 0 && (mSpinner < 0 || ((mEnableOverScrollDrag || isEnableRefresh()) && mRefreshContent
                                .canRefresh()))) {
                            mIsBeingDragged = true;
                            mTouchY = touchY - mTouchSlop;// 调整 mTouchSlop 偏差
                        }

RefreshWrapper最终调用ScrollBoundaryUtil.canRefresh(View, MotionEvent) 判断子View是否消费了此事件,消费此事件,则返回false。即不能进行下拉刷新。

此递归函数思路比较明确

1.遍历子View,找到包含MotionEvent事件点击坐标所在View,如果包含 canScrollUp(targetView) && targetView.getVisibility() == View.VISIBLE 的View,则消费此事件,如果不包含则继续递归。

2.大递归结束后,没有任何符合上述条件的View,则不消费事件。

代码语言:javascript复制
// 你肯定很疑惑,此函数的MotionEvent 来自哪里?
 在本章节开始,第一张大图上的step2,发生ActionDown事件时,将其传递给RefreshContentWrapper,并持有此MotionEvent。
发生ActionUp或者ActionCancel事件时,RefreshContentWrapper清空MotionEvent。

public static boolean canRefresh(View targetView, MotionEvent event) {
        if (canScrollUp(targetView) && targetView.getVisibility() == View.VISIBLE) {
            return false;
        }
        // event == null 时 canRefresh 不会动态递归搜索
        if (targetView instanceof ViewGroup && event != null) {
            ViewGroup viewGroup = (ViewGroup) targetView;
            final int childCount = viewGroup.getChildCount();
            PointF point = new PointF();
            for (int i = childCount; i > 0; i--) {
                View child = viewGroup.getChildAt(i - 1);
                if (isTransformedTouchPointInView(viewGroup, child, event.getX(), event.getY(), point)) {
                    event = MotionEvent.obtain(event);
                    event.offsetLocation(point.x, point.y);
                    return canRefresh(child, event);
                }
            }
        }
        return true;
    }
step 5 滑动状态,通知父View不要拦截事件
代码语言:javascript复制
dy = touchY - mTouchY;// 调整 mTouchSlop 偏差 重新计算 dy
if (mSuperDispatchTouchEvent) {// 如果父类拦截了事件,发送一个取消事件通知
    e.setAction(MotionEvent.ACTION_CANCEL);
    super.dispatchTouchEvent(e);
}
...
   getParent().requestDisallowInterceptTouchEvent(true);// 通知父控件不要拦截事件
step 6 移动视图 moveSpinnerInfinitely
滑动阻尼 - 实现丝滑的下拉效果

spinner :根据持续产生的move事件的dy值,累加产生的下拉偏移

那么手指偏移1px,Header就偏移1px吗?

SmartRefresh的阻尼相关参数有两个

DragRate = 显示拖动距离 / 手指真实拖动距离 (要求<= 1,越小阻尼越大) MaxDragRate = 最大拖动距离 / Header或者Footer的高度 (要求>=1,越大阻尼越小)

相关方法

name

format

description

setDragRate

dimension

设置拖动比率

setHeaderMaxDragRate

float

Header最大拖动距离与HeaderHeight的比率(默认2.5)

相关属性

name

format

description

srlDragRate

dimension

设置拖动比率

srlHeaderMaxDragRate

float

Header最大拖动距离与HeaderHeight的比率(默认2.5)

阻尼公式 y = M(1-100^(-x/H))

value

含义

M

指当前允许下拉的最大高度

H

控件高度

x

物理偏移值spinner * 拖动比率

y

y = M(1-100^(-x/H))

很显然,随着spinner 增大,100^(-x/H) 这个幂次函数无限接近于0。则y值,无限趋近于M,也就是当前case允许下拉的最大值。其他case大同小异。

moveSpinnerInfinitely

代码语言:javascript复制
else if (spinner >= 0) {
            final double M = mHeaderExtendHeight   mHeaderHeight - mHeaderTranslationY;
            final double H = Math.max(mScreenHeightPixels / 2, getHeight());
            final double x = Math.max(0, spinner * mDragRate);
            final double y = Math.min(M * (1 - Math.pow(100, -x / (H == 0 ? 1 : H))), x);//  公式 y = M(1-100^(-x/H))
            moveSpinner((int) y, false);
        }
视图移动 - mRefreshHeader.getView().setTranslationY()

根据上述代码,可以发现进行视图移送的是moveSpinner方法,moveSpinnerInifitely 仅是 按照 物理偏移值 当前状态 区分case,计算真正拖动值。

以业务使用的 SpinnerStyle.Translate 变换方式来看:

moveSpinner

代码语言:javascript复制
            // 启用下拉刷新   松手后进入Refreshing状态前的回弹动画ing
            if (isEnableRefresh() || (mState == RefreshState.RefreshFinish && isAnimator)) {
                if (oldSpinner != mSpinner) {
                    if (mRefreshHeader.getSpinnerStyle() == SpinnerStyle.Translate) {
                        // Header 位移
                        mRefreshHeader.getView().setTranslationY(mSpinner  mHeaderTranslationY);
                    } else if (mRefreshHeader.getSpinnerStyle() == SpinnerStyle.Scale) {
                        mRefreshHeader.getView().requestLayout();
                    }
                    if (isAnimator) {
                        // 通知Refreshheader,正在回弹
                        mRefreshHeader.onReleasing(percent, offset, headerHeight, extendHeight);
                    }
                }
                if (!isAnimator) {
                    if (mRefreshHeader.isSupportHorizontalDrag()) {
                        final int offsetX = (int) mLastTouchX;
                        final int offsetMax = getWidth();
                        final float percentX = mLastTouchX / (offsetMax == 0 ? 1 : offsetMax);
                        mRefreshHeader.onHorizontalDrag(percentX, offsetX, offsetMax);
                        mRefreshHeader.onPulling(percent, offset, headerHeight, extendHeight);
                    } else if (oldSpinner != mSpinner) {
                        // 通知Refreshheader,正在拖动
                        mRefreshHeader.onPulling(percent, offset, headerHeight, extendHeight);
                    }
                }
            }
状态切换 moveSpinner

影响状态切换的主要因素为 当前状态 下拉偏移值 (松手或者拖动)。

代码语言:javascript复制
    if (!isAnimator && mViceState.dragging) {
            if (mSpinner > mHeaderHeight * mHeaderTriggerRate) {
                if (mState != RefreshState.ReleaseToTwoLevel) {
                    mKernel.setState(RefreshState.ReleaseToRefresh);
                }
            } else if (-mSpinner > mFooterHeight * mFooterTriggerRate && !mFooterNoMoreData) {
                mKernel.setState(RefreshState.ReleaseToLoad);
            } else if (mSpinner < 0 && !mFooterNoMoreData) {
                mKernel.setState(RefreshState.PullUpToLoad);
            } else if (mSpinner > 0) {
                mKernel.setState(RefreshState.PullDownToRefresh);
            }
        }

下拉过程中,回调RefreshHeader.onPulling(),松手后,回调RefreshHeader.onReleasing()。

当前业务的Header动画,即是基于状态切换及其偏移值 分阶段进行的动画。

视图位移调用链 dispatchTouchEvent()→ RefreshContentWrapper.canRefresh() → moveSpinnerInfinitely → moveSpinner → (RefreshHeader.onPulling()RefreshHeader.onReleasing()...)
状态切换调用链 dispatchTouchEvent()→ RefreshContentWrapper.canRefresh() → moveSpinnerInfinitely → moveSpinner → RefreshKernel.setState → notifyStateChange→ RefreshHeader.onStateChanged
ACTION_UP & ACTION_CANCEL
overScroll滑动放到下个小节
overSpinner()- 松手后如何进行下一步?

overSpinner

代码语言:javascript复制
    /*
     * 手势拖动结束
     * 开始执行回弹动画
     */
    protected void overSpinner() {
        if (mState == RefreshState.TwoLevel) {
            if (mVelocityTracker.getYVelocity() > -1000 && mSpinner > getMeasuredHeight() / 2) {
                ValueAnimator animator = animSpinner(getMeasuredHeight());
                if (animator != null) {
                    animator.setDuration(mFloorDuration);
                }

            } else if (mIsBeingDragged) {
                mKernel.finishTwoLevel();
            }
        } else if (mState == RefreshState.Loading
                //                 || (mEnableAutoLoadMore && !mFooterNoMoreData && mSpinner < 0 && isEnableLoadMore()
                //  && mState != RefreshState.Refreshing)
                || (mEnableFooterFollowWhenLoadFinished && mFooterNoMoreData && mSpinner < 0 && isEnableLoadMore())) {
            if (mSpinner < -mFooterHeight) {
                //                 mTotalUnconsumed = -mFooterHeight;
                animSpinner(-mFooterHeight);
            } else if (mSpinner > 0) {
                //                 mTotalUnconsumed = 0;
                animSpinner(0);
            }
        } else if (mState == RefreshState.Refreshing) {
            if (mSpinner > mHeaderHeight) {
                //                 mTotalUnconsumed = mHeaderHeight;
                animSpinner(mHeaderHeight);
            } else if (mSpinner < 0) {
                //                 mTotalUnconsumed = 0;
                animSpinner(0);
            }
        } else if (mState == RefreshState.PullDownToRefresh) {
            mKernel.setState(RefreshState.PullDownCanceled);
        } else if (mState == RefreshState.PullUpToLoad) {
            mKernel.setState(RefreshState.PullDownCanceled);
        } else if (mState == RefreshState.ReleaseToRefresh) {
            setStateRefreshing();
        } else if (mState == RefreshState.ReleaseToLoad) {
            setStateLoading();
        } else if (mState == RefreshState.ReleaseToTwoLevel) {
            mKernel.setState(RefreshState.TwoLevelReleased);
        } else if (mSpinner != 0) { // 预期外的异常case
            animSpinner(0);
        }
    }
状态切换

状态切换路径见上图。

描述

State

role

role = 1 代表Header的状态 、role = 2 代表Footer的状态

正在拖动状态

PullDownToRefresh PullUpToLoad ReleaseToRefresh ReleaseToLoad ReleaseToTwoLevel

正在刷新状态

Refreshing Loading TwoLevel

正在完成状态

RefreshFinish LoadFinish TwoLevelFinish

·根据上述的讲解,我们发现下拉刷新组件下一步的行为,依赖 (ATCTION 事件 RefreshContent的滑动状态 下拉偏移值 spinner)的变化。

·在不同的条件下,下拉刷新组件可能作出 视图偏移 、刷新并执行刷新动画、进入二楼、回弹动画 等视觉操作。

·而状态模式就是解决“对象的行为依赖于它的状态(属性),并且可以根据它的状态改变而改变它的相关行为” 这类问题。

·因此,SmartRefreshLayout基于状态模式的思想,封装了RefreshState,主要由RefreshKernal.setState()完成条件转换。

·在经典状态模式里,后续操作由State控制,但是此处通过RefreshInternal接口,将State切换事件,传递给RefreshInternal接口,也就是RefreshHeader,完成对应状态下的操作。后续操作即业务实现。

处理Refreshing状态

setStateRefreshing() - 当Header进入刷新状态

1.notifyStateChange进入RefreshReleased状态

2.调用Header的onAnimatorStart回调,通知Header做刷新时的loading动画。

3.执行Header的onRelease回调,通知Header 发生了松手操作

4.通过animSpinner函数,执行属性动画,将Header高度移动到mReboundHeight(即回弹高度)

5.动画结束之后,才通过notifyStateChange进入Refreshing状态,此处调用RefreshListener.onRefresh(),即业务做下拉刷新的地方(注意:假如未设置RefreshListener,则使用默认,即3S后结束动画)

回弹动画 - animSpinner() - 根据属性动画差值器的计算mSpinner位移,并使用moveSpinner做位移。

回弹动画在两个场景下发生,分别是

·进入Refreshing状态,回弹到mReboundHeight
·结束刷新,回弹到0
代码语言:javascript复制
             reboundAnimator = ValueAnimator.ofInt(mSpinner, endSpinner); // 属性动画,修改spinner值
              ...
            reboundAnimator.addListener(new AnimatorListenerAdapter() {
                ...
                @Override
                public void onAnimationEnd(Animator animation) {
                    reboundAnimator = null;
                    // 假如动画结束后,Spinner = 0,则回到none状态
                    if (mSpinner == 0) { 
                        if (mState != RefreshState.None && !mState.opening) {
                            notifyStateChanged(RefreshState.None);
                        }
                    } else if (mState != mViceState) {
                        setViceState(mState);
                    }
                }
            });
            reboundAnimator.addUpdateListener(new AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    //根据差值器计算的结果,做视图位移
                    moveSpinner((int) animation.getAnimatedValue(), true);
                }
            });
interceptByAnimator

在动画执行时,触摸屏幕,打断动画,转为拖动状态

关于动画,有个比较有意思的地方。因此,disptachTouchEvent 方法case1的位置,做拦截,不处理当前event。

调用链 下拉(moveSpinnerInfinitely) - 刷新(setStateRefreshing|animSpinner) - 松手(overSpinner) - 回弹(animSpinner)

越界回弹

ACTION_UP startFlingIfNeed

当手指抬起,完成一个事件后,会主动判断当前是否启用了越界回弹。

启用状态下才会根据滑动速度,执行Scroller.fling操作。

代码语言:javascript复制
        if (Math.abs(velocity) > mMinimumVelocity) {
            if ((velocity < 0 && ((mEnableOverScrollBounce && (mEnableOverScrollDrag || isEnableLoadMore())) || (
                    mState == RefreshState.Loading && mSpinner >= 0) || (mEnableAutoLoadMore && isEnableLoadMore())))
                    || (velocity > 0 && ((mEnableOverScrollBounce && (mEnableOverScrollDrag || isEnableRefresh())) || (
                    mState == RefreshState.Refreshing && mSpinner <= 0)))) {
                mVerticalPermit = false;// 关闭竖直通行证
                // 并没有做动画,根据速度计算了fling的事件长度,滑动间距等。
                mScroller.fling(0, 0, 0, (int) -velocity, 0, 0, -Integer.MAX_VALUE, Integer.MAX_VALUE);
                // 获得控制新位置
                mScroller.computeScrollOffset();
                // invalidate();
                invalidate();
            }
          
        }
Scroller VelocityTracker 惯性地动起来

自定义View想要实现自主滑动,必不可少的两个API

step 1 获得滑动速度边界值,TouchSlop

mMaximumVelocity = ViewConfiguration.get(context).getScaledMaximumFlingVelocity();

mMinimumVelocity = ViewConfiguration.get(context).getScaledMinimumFlingVelocity();

step2 监听Event事件,记录并计算速度,如本控件代码

if (mRefreshContent != null) {    // 为 RefreshContent 传递当前触摸事件的坐标,用于智能判断对应坐标位置View的滚动边界和相关信息    switch (action) {        case MotionEvent.ACTION_DOWN:            mVelocityTracker.clear();            // 记录Event            mVelocityTracker.addMovement(e);            mRefreshContent.onActionDown(e);            mScroller.forceFinished(true);            break;        case MotionEvent.ACTION_MOVE:            if (!mNestedScrollInProgress) {                mVelocityTracker.addMovement(e);            }            break;        case MotionEvent.ACTION_UP:            if (!mNestedScrollInProgress) {                // 计算当前滑动速度                mVelocityTracker.addMovement(e);                mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);            }        case MotionEvent.ACTION_CANCEL:            mRefreshContent.onActionUpOrCancel();    }}

step3 在ActionUp事件处理越界滚动,即startFlingIfNeed。

step 4 滑动开始的地方 -  computeScroll

代码中animBounceRunable,即属性动画 回弹差值器 moveSpinnerInfinitely 完成回弹效果。

篇幅原因,不做赘述。

0 人点赞