接上一篇文章的内容,这篇文章主要是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里面获得),然后每次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的具体应用,大家可以用于参考(注释写得很详细了)。
转载请注明出处http://blog.youkuaiyun.com/crazy__chen/article/details/45917273