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 完成回弹效果。
篇幅原因,不做赘述。