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

时间: 2024-11-08 01:41:49

PullToRefresh原理解析的相关文章

MyBatis框架中Mapper映射配置的使用及原理解析(七) MapperProxy,MapperProxyFactory

从上文<MyBatis框架中Mapper映射配置的使用及原理解析(六) MapperRegistry> 中我们知道DefaultSqlSession的getMapper方法,最后是通过MapperRegistry对象获得Mapper实例: public <T> T getMapper(Class<T> type, SqlSession sqlSession) { final MapperProxyFactory<T> mapperProxyFactory =

Android中微信抢红包插件原理解析和开发实现

一.前言 自从去年中微信添加抢红包的功能,微信的电商之旅算是正式开始正式火爆起来.但是作为Android开发者来说,我们在抢红包的同时意识到了很多问题,就是手动去抢红包的速度慢了,当然这些有很多原因导致了.或许是网络的原因,而且这个也是最大的原因.但是其他的不可忽略的因素也是要考虑到进去的,比如在手机充电锁屏的时候,我们并不知道有人已经开始发红包了,那么这时候也是让我们丧失了一大批红包的原因.那么关于网络的问题,我们开发者可能用相关技术无法解决(当然在Google和Facebook看来的话,他们

MyBatis框架中Mapper映射配置的使用及原理解析(三) 配置篇 Configuration

从上文<MyBatis框架中Mapper映射配置的使用及原理解析(二) 配置篇 SqlSessionFactoryBuilder,XMLConfigBuilder> 我们知道XMLConfigBuilder调用parse()方法解析Mybatis配置文件,生成Configuration对象. Configuration类主要是用来存储对Mybatis的配置文件及mapper文件解析后的数据,Configuration对象会贯穿整个Mybatis的执行流程,为Mybatis的执行过程提供必要的配

MyBatis框架中Mapper映射配置的使用及原理解析(二) 配置篇 SqlSessionFactoryBuilder,XMLConfigBuilder

在 <MyBatis框架中Mapper映射配置的使用及原理解析(一) 配置与使用> 的demo中看到了SessionFactory的创建过程: SqlSessionFactory sessionFactory = null; String resource = "mybatisConfig.xml"; try { sessionFactory = new SqlSessionFactoryBuilder().build(Resources .getResourceAsRea

Spring Boot启动原理解析

Spring Boot启动原理解析http://www.cnblogs.com/moonandstar08/p/6550758.html 前言 前面几章我们见识了SpringBoot为我们做的自动配置,确实方便快捷,但是对于新手来说,如果不大懂SpringBoot内部启动原理,以后难免会吃亏.所以这次博主就跟你们一起一步步揭开SpringBoot的神秘面纱,让它不在神秘. 正文 我们开发任何一个Spring Boot项目,都会用到如下的启动类 从上面代码可以看出,Annotation定义(@Sp

游戏外挂原理解析与制作 - [内存数值修改类 篇一]

本章旨在讲解外挂实现原理,未深入涉及至代码层面.希望能与对这方面感兴趣的朋友多多交流,毕竟理论是死的,套路是固定的,只有破解经验是花大量时间和心血积累的. 对于单机游戏而言,游戏中绝大部分的参数(比如血.蓝.能量亦或是金币)都存储在计算机的堆栈中,一些类似剧情进度的则加密后写入本地的自定义配置文件中: 对于页游.网游和手游,虽然服务器保存了大量的重要的参数,但由于客户端不可避免的需要进行大量的计算和资源的加载,本地内存种必定存有部分的临时变量,通过判断这些变量的变化规律和函数的破密寻到利于自身的

JSONP跨域的原理解析

JSONP跨域的原理解析 一种脚本注入行为 在 2011年10月27日 那天写的     已经有 99238 次阅读了 感谢 参考或原文 JavaScript是一种在Web开发中经常使用的前端动态脚本技术.在JavaScript中,有一个很重要的安全性限制,被称为"Same-Origin Policy"(同源策略).这一策略对于JavaScript代码能够访问的页面内容做了很重要的限制,即JavaScript只能访问与包含它的文档在同一域下的内容. JavaScript这个安全策略在进

经典CSS实现三角形图标原理解析

前言: 在写这篇文章之前,我也看过很多前端大神写的代码,But,都只是粘贴代码和给出显示效果,对于初学者来说大家都喜欢刨根问底,为什么要这样做呢? 接下来就让我给大家分享一下我对CSS实现三角形的理解: border边框语法: border 四条边框设置 border-left 设置左边框,一般单独设置左边框样式使用 border-right 设置右边框,一般单独设置右边框样式使用 border-top 设置上边框,一般单独设置上边框样式使用 border-bottom 设置下边框,一般单独设置

Request 接收参数乱码原理解析二:浏览器端编码原理

上一篇<Request 接收参数乱码原理解析一:服务器端解码原理>,分析了服务器端解码的过程,那么浏览器是根据什么编码的呢? 1. 浏览器解码 浏览器根据服务器页面响应Header中的“Content-Type: text/html; charset=gb2312”解码.修改web.config中“responseEncoding=utf-8”,发现服务器页面响应Header变成了“Content-Type: text/html; charset=utf8”. <system.web&g