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

接上一篇文章的内容,这篇文章主要是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的具体应用,大家可以用于参考(注释写得很详细了)。

转载请注明出处

时间: 2024-10-06 12:53:56

Scroller类源码解析及其应用(二)的相关文章

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

滑动是我们在自定义控件时候经常遇见的难听,让新手们倍感困惑,这篇文章主要介绍Scroller类的源码,告诉打击这个到底有什么用,怎么使用它来控制滑动.另外,我还会结合一个简单的例子,来看一下这个类的应用. 要说明Scroller类,我们往往要从另外两个方法说起,一个是ScrollTo(),一个是ScrollBy() 这两个方法我们可以在View的源码看到,我们知道其实每个空间都有滚动条,只是有的我们将它隐藏,所以我们看不见 下面是ScrollTo方法 /** * Set the scrolled

Java集合---Array类源码解析

Java集合---Array类源码解析              ---转自:牛奶.不加糖 一.Arrays.sort()数组排序 Java Arrays中提供了对所有类型的排序.其中主要分为Primitive(8种基本类型)和Object两大类. 基本类型:采用调优的快速排序: 对象类型:采用改进的归并排序. 1.对于基本类型源码分析如下(以int[]为例): Java对Primitive(int,float等原型数据)数组采用快速排序,对Object对象数组采用归并排序.对这一区别,sun在

【原】Android热更新开源项目Tinker源码解析系列之二:资源文件热更新

上一篇文章介绍了Dex文件的热更新流程,本文将会分析Tinker中对资源文件的热更新流程. 同Dex,资源文件的热更新同样包括三个部分:资源补丁生成,资源补丁合成及资源补丁加载. 本系列将从以下三个方面对Tinker进行源码解析: Android热更新开源项目Tinker源码解析系列之一:Dex热更新 Android热更新开源项目Tinker源码解析系列之二:资源热更新 Android热更新开源项目Tinker源码解析系类之三:so热更新 转载请标明本文来源:http://www.cnblogs

java.lang.Void类源码解析_java - JAVA

文章来源:嗨学网 敏而好学论坛www.piaodoo.com 欢迎大家相互学习 在一次源码查看ThreadGroup的时候,看到一段代码,为以下: /* * @throws NullPointerException if the parent argument is {@code null} * @throws SecurityException if the current thread cannot create a * thread in the specified thread grou

java-AbstractCollection类-源码解析

转载:原文地址 http://www.cnblogs.com/android-blogs/p/5566212.html 一.Collection接口 从<Java集合:整体结构>一文中我们知道所有的List和Set都继承自Collection接口,该接口类提供了集合最基本的方法,虽然List接口和Set等都有一些自己独有的方法,但是基本的操作类似.我们先看下Collection接口提供的方法: 总体上可以将Collection的方法分为以下几大类: 1.增加(add/addAll) 2.删除(

Object类源码解析

本文的分析基于JDK 1.8 Java中所有的类都继承自Object类. Object类的源码解析 1.void registerNatives() private static native void registerNatives(); static { registerNatives(); } 1 2 3 4 5 1 2 3 4 5 该方法只是对几个本地方法进行注册(即初始化java方法映射到C的方法).需要注意的是,很多类中都有这个方法,但是执行注册的目标是不同的.System类中也有该

JDK8源码解析 -- HashMap(二)

在上一篇JDK8源码解析 -- HashMap(一)的博客中关于HashMap的重要知识点已经讲了差不多了,还有一些内容我会在今天这篇博客中说说,同时我也会把一些我不懂的问题抛出来,希望看到我这篇博客的大神帮忙解答困扰我的问题,让我明白一个所以然来.彼此互相进步,互相成长.HashMap从jdk7到jdk8版本改变大,1.新增加的节点在链表末尾进行添加  2.使用了红黑树. 1. HashMap容量大小求值方法 // 返回2的幂次 static final int tableSizeFor(in

Android中的ViewRootImpl类源码解析

转载请注明出处 http://blog.csdn.net/qianhaifeng2012/article/details/51737370 ViewRoot目前这个类已经没有了,是老版本中的一个类,在Android2.2以后用ViewRootImpl代替ViewRoot,对应于ViewRootImpl.java,他是链接WindowManager和DecorView的纽带,另外View的绘制也是通过ViewRootImpl来完成的. 它的主要作用我的总结为如下: A:链接WindowManage

Java集合---Arrays类源码解析

一.Arrays.sort()数组排序 Java Arrays中提供了对所有类型的排序.其中主要分为Primitive(8种基本类型)和Object两大类. 基本类型:采用调优的快速排序: 对象类型:采用改进的归并排序. 1.对于基本类型源码分析如下(以int[]为例): Java对Primitive(int,float等原型数据)数组采用快速排序,对Object对象数组采用归并排序.对这一区别,sun在<<The Java Tutorial>>中做出的解释如下: The sort