代码届有一句非常经典的话:“不要重复制造轮子”,多少人看过之后便以此为本,把鲁迅的“拿来主义”发扬光大,只搜轮子,不造轮子。但现在我想补充的一句是“不要重复制造轮子,不等于不需要知道轮子是如何制造的”!
读过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; //当前拉动方向
private void init(Context context){ mState = State.NORMAL; }
@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; }
case MotionEvent.ACTION_DOWN: mLastMotionX = event.getX(); mLastMotionY = event.getY(); mDeltaY = .0F; mDeltaX = .0F; break;
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为正值的时候,需要累减;为负值(用户开始向上滑动),要累加的原因。
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;
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方向上保持不变 } }
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的任务给它 }
通过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(); }
在规格化时间方面,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状态 } }
if(mState == State.REFRESHING){ return true; }
3-10:个人QQ:1291700520,Android Programmer. 如转载、引用等还望注明链接来源,代码下载地址:
https://github.com/anxiaoyi/PullToRefreshTheory
原文地址:http://blog.csdn.net/anxiaoyi520/article/details/41677507