前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >SmartRefreshLayout dispatchTouchEvent 解读

SmartRefreshLayout dispatchTouchEvent 解读

原创
作者头像
Erossssssss
修改2021-04-12 10:20:54
1.6K0
修改2021-04-12 10:20:54
举报
文章被收录于专栏:Eleanor的专栏Eleanor的专栏

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 完成回弹效果。

篇幅原因,不做赘述。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • SmartRefreshLayout 集成
    • 集成SmartRefreshLayout
      • 在XML中声明该布局
        • 在 Activity 或者 Fragment 中添加代码
        • SmartRefreshLayout体系结构
          • 类图
            • 开放接口
            • 自定义ViewGroup
              • 属性变量
                • 智能识别 - onFinishInflate()
                  • 默认填充 - onAttachedToWindow()
                    • onMeasure() - onLayout()
                      • OverDraw 优化 - drawChild()
                        • 防止内存泄漏 - onDetachedFromWindow()
                        • 事件分发核心 - 从dispatchTouchEvent开始...
                          • step1:多点触控
                            • 假如不处理多点触摸事件,会发生什么?
                            • MotionEvent.getActionMasked() 和 MotionEvent.getAction 有什么区别
                            • MotionEvent.getY() 和 MotionEvent.getRawY() 的区别
                            • MotionEvent.getActionIndex()
                            • 支持多点触控
                            • 1.获取多手指触点均值
                            • 2.使用mLastTouchY记录此均值,用于下拉状态下,多手指触摸时,坐标计算
                          • Step5 基于状态模式的下拉刷新及视图位移
                            • ACTION_DOWN
                            • ACTION_MOVE
                          • 防止内存泄漏 - onDetachedFromWindow()
                          • 事件分发核心 - 从dispatchTouchEvent开始...
                            • step1:多点触控
                              • 假如不处理多点触摸事件,会发生什么?
                              • 视图位移调用链 dispatchTouchEvent()→ RefreshContentWrapper.canRefresh() → moveSpinnerInfinitely → moveSpinner → (RefreshHeader.onPulling()\RefreshHeader.onReleasing()...)
                              • 状态切换调用链 dispatchTouchEvent()→ RefreshContentWrapper.canRefresh() → moveSpinnerInfinitely → moveSpinner → RefreshKernel.setState → notifyStateChange→ RefreshHeader.onStateChanged
                              • ACTION_UP & ACTION_CANCEL
                              • ACTION_DOWN
                              • ACTION_MOVE
                              • 视图位移调用链 dispatchTouchEvent()→ RefreshContentWrapper.canRefresh() → moveSpinnerInfinitely → moveSpinner → (RefreshHeader.onPulling()\RefreshHeader.onReleasing()...)
                              • 状态切换调用链 dispatchTouchEvent()→ RefreshContentWrapper.canRefresh() → moveSpinnerInfinitely → moveSpinner → RefreshKernel.setState → notifyStateChange→ RefreshHeader.onStateChanged
                              • ACTION_UP & ACTION_CANCEL
                              • 调用链 下拉(moveSpinnerInfinitely) - 刷新(setStateRefreshing|animSpinner) - 松手(overSpinner) - 回弹(animSpinner)
                            • 越界回弹
                              • ACTION_UP startFlingIfNeed
                              • Scroller + VelocityTracker 惯性地动起来
                          相关产品与服务
                          智能识别
                          腾讯云智能识别(Intelligent Identification,II)基于腾讯各实验室最新研究成果,为您提供视频内容的全方位识别,支持识别视频内的人物、语音、文字以及帧标签,对视频进行多维度结构化分析。
                          领券
                          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档