Scroller类源码解析及其应用(二)

接上一篇文章的内容,这篇文章主要是Scroller类的应用,在讲具体实例之前,我还有顺便提一个Scroller的问题。

就是fling()方法和startScroll()方法的区别,其实确保已经在上篇文章说得很清楚(注释里面)。

fling没有设置起点坐标和终点坐标,而是根据滑动的起始速度来计算最后会到达的坐标位置。


在了解scroller的使用之前,我们来看一下调用示意图

Scroller类源码解析及其应用(二)_第1张图片

据我们的了解,我们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的时候,判断是不是我们维护的手指离开了,如果是,就需要换一只来维护(如果有的话)


下面是一个用于屏幕切换的例子代码

看一下截图(由于是滑动的,没办法截到动态)

Scroller类源码解析及其应用(二)_第2张图片

我自定义了一个控件,下面是初始化代码

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的具体应用,大家可以用于参考(注释写得很详细了)。

转载请注明出处http://blog.csdn.net/crazy__chen/article/details/45917273

你可能感兴趣的:(Scroller类源码解析及其应用(二))