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解析的更多相关文章

  1. Atitit 表达式原理 语法分析 原理与实践 解析java的dsl  递归下降是现阶段主流的语法分析方法

    Atitit 表达式原理 语法分析 原理与实践 解析java的dsl  递归下降是现阶段主流的语法分析方法 于是我们可以把上面的语法改写成如下形式:1 合并前缀1 语法分析有自上而下和自下而上两种分析 ...

  2. Dubbo原理和源码解析之服务引用

    一.框架设计 在官方<Dubbo 开发指南>框架设计部分,给出了引用服务时序图: 另外,在官方<Dubbo 用户指南>集群容错部分,给出了服务引用的各功能组件关系图: 本文将根 ...

  3. Dubbo原理和源码解析之标签解析

    一.Dubbo 配置方式 Dubbo 支持多种配置方式: XML 配置:基于 Spring 的 Schema 和 XML 扩展机制实现 属性配置:加载 classpath 根目录下的 dubbo.pr ...

  4. 【原创】大数据基础之Spark(5)Shuffle实现原理及代码解析

    一 简介 Shuffle,简而言之,就是对数据进行重新分区,其中会涉及大量的网络io和磁盘io,为什么需要shuffle,以词频统计reduceByKey过程为例, serverA:partition ...

  5. 【原创】大数据基础之Spark(4)RDD原理及代码解析

    一 简介 spark核心是RDD,官方文档地址:https://spark.apache.org/docs/latest/rdd-programming-guide.html#resilient-di ...

  6. Dubbo原理和源码解析之“微内核+插件”机制

    github新增仓库 "dubbo-read"(点此查看),集合所有<Dubbo原理和源码解析>系列文章,后续将继续补充该系列,同时将针对Dubbo所做的功能扩展也进行 ...

  7. Dubbo原理和源码解析之服务暴露

    github新增仓库 "dubbo-read"(点此查看),集合所有<Dubbo原理和源码解析>系列文章,后续将继续补充该系列,同时将针对Dubbo所做的功能扩展也进行 ...

  8. Laya Timer原理 & 源码解析

    Laya Timer原理 & 源码解析 @author ixenos 2019-03-18 16:26:38 一.原理 1.将所有Handler注册到池中 1.普通Handler在handle ...

  9. 05_XML的解析_01_dom4j 解析

    [简述] Xml文件出了给开发者看,更多情况使用程序读取xml文件里的内容,这叫做xml解析. 根据解析方式分为:DOM解析 和 SAX解析 [解析工具] (一). 使用DOM解析原理的工具: 1.J ...

随机推荐

  1. 如何:确定已安装的 .NET Framework 版本

    用户可在他们的计算机上安装和运行 .NET Framework 的多个版本. 当你开发或部署应用时,你可能需要知道用户的计算机上安装了哪些 .NET Framework 版本. .NET Framew ...

  2. MySQL运行机制原理&架构

    1.MySQL知识普及: MySQL是一个开放源代码的关系数据库管理系统. MySQL架构可以在多种不同场景中应用并发挥良好作用.主要体现在存储引擎的架构上,插件式的存储引擎架构将查询处理和其它的系统 ...

  3. 如何写好demo——学习感悟

    文章标题:教你如何写好Demo应用 如何制作出最有用的demo呢? 简,易 在demo中,我们要专注于单一的主题.我们的教学覆盖了很大的知识范围,因此,化整为零是非常必要的. 例如,我们要说明Andr ...

  4. 2019-ACM-ICPC-南昌区网络赛-H. The Nth Item-特征根法求通项公式+二次剩余+欧拉降幂

    2019-ACM-ICPC-南昌区网络赛-H. The Nth Item-特征根法求通项公式+二次剩余+欧拉降幂 [Problem Description] ​ 已知\(f(n)=3\cdot f(n ...

  5. What Does Reconciliation Mean in Accounting

    Account reconciliation is the process of comparing transactions you have recorded using internal rec ...

  6. Selenium(十三)调用js,控制浏览器的滚动条

    WebDiver 不能操作本地 Windows 控件,但对于浏览器上的控件也不是都可以操作的.比如浏览器的滚动条,虽然 WebDriver 提供操作浏览器的前进和后退按钮,但对于滚动条并没有提供相应用 ...

  7. php遍历目录与文件夹的多种方法详解

    遍历目录或遍历目录下指定类型的文件,这是每一个童鞋在写程序的时候难免会用到的.PHP本身也提供了很多灰常有用的函数,正确地使用它们,不会有错滴.下面就我个人学习过程中的一些总结,希望对想学PHP的童鞋 ...

  8. 数据插入异常,原因是: (1054, "Unknown column '\ufeff95001' in 'field list'")

    今天用python调用本地文本插入数据库时出现标题错误,多了个ufeff. 这涉及的编码知识和各编码之间的转换问题. 方法1:只需在后面加入decode 方法2: 用编辑器打开,选择相应编码 选择UT ...

  9. seastar笔记

    1.seastar::A.then([]{B});C:的核心思想是说我们开始了A之后就开始B,开始B之后就开始C,而不需要等A做完了再做B,B做完再做C 2.通常的return是一个需要执行的函数,例 ...

  10. Python-sokect 示例

    server: #coding=utf-8 import socket _sokect =socket.socket() #创建sokect _host =socket.gethostname() # ...