PullToRefresh原理解析,pulltorefresh解析
PullToRefresh原理解析,pulltorefresh解析
代码届有一句非常经典的话:“不要重复制造轮子”,多少人看过之后便以此为本,把鲁迅的“拿来主义”发扬光大,只搜轮子,不造轮子。但现在我想补充的一句是“不要重复制造轮子,不等于不需要知道轮子是如何制造的”!
读过PullToRefresh的源码之后,我便依照着做了一个小Demo出来,下面就此原理为大家解析一番。究竟是哪句代码实现了如此强大的功能,究竟是哪个方法是贯穿全文上下?
原理:在View中有一个scrollTo方法,可以将整个View移动到指定的位置,PullToRefresh就是重写了onTouchEvent方法来检测用户滑动的偏移距离,然后用滑动距离调用scrollTo方法来实现整个View的上下左右移动的。
先上图:
我的Demo:
手机QQ的是上下可以滑动,我的demo是向上,向下,向左,向右都可以滑动,松手之后,自动回到原来的位置。
注,在我的demo里继承的是FrameLayout进行的重写,同样你也可以选择重写LinearLayout或者其他ViewGroup,你可以在新版的手机QQ中看到有大量的布局都支持类似我这种demo的上下滑动(或者叫做弹动)
1.首先来看所需要的变量:
private float mLastMotionX, mLastMotionY; //记录手指触摸的位置X,Y坐标
private float mDeltaX, mDeltaY;//记录当前手指拉动的X和Y偏移量 private ScrollToHomeRunnable mScrollToHomeRunnable; //一会儿介绍,用来从偏移点回到原点的 private enum State{ //当前出于什么状态:正在刷新?水平拉动?垂直拉动?正常状态?
REFRESHING,
PULLING_HORIZONTAL,
PULLING_VERTICAL,
NORMAL,
} private enum Orientation{ //记录拉动的方向:水平?垂直?
HORIZONTAL,
VERTICAL
} private State mState; //当前状态
private Orientation mOrientation; //当前拉动方向
2.初始化方法,初始化的时候只需要将mState 设置为 NORMAL状态即可。
private void init(Context context){
mState = State.NORMAL;
}
3-1.重写onTouchEvent方法,检测用户滑动的距离,方向等,然后调用scrollTo来让整个View偏移,这可谓是核心代码了,先看伪代码:
@Override
public boolean onTouchEvent(MotionEvent event) {
int action = event.getAction();
switch(action){
case MotionEvent.ACTION_DOWN:
/**
* 记录X,Y坐标,恢复mDeltaX和mDeltaY为0
*/
break; case MotionEvent.ACTION_MOVE:
/**
* 根据当前触摸点 - 上次记录的x或者y坐标,得到增量,然后应用到scrollTo方法上去,
* 然后重新记录x,y坐标
*/
break; case MotionEvent.ACTION_UP:
/**
* 用户松开手指之后,View自动回到偏移量为0的位置
*/
break;
}
return true;
}
3-2:我们先来实现当手指ACTION_DOWN,即刚刚点下的时候,应该做哪些工作?如下代码所示,其实所做的工作非常简单,每次只需要重新记录当前手指的X坐标,Y坐标,并把X偏移量和Y偏移量调整为0即可,每次都从最远点的位置开始拉动。
case MotionEvent.ACTION_DOWN:
mLastMotionX = event.getX();
mLastMotionY = event.getY();
mDeltaY = .0F;
mDeltaX = .0F;
break;
3-3:当用户在屏幕上滑动的时候,即ACTION_MOVE的时候,应该记录用户此次点击的X坐标(或Y坐标)与一开始ACTION_DOWN的时候X坐标(或者Y坐标)的差值,然后记录此差值并调用scrollTo方法:
case MotionEvent.ACTION_MOVE:
float innerDeltaY = event.getY() - mLastMotionY; //记录Y的差值
float innerDeltaX = event.getX() - mLastMotionX; //记录X的差值
float absInnerDeltaY = Math.abs(innerDeltaY); //Y差值绝对值
float absInnerDeltaX = Math.abs(innerDeltaX); //X差值绝对值
//当Y差值绝对值 大于 X差值绝对值的时候,我们可以认为用户正在上下滑动
if(absInnerDeltaY > absInnerDeltaX && mState != State.PULLING_HORIZONTAL){
mOrientation = Orientation.VERTICAL;//将当前滑动方向置为垂直滑动
mState = State.PULLING_VERTICAL;//滑动状态:正在垂直拉动
if(innerDeltaY > 1.0F){ //innerDeltaY为正数,用户正在向下拉动,1.0F可看做阈值,下面类似
mDeltaY -= absInnerDeltaY; //注意这个地方是-=,即累减的过程
pull(mDeltaY);
}else if(innerDeltaY < -1.0F){ //innerDeltaY为负数,用户正在向上拉动
mDeltaY += absInnerDeltaY; //累加
pull(mDeltaY);
}//下面的代码是水平滑动
}else if(absInnerDeltaY < absInnerDeltaX && mState != State.PULLING_VERTICAL){
mOrientation = Orientation.HORIZONTAL;
mState = State.PULLING_HORIZONTAL;
if(innerDeltaX > 1.0F){
mDeltaX -= absInnerDeltaX;
pull(mDeltaX);
}else if(innerDeltaX < -1.0F){
mDeltaX += absInnerDeltaX;
pull(mDeltaX);
}
}
//重新记录新的坐标值
mLastMotionX = event.getX();
mLastMotionY = event.getY();
break;
3-4 大家可能注意到,判断水平还是垂直移动的时候,有一个mState != State.PULLING_XXX 条件,这个条件是为了限制滑动的方向的,即当用户正在处于垂直滑动的时候,就禁止用户水平滑动;当水平滑动的时候就禁止垂直滑动,每次只能按一个方向进行滑动。
在测试的时候,大家即可感受到,向下拉动屏幕的时候,比如偏移值为200,那么如果你想让屏幕真的偏移200,需要调用scrollTo(0, -200),这也是为什么innerDelta为正值的时候,需要累减;为负值(用户开始向上滑动),要累加的原因。
3-5 当用户手指离开的时候,按照PullToRefresh来说就是开始执行用户的任务(比如刷新数据等等操作),任务执行完毕之后,View重新调用scrollTo一定的距离(这个距离就是在ACTION_MOVE阶段,记录的mDeltaY和mDeltaX偏移量)回到自己原来的位置,在我的demo里,只是简单的当用户手指离开之后就立马回到原来的位置:
case MotionEvent.ACTION_UP:
switch(mOrientation){//根据ACTION_MOVE的时候所确定的方向开始判断
case VERTICAL:
smoothScrollTo(mDeltaY);//垂直拉动,让View重新mDeltaY,重新回到Y的原点
break; case HORIZONTAL:
smoothScrollTo(mDeltaX);//如果水平拉动,让View重新滑动mDeltaX,重新回到X的原点
break; default:
break;
}
break;
3-6:用户触摸阶段(ACTION_MOVE)的拉动方法,除以2.0将拉动的距离缩放,这里代表如果用户已知从上滑到下面,那么整个View最多偏移半个屏幕。
private void pull(float diff){
int value = Math.round(diff / 2.0F);//diff就是偏移量,除以2.0相当于一个缩放
if(mOrientation == Orientation.VERTICAL){
scrollTo(0, value);//注意这里是核心了,Y方向上移动value距离,X方向上保持不变
}else if(mOrientation == Orientation.HORIZONTAL){
scrollTo(value, 0);//X方向上移动value距离,Y方向上保持不变
}
}
3-7:用户松手之后(ACTION_UP)的View自动回到原位置的方法:
private void smoothScrollTo(float diff){
int value = Math.round(diff / 2.0F);
mScrollToHomeRunnable = new ScrollToHomeRunnable(value, 0);
mState = State.REFRESHING;//当前状态为正在刷新
post(mScrollToHomeRunnable);//view自身有一个post方法,我们提交一个scrollTo的任务给它
}
3-8-1:下面来看ScrollToHomeRunnable类的定义,ScrollToHomeRunnable就是用来调用scrollTo的,目的就是要使View从X偏移量或者Y偏移量返回到初始位置去,那么为什么要单独的把它封装成一个Runnable类呢?其一是为了调用View自身的View.post(Runnable runnable)和View.postDelayed(Runnable runnable, int delayMills)方法,其二是为了给他一些装饰效果,比如这里的减速差值,模拟一些特殊的效果。
通过post和postDelayed方法我们可以不停的把这个任务放在View自身的消息队列中,以达到不停地调用scrollTo的目的,一旦回到原点之后,我们就停止调用。其实在这里使用Handler.post(Runnable runnable)也可以实现这样的操作(不停的把一个Runnable任务添加到消息队列中去),大家可以试试,但我测试的是Handler没有直接View.post(Runnable)平滑性高。
3-8-2:ScrollToHomeRunnable的构造器,很简单,存储目标值和当前的偏移量,初始化DecelerateInterpolator差值器。
public ScrollToHomeRunnable(int current, int target){
this.target = target;
this.current = current;
mInterpolator = new DecelerateInterpolator();
}
3-8-3:ScrollToHomeRunnable的run方法,在这个地方,原PullToRefresh的作者很聪明,知道调用getInterpolation这个方法,来得到一系列的差值点,以此得到一串不同的滑动距离,比如”102,86,67,53,40,32,22,15,9,5,2,0“或者”-178,-157,-130,-107,-84,-65,-49,-35,-22,-13,-6,-2,0“,就是这么一串值,来让用户感觉到这个”突然性的回到了原来的位置“这种感觉。
在规格化时间方面,PullToRefresh的作者想的也很周到,全部都使用的long这种存储类型,避免使用了float来让软件做较多的float计算,他先将时间差值放大1000倍,然后规格化至(0,1000)中的一个值,然后再缩小得到(0,1.0F)中的一个浮点值,并当做参数调用getInterpolation方法,得到下一个需要移动到哪一个位置,以此不停的确定的delta值,就不停的确定current值,只要scrollTo之后,发现current 依然没有到达target的值,那么就再次调用postDelayed方法,重新scrollTo。
注:代码中200这个值代表的意思是:200ms,即经过200ms返回原位置去。
@Override
public void run() {
if(mStartTime == -1){
mStartTime = System.currentTimeMillis();
}else{
long normalizedTime = (1000 * (System.currentTimeMillis() - mStartTime)) / 200;
normalizedTime = Math.max(Math.min(normalizedTime, 1000), 0);
final int delta = Math.round((current - target)
* mInterpolator.getInterpolation(normalizedTime / 1000f)); current = current - delta;
if(mOrientation == Orientation.HORIZONTAL){
scrollTo(current, 0);//水平scroll
}else if(mOrientation == Orientation.VERTICAL){
scrollTo(0, current);//垂直scroll
}
} if(current != target){//没有回到原点:在经过16毫秒之后继续postDelayed这个任务
postDelayed(this, 16);
}else{
mState = State.NORMAL;//回到原点,mState置为NORMAL状态
}
}
3-9:最后的补充。如果没有上述的差值器的帮忙,以及不是200ms,或者每次current 只是简单的递减的话,用户可以清楚的看到这个不断回到原来位置的过程,在这期间,如果用户点击的话,将会导致mDeltaX和mDeltaY为0,View会立马调到原位置去,虽然这里在特别快的时候来不及点击,但是还是需要在onTouchEvent一开头的地方补充上这句代码,如果当前正处于刷新状态,那么立即返回(禁止用户点击):
if(mState == State.REFRESHING){
return true;
}
3-10:个人QQ:1291700520,Android Programmer. 如转载、引用等还望注明链接来源,代码下载地址:
https://github.com/anxiaoyi/PullToRefreshTheory
PullToRefresh原理解析,pulltorefresh解析的更多相关文章
- Atitit 表达式原理 语法分析 原理与实践 解析java的dsl 递归下降是现阶段主流的语法分析方法
Atitit 表达式原理 语法分析 原理与实践 解析java的dsl 递归下降是现阶段主流的语法分析方法 于是我们可以把上面的语法改写成如下形式:1 合并前缀1 语法分析有自上而下和自下而上两种分析 ...
- Dubbo原理和源码解析之服务引用
一.框架设计 在官方<Dubbo 开发指南>框架设计部分,给出了引用服务时序图: 另外,在官方<Dubbo 用户指南>集群容错部分,给出了服务引用的各功能组件关系图: 本文将根 ...
- Dubbo原理和源码解析之标签解析
一.Dubbo 配置方式 Dubbo 支持多种配置方式: XML 配置:基于 Spring 的 Schema 和 XML 扩展机制实现 属性配置:加载 classpath 根目录下的 dubbo.pr ...
- 【原创】大数据基础之Spark(5)Shuffle实现原理及代码解析
一 简介 Shuffle,简而言之,就是对数据进行重新分区,其中会涉及大量的网络io和磁盘io,为什么需要shuffle,以词频统计reduceByKey过程为例, serverA:partition ...
- 【原创】大数据基础之Spark(4)RDD原理及代码解析
一 简介 spark核心是RDD,官方文档地址:https://spark.apache.org/docs/latest/rdd-programming-guide.html#resilient-di ...
- Dubbo原理和源码解析之“微内核+插件”机制
github新增仓库 "dubbo-read"(点此查看),集合所有<Dubbo原理和源码解析>系列文章,后续将继续补充该系列,同时将针对Dubbo所做的功能扩展也进行 ...
- Dubbo原理和源码解析之服务暴露
github新增仓库 "dubbo-read"(点此查看),集合所有<Dubbo原理和源码解析>系列文章,后续将继续补充该系列,同时将针对Dubbo所做的功能扩展也进行 ...
- Laya Timer原理 & 源码解析
Laya Timer原理 & 源码解析 @author ixenos 2019-03-18 16:26:38 一.原理 1.将所有Handler注册到池中 1.普通Handler在handle ...
- 05_XML的解析_01_dom4j 解析
[简述] Xml文件出了给开发者看,更多情况使用程序读取xml文件里的内容,这叫做xml解析. 根据解析方式分为:DOM解析 和 SAX解析 [解析工具] (一). 使用DOM解析原理的工具: 1.J ...
随机推荐
- 【转载】C#中PadLeft函数按特定字符补足字符串长度
在C#开发过程中字符串String类处理过程中,有时字符串长度不够时,需要在左侧指定特定的字符来补足字符串长度,此时可以使用String类下的PadLeft方法对字符串的左边进行按特定的字符和特定的长 ...
- H5表单新特性
1.HTML5表单新特性之——新的input type <input type=" "> HTML5之前已有的input type: text.password.rad ...
- 大专生自学web前端到找到工作的经验
先做个自我介绍,我13年考上一所很烂专科民办的学校,学的是生物专业,具体的学校名称我就不说出来献丑了.13年我就辍学了,我在那样的学校,一年学费要1万多,但是根本没有人学习,我实在看不到希望,我就退学 ...
- PHP变量的范围
1.局部变量 function test(){ $a=1;//局部变量$a,尽在这个函数内部有效 } echo $a; 2.全局变量 $i=10;//全局变量(外部变量) define('MY_NAM ...
- 微信H5支付(基于Java实现微信H5支付)
微信的H5支付区别与APP支付,主要在于预下单(返回的参数不一样),其它大体相同(基本没什么区别,区别在于有些人加密喜欢用MD5有些人喜欢用官方提供的加密方式加密,我用的是官方的),贴一下H5支付预下 ...
- Mongodb创建用户Error: couldn’t add user: Use of SCRAM-SHA-256 requires undigested passwords
解决方案:修改mechanisms加密方式为SCRAM-SHA-1 db.createUser({ user: "admin", pwd: "xxx", rol ...
- G1垃圾收集器角色划分与重要概念详解【纯理论】
继续接着上一次[https://www.cnblogs.com/webor2006/p/11129326.html]对G1进行理论化的学习,上一次学到了G1收集器的堆结构,回忆下: 接着继续对它进行了 ...
- mybatis3.1-[topic-16-17]-映射文件_增删改查_insert_获取自增主键的值
笔记要点出错分析与总结 /**测试第16章的增,删,改 的内容 * 错误1: <insert id="addEmp" parameterType="com.bean ...
- UML再论关系extend和include
我在画用例图时,图中既有extend关系也有include关系,师父就问我这两种关系的区别,我在画的时候确实查阅了很多资料,可是在问的时候还是回答不上来,这就是这篇博客得来的缘由了. [include ...
- 【Java】debug初级使用(Eclipse)
1.设置.取消断点 双击要设置断点的代码行数字的前面 2.切换成Debug界面 就会发现画面变成了下图这样 以下是调试的界面解说(图源百度) 3.切换为原界面