接上一篇文章的内容,这篇文章主要是Scroller类的应用,在讲具体实例之前,我还有顺便提一个Scroller的问题。
就是fling()方法和startScroll()方法的区别,其实确保已经在上篇文章说得很清楚(注释里面)。
fling没有设置起点坐标和终点坐标,而是根据滑动的起始速度来计算最后会到达的坐标位置。
在了解scroller的使用之前,我们来看一下调用示意图
据我们的了解,我们computeScroll()方法将会在draw()方法中调用。对于一个groupView而言,每次重绘,它会先调用draw()方法,然后调用dispatchDraw(),杂这个方法里面,会逐个对子控件调用drawChild()方法,最后在drawChild()方法里面,我们看到了对computeScroll()方法的调用
protected boolean drawChild(Canvas canvas, View child, long drawingTime) { ...... ...... if (!concatMatrix && canvas.quickReject(cl, ct, cr, cb, Canvas.EdgeType.BW) && (child.mPrivateFlags & DRAW_ANIMATION) == 0) { return more; } child.computeScroll(); final int sx = child.mScrollX; final int sy = child.mScrollY; boolean scalingRequired = false; Bitmap cache = null; ...... ...... }
下面再来说一下写滑动效果步骤
- 重写onTouchEvent()以支持滑动:
- 借助Scroller,并且处理ACTION_UP事件
- 重写computeScroll(),实现View的连续绘制
- 处理ACTION_POINTER_UP事件,解决多指交替滑动跳动的问题
对于步骤一:假设我们只是需要简单的拖动,那么我们在onTouchEvent()方法里面,先获取down的坐标,然后每次move,获得新坐标,再利用scrollBy()方法传入就可以实现拖动的效果
对于步骤二:我们往往利用Scroller是用于当手指离开屏幕以后,滑动效果还会继续进行,所以我们最好在up的时候,调用startScroll()方法。那么怎么在手指离开以后,还知道目标坐标呢,那当然是Scroller的作用,它就是替我们计算坐标的工具。当然我们每次要指定最终目的坐标,这个根据现实的要求不同而不同,在接下来的实例中,目的坐标是下一屏的位置
对于步骤三:在computeScroll()方法里面,不断的scrollTo
对于步骤四:首先我们要清楚多点触控的一个重要调用顺序
- MotionEvent.ACTION_DOWN:在第一个点被按下时触发
- MotionEvent.ACTION_UP:当屏幕上唯一的点被放开时触发
- MotionEvent.ACTION_POINTER_DOWN:当屏幕上已经有一个点被按住,此时再按下其他点时触发。
- MotionEvent.ACTION_POINTER_UP:当屏幕上有多个点被按住,松开其中一个点时触发(即非最后一个点被放开时)。
- MotionEvent.ACTION_MOVE:当有点在屏幕上移动时触发。值得注意的是,由于它的灵敏度很高,而我们的手指又不可能完全静止(即使我们感觉不到移动,但其实我们的手指也在不停地抖动),所以实际的情况是,基本上只要有点在屏幕上,此事件就会一直不停地被触发。
举例来讲:当我们放一个食指到屏幕上时,触发ACTION_DOWN事件;再放一个拇指到屏幕上,触发ACTION_POINTER_DOWN事件;此时再把食指或拇指放开,都会触发ACTION_POINTER_UP事件;再放开最后一个手指,触发ACTION_UP事件;而同时在整个过程中,ACTION_MOVE事件会一直不停地被触发。
具体来说,第四步就是维护一个唯一的手指,我们首先维护第一个按下的手指(在Action_down里面获得),然后每次move,都是依据这个手指通过的坐标。最后action_pointer_up的时候,判断是不是我们维护的手指离开了,如果是,就需要换一只来维护(如果有的话)
下面是一个用于屏幕切换的例子代码
看一下截图(由于是滑动的,没办法截到动态)
我自定义了一个控件,下面是初始化代码
public class MyView extends ViewGroup { public MyView(Context context) { super(context); init(context); } public MyView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(context); } public MyView(Context context, AttributeSet attrs) { super(context, attrs); init(context); } public void init(Context context){ this.context = context; mScroller = new Scroller(context); l1 = new LinearLayout(context); l1.setBackgroundColor(Color.GREEN); l2 = new LinearLayout(context); l2.setBackgroundColor(Color.RED);; l3 = new LinearLayout(context); l3.setBackgroundColor(Color.YELLOW); addView(l1);addView(l2);addView(l3); }
也就是说有三个linearLayout组成myView,我们在onMeassure()方法里面,让每个linearLayout都占据一个屏幕大小
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int w = MeasureSpec.getSize(widthMeasureSpec); int h = MeasureSpec.getSize(heightMeasureSpec); setMeasuredDimension(w, h); int childcount = getChildCount(); for(int i=0;i<childcount;i++){ View child = getChildAt(i); child.measure(MainActivity.screenWidth, MainActivity.screenHeight); } }
在onlayout()方法里面,为三个linearLayout设置位置(按顺序),这样我们每次就只能看到一个
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int startX = 0; int childcount = getChildCount(); for(int i=0;i<childcount;i++){ View child = getChildAt(i); child.layout(startX, 0, startX+MainActivity.screenWidth, MainActivity.screenHeight); startX += MainActivity.screenWidth; } }
然后我们按照上面的说法,开始处理滑动时间,首先是down
float oldX = 0; private VelocityTracker mVelocityTracker; private int mPointerId; @Override public boolean onTouchEvent(MotionEvent event) { if (mVelocityTracker == null) { mVelocityTracker = VelocityTracker.obtain(); } mVelocityTracker.addMovement(event); int action = event.getActionMasked(); switch(action){ case MotionEvent.ACTION_DOWN: float x = event.getX(); // 获取索引为0的手指id mPointerId = event.getPointerId(0);//也就是第一个放下的手指id if(!mScroller.isFinished()){ mScroller.abortAnimation(); } oldX = x; break;
其中有跟多点触控相关的一些代码,大家可以回头再看,这里重要的是,获得down时候的x坐标值,并且强制停止滑动
接下来是move方法,让我们可以实现简单的拖动
case MotionEvent.ACTION_MOVE: // 获取当前手指id所对应的索引,虽然在ACTION_DOWN的时候,我们默认选取索引为0 // 的手指,但当有第二个手指触摸,并且先前有效的手指up之后,我们会调整有效手指 // 屏幕上可能有多个手指,我们需要保证使用的是同一个手指的移动轨迹, // 因此此处不能使用event.getActionIndex()来获得索引 final int pointerIndex = event.findPointerIndex(mPointerId); float mx = event.getX(pointerIndex); int offset = (int)(oldX-mx); scrollBy(offset, 0); oldX = mx; break;
同样,跟多指触控相关的不必理会,这里只是根据新旧坐标,计算出偏移值,然后调用ScrollBy方法进行滑动
接下来是up方法,这里我们说过要进行startScroll(),而目标坐标,我们是经过计算的出来的
case MotionEvent.ACTION_UP: mVelocityTracker.computeCurrentVelocity(1000); //计算速率 float velocityX = mVelocityTracker.getXVelocity(mPointerId); if (velocityX > SNAP_VELOCITY && cur > 0) { //滑动速率达到了一个标准(快速向右滑屏,返回上一个屏幕) 马上进行切屏处理 snapToScreen(cur - 1); }else if(velocityX < - SNAP_VELOCITY && cur < (getChildCount()-1)){////快速向左滑屏,返回下一个屏幕) snapToScreen(cur + 1); } //以上为快速移动的 ,强制切换屏幕 else{ //我们是缓慢移动的,因此先判断是保留在本屏幕还是到下一屏幕 snapToDestination(); } if (mVelocityTracker != null) { mVelocityTracker.recycle(); mVelocityTracker = null; } break;
下面是屏幕切换的具体方法,我们在这个方法里面,可以看到对startScroll的调用
private void snapToDestination(){ int destScreen = (getScrollX() + MainActivity.screenWidth / 2 ) / MainActivity.screenWidth ; snapToScreen(destScreen); } private void snapToScreen(int wcur) { cur = wcur; //防止屏幕越界,即超过屏幕数 if(cur > getChildCount() - 1) cur = getChildCount() - 1 ; //为了达到下一屏幕或者当前屏幕,我们需要继续滑动的距离.根据dx值,可能想左滑动,也可能像又滑动 int dx = cur * MainActivity.screenWidth - getScrollX() ; mScroller.startScroll(getScrollX(), 0, dx, 0,Math.abs(dx) * 2); postInvalidate(); }
最后,还有记得在computScoll方法里面,调用ScrollTo进行实际的滑动
@Override public void computeScroll() { if(mScroller.computeScrollOffset()){ scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); postInvalidate(); } }
OK,只要按照上面的顺序,我们很轻松的写出了手势滑动的代码
再来,我们要处理多指触控问题
case MotionEvent.ACTION_POINTER_UP: // 获取离开屏幕的手指的索引 int pointerIndexLeave = event.getActionIndex(); int pointerIdLeave = event.getPointerId(pointerIndexLeave); //System.out.println("up "+pointerIdLeave); if (pointerIdLeave == mPointerId) { // 离开屏幕的正是目前的有效手指,此处需要重新调整,并且需要重置VelocityTracker int reIndex = pointerIndexLeave == 0 ? 1 : 0; mPointerId = reIndex;//event.getPointerId(reIndex); // 调整触摸位置,防止出现跳动 x = event.getX(reIndex); oldX = x; if (mVelocityTracker != null) mVelocityTracker.recycle(); mVelocityTracker = null; } break;
在Action_point_up事件里面,我检查是否需要更新跟踪的手指,我们在回到前面的代码,可以看到down,move方法我们都对多指触控进行了出来,每次只使用我们跟踪的index来获得当前的X坐标
下面贴出例子的完整代码
package com.example.androidtest; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.View; import android.view.ViewGroup; import android.widget.LinearLayout; import android.widget.Scroller; public class MyView extends ViewGroup { LinearLayout l1,l2,l3; Context context; Scroller mScroller; int cur = 0;//当前页号 public static int SNAP_VELOCITY = 600 ; //最小的滑动速率 public MyView(Context context) { super(context); init(context); } public MyView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(context); } public MyView(Context context, AttributeSet attrs) { super(context, attrs); init(context); } public void init(Context context){ this.context = context; mScroller = new Scroller(context); l1 = new LinearLayout(context); l1.setBackgroundColor(Color.GREEN); l2 = new LinearLayout(context); l2.setBackgroundColor(Color.RED);; l3 = new LinearLayout(context); l3.setBackgroundColor(Color.YELLOW); addView(l1);addView(l2);addView(l3); } float oldX = 0; private VelocityTracker mVelocityTracker; private int mPointerId; @Override public boolean onTouchEvent(MotionEvent event) { if (mVelocityTracker == null) { mVelocityTracker = VelocityTracker.obtain(); } mVelocityTracker.addMovement(event); int action = event.getActionMasked(); switch(action){ case MotionEvent.ACTION_DOWN: float x = event.getX(); // 获取索引为0的手指id mPointerId = event.getPointerId(0);//也就是第一个放下的手指id if(!mScroller.isFinished()){ mScroller.abortAnimation(); } oldX = x; break; case MotionEvent.ACTION_MOVE: // 获取当前手指id所对应的索引,虽然在ACTION_DOWN的时候,我们默认选取索引为0 // 的手指,但当有第二个手指触摸,并且先前有效的手指up之后,我们会调整有效手指 // 屏幕上可能有多个手指,我们需要保证使用的是同一个手指的移动轨迹, // 因此此处不能使用event.getActionIndex()来获得索引 final int pointerIndex = event.findPointerIndex(mPointerId); float mx = event.getX(pointerIndex); int offset = (int)(oldX-mx); scrollBy(offset, 0); oldX = mx; break; case MotionEvent.ACTION_UP: mVelocityTracker.computeCurrentVelocity(1000); //计算速率 float velocityX = mVelocityTracker.getXVelocity(mPointerId); if (velocityX > SNAP_VELOCITY && cur > 0) { //滑动速率达到了一个标准(快速向右滑屏,返回上一个屏幕) 马上进行切屏处理 snapToScreen(cur - 1); }else if(velocityX < - SNAP_VELOCITY && cur < (getChildCount()-1)){////快速向左滑屏,返回下一个屏幕) snapToScreen(cur + 1); } //以上为快速移动的 ,强制切换屏幕 else{ //我们是缓慢移动的,因此先判断是保留在本屏幕还是到下一屏幕 snapToDestination(); } if (mVelocityTracker != null) { mVelocityTracker.recycle(); mVelocityTracker = null; } break; case MotionEvent.ACTION_POINTER_UP: // 获取离开屏幕的手指的索引 int pointerIndexLeave = event.getActionIndex(); int pointerIdLeave = event.getPointerId(pointerIndexLeave); //System.out.println("up "+pointerIdLeave); if (pointerIdLeave == mPointerId) { // 离开屏幕的正是目前的有效手指,此处需要重新调整,并且需要重置VelocityTracker int reIndex = pointerIndexLeave == 0 ? 1 : 0; mPointerId = reIndex;//event.getPointerId(reIndex); // 调整触摸位置,防止出现跳动 x = event.getX(reIndex); oldX = x; if (mVelocityTracker != null) mVelocityTracker.recycle(); mVelocityTracker = null; } break; } return true; } private void snapToDestination(){ int destScreen = (getScrollX() + MainActivity.screenWidth / 2 ) / MainActivity.screenWidth ; snapToScreen(destScreen); } private void snapToScreen(int wcur) { cur = wcur; //防止屏幕越界,即超过屏幕数 if(cur > getChildCount() - 1) cur = getChildCount() - 1 ; //为了达到下一屏幕或者当前屏幕,我们需要继续滑动的距离.根据dx值,可能想左滑动,也可能像又滑动 int dx = cur * MainActivity.screenWidth - getScrollX() ; mScroller.startScroll(getScrollX(), 0, dx, 0,Math.abs(dx) * 2); postInvalidate(); } @Override public void computeScroll() { if(mScroller.computeScrollOffset()){ scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); postInvalidate(); } } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int startX = 0; int childcount = getChildCount(); for(int i=0;i<childcount;i++){ View child = getChildAt(i); child.layout(startX, 0, startX+MainActivity.screenWidth, MainActivity.screenHeight); startX += MainActivity.screenWidth; } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int w = MeasureSpec.getSize(widthMeasureSpec); int h = MeasureSpec.getSize(heightMeasureSpec); setMeasuredDimension(w, h); int childcount = getChildCount(); for(int i=0;i<childcount;i++){ View child = getChildAt(i); child.measure(MainActivity.screenWidth, MainActivity.screenHeight); } } @Override protected void onDraw(Canvas canvas) { } }
这篇文章介绍了scroller的具体应用,大家可以用于参考(注释写得很详细了)。
转载请注明出处