Android的Scroller和VelocityTracker
原文:https://blog.csdn.net/uyy203/article/details/78926262
一. Android的Scroller介绍
public class Scroller extends Object
java.lang.Object
android.widget.Scroller
二. 概述
这个类封装了滚动操作。滚动的持续时间可以通过构造函数传递,并且可以指定滚动动作的持续的最长时间。经过这段时间,滚动会自动定位到最终位置,并且通过computeScrollOffset()会得到的返回值为false,表明滚动动作已经结束。
三、构造函数
public Scroller (Context context)
使用缺省的持续持续时间和动画插入器创建一个Scroller。(译者注:interpolator这里翻译为动画插入器,见这里。)
public Scroller (Context context, Interpolator interpolator)
根据指定的动画插入器创建一个Scroller,如果指定的动画插入器为空,则会使用缺省的动画插入器(粘滞viscous)创建。
四、公共方法
public void abortAnimation ()
停止动画。与forceFinished(boolean)
相反,Scroller滚动到最终x与y位置时中止动画。
参见
forceFinished(boolean)
public boolean computeScrollOffset ()
当想要知道新的位置时,调用此函数。如果返回true,表示动画还没有结束。位置改变以提供一个新的位置。
public void extendDuration (int extend)
延长滚动动画时间。此函数允许当使用setFinalX(int) or setFinalY(int) 时,卷动动作持续更长时间并且卷动更长距离。
参数
extend 卷动事件延长的时间,以毫秒为单位
参见
setFinalX(int)
setFinalY(int)
public void fling (int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY)
在fling(译者注:快滑,用户按下触摸屏、快速移动后松开)手势基础上开始滚动。滚动的距离取决于fling的初速度。
参数
startX 滚动起始点X坐标
startY 滚动起始点Y坐标
velocityX 当滑动屏幕时X方向初速度,以每秒像素数计算
velocityY 当滑动屏幕时Y方向初速度,以每秒像素数计算
minX X方向的最小值,scroller不会滚过此点。
maxX X方向的最大值,scroller不会滚过此点。
minY Y方向的最小值,scroller不会滚过此点。
maxY Y方向的最大值,scroller不会滚过此点。
public final void forceFinished (boolean finished)
强制终止的字段到特定值。(译者注:立即停止滚动?)
参数
finished 新的结束值
public final int getCurrX ()
返回当前滚动X方向的偏移
返回值
距离原点X方向的绝对值
public final int getCurrY ()
返回当前滚动Y方向的偏移
返回值
距离原点Y方向的绝对值
public final int getDuration ()
返回滚动事件的持续时间,以毫秒计算。
返回值
滚动持续的毫秒数
public final int getFinalX ()
返回滚动结束位置。仅针对“fling”手势有效
返回值
最终位置X方向距离原点的绝对距离
public final int getFinalY ()
返回滚动结束位置。仅针对“fling”操作有效
返回值
最终位置Y方向距离原点的绝对距离
public final int getStartX ()
返回滚动起始点的X方向的偏移
返回值
起始点在X方向距离原点的绝对距离
public final int getStartY ()
返回滚动起始点的Y方向的偏移
返回值
起始点在Y方向距离原点的绝对距离
public final boolean isFinished ()
返回scroller是否已完成滚动。
返回值
停止滚动返回true,否则返回false
public void setFinalX (int newX)
设置scroller的X方向终止位置
参数
newX 新位置在X方向距离原点的绝对偏移。
参见
extendDuration(int)
setFinalY(int)
public void setFinalY (int newY)
设置scroller的Y方向终止位置
参数
newY 新位置在Y方向距离原点的绝对偏移。
参见
extendDuration(int)
setFinalY(int)
public void startScroll (int startX, int startY, int dx, int dy)
以提供的起始点和将要滑动的距离开始滚动。滚动会使用缺省值250ms作为持续时间。
参数
startX 水平方向滚动的偏移值,以像素为单位。负值表明滚动将向左滚动
startY 垂直方向滚动的偏移值,以像素为单位。负值表明滚动将向上滚动
dx 水平方向滑动的距离,负值会使滚动向左滚动
dy 垂直方向滑动的距离,负值会使滚动向上滚动
public void startScroll (int startX, int startY, int dx, int dy, int duration)
以提供的起始点和将要滑动的距离开始滚动。
参数
startX 水平方向滚动的偏移值,以像素为单位。负值表明滚动将向左滚动
startY 垂直方向滚动的偏移值,以像素为单位。负值表明滚动将向上滚动
dx 水平方向滑动的距离,负值会使滚动向左滚动
dy 垂直方向滑动的距离,负值会使滚动向上滚动
duration 滚动持续时间,以毫秒计。
public int timePassed ()
返回自滚动开始经过的时间
返回值
经过时间以毫秒为单位
五、补充
文章精选
Scroller 粗浅理解
ScrollTextView - scrolling TextView for Android
Android里Scroller类是为了实现View平滑滚动的一个Helper类。通常在自定义的View时使用,在View中定义一个私有成员mScroller = new Scroller(context)。设置mScroller滚动的位置时,并不会导致View的滚动,通常是用mScroller记录/计算View滚动的位置,再重写View的computeScroll(),完成实际的滚动。
相关API介绍如下
mScroller.getCurrX() //获取mScroller当前水平滚动的位置
mScroller.getCurrY() //获取mScroller当前竖直滚动的位置
mScroller.getFinalX() //获取mScroller最终停止的水平位置
mScroller.getFinalY() //获取mScroller最终停止的竖直位置
mScroller.setFinalX(int newX) //设置mScroller最终停留的水平位置,没有动画效果,直接跳到目标位置
mScroller.setFinalY(int newY) //设置mScroller最终停留的竖直位置,没有动画效果,直接跳到目标位置
//滚动,startX, startY为开始滚动的位置,dx,dy为滚动的偏移量, duration为完成滚动的时间
mScroller.startScroll(int startX, int startY, int dx, int dy) //使用默认完成时间250ms
mScroller.startScroll(int startX, int startY, int dx, int dy, int duration)
mScroller.computeScrollOffset() //返回值为boolean,true说明滚动尚未完成,false说明滚动已经完成。这是一个很重要的方法,通常放在View.computeScroll()中,用来判断是否滚动是否结束。
举例说明,自定义一个CustomView,使用Scroller实现滚动:
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.Scroller;
public class CustomView extends LinearLayout {
private static final String TAG = "Scroller";
private Scroller mScroller;
public CustomView(Context context, AttributeSet attrs) {
super(context, attrs);
mScroller = new Scroller(context);
}
//调用此方法滚动到目标位置
public void smoothScrollTo(int fx, int fy) {
int dx = fx - mScroller.getFinalX();
int dy = fy - mScroller.getFinalY();
smoothScrollBy(dx, dy);
}
//调用此方法设置滚动的相对偏移
public void smoothScrollBy(int dx, int dy) {
//设置mScroller的滚动偏移量
mScroller.startScroll(mScroller.getFinalX(), mScroller.getFinalY(), dx, dy);
invalidate();//这里必须调用invalidate()才能保证computeScroll()会被调用,否则不一定会刷新界面,看不到滚动效果
}
@Override
public void computeScroll() {
//先判断mScroller滚动是否完成
if (mScroller.computeScrollOffset()) {
//这里调用View的scrollTo()完成实际的滚动
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
//必须调用该方法,否则不一定能看到滚动效果
postInvalidate();
}
super.computeScroll();
}
}
关于scrollTo()
和scrollBy()
以及偏移坐标的设置/取值问题
在前面一篇博文中《Android中滑屏初探 —- scrollTo 以及 scrollBy方法使用说明》,我们掌握了scrollTo()和
scrollBy()方法的作用,这两个方法的主要作用是将View/ViewGroup移至指定的坐标中,并且将偏移量保存起来。另外:
mScrollX 代表X轴方向的偏移坐标
mScrollY 代表Y轴方向的偏移坐标
关于偏移量的设置我们可以参看下源码:
package com.qin.customviewgroup;
public class View {
....
protected int mScrollX; //该视图内容相当于视图起始坐标的偏移量 , X轴 方向
protected int mScrollY; //该视图内容相当于视图起始坐标的偏移量 , Y轴方向
//返回值
public final int getScrollX() {
return mScrollX;
}
public final int getScrollY() {
return mScrollY;
}
public void scrollTo(int x, int y) {
//偏移位置发生了改变
if (mScrollX != x || mScrollY != y) {
int oldX = mScrollX;
int oldY = mScrollY;
mScrollX = x; //赋新值,保存当前便宜量
mScrollY = y;
//回调onScrollChanged方法
onScrollChanged(mScrollX, mScrollY, oldX, oldY);
if (!awakenScrollBars()) {
invalidate(); //一般都引起重绘
}
}
}
// 看出原因了吧 。。 mScrollX 与 mScrollY 代表我们当前偏移的位置 , 在当前位置继续偏移(x ,y)个单位
public void scrollBy(int x, int y) {
scrollTo(mScrollX + x, mScrollY + y);
}
//...
}
Scroller类的介绍
在初次看Launcher滑屏的时候,我就对Scroller类的学习感到非常蛋疼,完全失去了继续研究的欲望。如今,没得办法,得重新看Launcher模块,基本上将Launcher大部分类以及功能给掌握了。当然,也花了一天时间来学习Launcher里的滑屏实现,基本上业是拨开云雾见真知了。
我们知道想把一个View偏移至指定坐标(x,y)处,利用scrollTo()方法直接调用就OK了,但我们不能忽视的是,该方法本身
来的的副作用:非常迅速的将View/ViewGroup偏移至目标点,而没有对这个偏移过程有任何控制,对用户而言可能是不太
友好的。于是,基于这种偏移控制,Scroller类被设计出来了,该类的主要作用是为偏移过程制定一定的控制流程(后面我们会知道的更多),从而使偏移更流畅,更完美。
可能上面说的比较悬乎,道理也没有讲透。下面我就根据特定情景帮助大家分析下:
情景: 从上海如何到武汉?
普通的人可能会想,so easy : 飞机、轮船、11路公交车...
文艺的人可能会想, 小 case : 时空忍术(火影的招数)、翻个筋斗(孙大圣的招数)...
不管怎么样,我们想出来的套路可能有两种:
1、有个时间控制过程才能抵达(缓慢的前进) ----- 对应于Scroller的作用
假设做火车,这个过程可能包括: 火车速率,花费周期等;
2、瞬间抵达(超神太快了,都眩晕了,用户体验不太好) ------ 对应于scrollTo()的作用
模拟Scroller类的实现功能:
假设从上海做动车到武汉需要10个小时,行进距离为1000km ,火车速率200/h 。采用第一种时间控制方法到达武汉的
整个配合过程可能如下:
我们每隔一段时间(例如1小时),计算火车应该行进的距离,然后调用scrollTo()方法,行进至该处。10小时过完后,
我们也就达到了目的地了。
相信大家心里应该有个感觉了。我们就分析下源码里去看看Scroller类的相关方法.
其源代码(部分)如下: 路径位于 \frameworks\base\core\java\android\widget\Scroller.java
public class Scroller {
private int mStartX; //起始坐标点 , X轴方向
private int mStartY; //起始坐标点 , Y轴方向
private int mCurrX; //当前坐标点 X轴, 即调用startScroll函数后,经过一定时间所达到的值
private int mCurrY; //当前坐标点 Y轴, 即调用startScroll函数后,经过一定时间所达到的值
private float mDeltaX; //应该继续滑动的距离, X轴方向
private float mDeltaY; //应该继续滑动的距离, Y轴方向
private boolean mFinished; //是否已经完成本次滑动操作, 如果完成则为 true
//构造函数
public Scroller(Context context) {
this(context, null);
}
public final boolean isFinished() {
return mFinished;
}
//强制结束本次滑屏操作
public final void forceFinished(boolean finished) {
mFinished = finished;
}
public final int getCurrX() {
return mCurrX;
}
/* Call this when you want to know the new location. If it returns true,
* the animation is not yet finished. loc will be altered to provide the
* new location. */
//根据当前已经消逝的时间计算当前的坐标点,保存在mCurrX和mCurrY值中
public boolean computeScrollOffset() {
if (mFinished) { //已经完成了本次动画控制,直接返回为false
return false;
}
int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
if (timePassed < mDuration) {
switch (mMode) {
case SCROLL_MODE:
float x = (float)timePassed * mDurationReciprocal;
...
mCurrX = mStartX + Math.round(x * mDeltaX);
mCurrY = mStartY + Math.round(x * mDeltaY);
break;
...
}
else {
mCurrX = mFinalX;
mCurrY = mFinalY;
mFinished = true;
}
return true;
}
//开始一个动画控制,由(startX , startY)在duration时间内前进(dx,dy)个单位,即到达坐标为(startX+dx , startY+dy)出
public void startScroll(int startX, int startY, int dx, int dy, int duration) {
mFinished = false;
mDuration = duration;
mStartTime = AnimationUtils.currentAnimationTimeMillis();
mStartX = startX; mStartY = startY;
mFinalX = startX + dx; mFinalY = startY + dy;
mDeltaX = dx; mDeltaY = dy;
...
}
}
其中比较重要的两个方法为:
public void startScroll(int startX, int startY, int dx, int dy, int duration)
函数功能说明:根据当前已经消逝的时间计算当前的坐标点,保存在mCurrX和mCurrY值中
public void startScroll(int startX, int startY, int dx, int dy, int duration)
函数功能说明:开始一个动画控制,由(startX , startY)在duration时间内前进(dx,dy)个单位,到达坐标为(startX+dx , startY+dy)
处。
PS : 强烈建议大家看看该类的源码,便于后续理解。
computeScroll()方法介绍
为了易于控制滑屏控制,Android框架提供了 computeScroll()方法去控制这个流程。在绘制View时,会在draw()过程调用该
方法。因此, 再配合使用Scroller实例,我们就可以获得当前应该的偏移坐标,手动使View/ViewGroup偏移至该处。
computeScroll()方法原型如下,该方法位于ViewGroup.java类中
/**
* Called by a parent to request that a child update its values for mScrollX
* and mScrollY if necessary. This will typically be done if the child is
* animating a scroll using a {@link android.widget.Scroller Scroller}
* object.
*/由父视图调用用来请求子视图根据偏移值 mScrollX,mScrollY重新绘制
public void computeScroll() { //空方法 ,自定义ViewGroup必须实现方法体
}
为了实现偏移控制,一般自定义View/ViewGroup都需要重载该方法 。
其调用过程位于View绘制流程draw()过程中,如下:
@Override
protected void dispatchDraw(Canvas canvas){
...
for (int i = 0; i < count; i++) {
final View child = children[getChildDrawingOrder(count, i)];
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
more |= drawChild(canvas, child, drawingTime);
}
}
}
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
...
child.computeScroll();
...
}
相关知识点介绍:
VelocityTracker类
功能: 根据触摸位置计算每像素的移动速率。
常用方法:
public void addMovement (MotionEvent ev)
功能:添加触摸对象MotionEvent , 用于计算触摸速率。
public void computeCurrentVelocity (int units)
功能:以每像素units单位考核移动速率。额,其实我也不太懂,赋予值1000即可。
参照源码 该units的意思如下:
参数 units : The units you would like the velocity in. A value of 1 provides pixels per millisecond, 1000 provides pixels per second, etc.
public float getXVelocity ()
功能:获得X轴方向的移动速率。
ViewConfiguration类
功能: 获得一些关于timeouts(时间)、sizes(大小)、distances(距离)的标准常量值 。
常用方法:
public int getScaledEdgeSlop()
说明:获得一个触摸移动的最小像素值。也就是说,只有超过了这个值,才代表我们该滑屏处理了。
public static int getLongPressTimeout()
说明:获得一个执行长按事件监听(onLongClickListener)的值。也就是说,对某个View按下触摸时,只有超过了
这个时间值在,才表示我们该对该View回调长按事件了;否则,小于这个时间点松开手指,只执行onClick监听
我能写下来的也就这么多了,更多的东西参考代码注释吧。 在掌握了上面我罗列的知识后(重点scrollTo、Scroller类),
其他方面的知识都是关于点与点之间的计算了以及触摸事件的分发了。这方面感觉也没啥可写的。
public class MultiViewGroup extends ViewGroup {
private static String TAG = "MultiViewGroup";
private int curScreen = 0 ; //当前屏幕
private Scroller mScroller = null ; //Scroller对象实例
public MultiViewGroup(Context context) {
super(context);
mContext = context;
init();
}
public MultiViewGroup(Context context, AttributeSet attrs) {
super(context, attrs);
mContext = context;
init();
}
//初始化
private void init() {
...
//初始化Scroller实例
mScroller = new Scroller(mContext);
// 初始化3个 LinearLayout控件
...
//初始化一个最小滑动距离
mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
}
// 由父视图调用用来请求子视图根据偏移值 mScrollX,mScrollY重新绘制
@Override
public void computeScroll() {
// TODO Auto-generated method stub
Log.e(TAG, "computeScroll");
// 如果返回true,表示动画还没有结束
// 因为前面startScroll,所以只有在startScroll完成时 才会为false
if (mScroller.computeScrollOffset()) {
Log.e(TAG, mScroller.getCurrX() + "======" + mScroller.getCurrY());
// 产生了动画效果,根据当前值 每次滚动一点
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
Log.e(TAG, "### getleft is " + getLeft() + " ### getRight is " + getRight());
//此时同样也需要刷新View ,否则效果可能有误差
postInvalidate();
}
else
Log.i(TAG, "have done the scoller -----");
}
//两种状态: 是否处于滑屏状态
private static final int TOUCH_STATE_REST = 0; //什么都没做的状态
private static final int TOUCH_STATE_SCROLLING = 1; //开始滑屏的状态
private int mTouchState = TOUCH_STATE_REST; //默认是什么都没做的状态
//--------------------------
//处理触摸事件 ~
public static int SNAP_VELOCITY = 600 ; //最小的滑动速率
private int mTouchSlop = 0 ; //最小滑动距离,超过了,才认为开始滑动
private float mLastionMotionX = 0 ; //记住上次触摸屏的位置
//处理触摸的速率
private VelocityTracker mVelocityTracker = null ;
// 这个感觉没什么作用 不管true还是false 都是会执行onTouchEvent的 因为子view里面onTouchEvent返回false了
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
// TODO Auto-generated method stub
Log.e(TAG, "onInterceptTouchEvent-slop:" + mTouchSlop);
final int action = ev.getAction();
//表示已经开始滑动了,不需要走该Action_MOVE方法了(第一次时可能调用)。
//该方法主要用于用户快速松开手指,又快速按下的行为。此时认为是出于滑屏状态的。
if ((action == MotionEvent.ACTION_MOVE) && (mTouchState != TOUCH_STATE_REST)) {
return true;
}
final float x = ev.getX();
final float y = ev.getY();
switch (action) {
case MotionEvent.ACTION_MOVE:
Log.e(TAG, "onInterceptTouchEvent move");
final int xDiff = (int) Math.abs(mLastionMotionX - x);
//超过了最小滑动距离,就可以认为开始滑动了
if (xDiff > mTouchSlop) {
mTouchState = TOUCH_STATE_SCROLLING;
}
break;
case MotionEvent.ACTION_DOWN:
Log.e(TAG, "onInterceptTouchEvent down");
mLastionMotionX = x;
mLastMotionY = y;
Log.e(TAG, mScroller.isFinished() + "");
mTouchState = mScroller.isFinished() ? TOUCH_STATE_REST : TOUCH_STATE_SCROLLING;
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
Log.e(TAG, "onInterceptTouchEvent up or cancel");
mTouchState = TOUCH_STATE_REST;
break;
}
Log.e(TAG, mTouchState + "====" + TOUCH_STATE_REST);
return mTouchState != TOUCH_STATE_REST;
}
public boolean onTouchEvent(MotionEvent event){
super.onTouchEvent(event);
Log.i(TAG, "--- onTouchEvent--> " );
// TODO Auto-generated method stub
Log.e(TAG, "onTouchEvent start");
//获得VelocityTracker对象,并且添加滑动对象
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(event);
//触摸点
float x = event.getX();
float y = event.getY();
switch(event.getAction()){
case MotionEvent.ACTION_DOWN:
//如果屏幕的动画还没结束,你就按下了,我们就结束上一次动画,即开始这次新ACTION_DOWN的动画
if(mScroller != null){
if(!mScroller.isFinished()){
mScroller.abortAnimation();
}
}
mLastionMotionX = x ; //记住开始落下的屏幕点
break ;
case MotionEvent.ACTION_MOVE:
int detaX = (int)(mLastionMotionX - x ); //每次滑动屏幕,屏幕应该移动的距离
scrollBy(detaX, 0);//开始缓慢滑屏咯。 detaX > 0 向右滑动 , detaX < 0 向左滑动 ,
Log.e(TAG, "--- MotionEvent.ACTION_MOVE--> detaX is " + detaX );
mLastionMotionX = x ;
break ;
case MotionEvent.ACTION_UP:
final VelocityTracker velocityTracker = mVelocityTracker ;
velocityTracker.computeCurrentVelocity(1000);
//计算速率
int velocityX = (int) velocityTracker.getXVelocity() ;
Log.e(TAG , "---velocityX---" + velocityX);
//滑动速率达到了一个标准(快速向右滑屏,返回上一个屏幕) 马上进行切屏处理
if (velocityX > SNAP_VELOCITY && curScreen > 0) {
// Fling enough to move left
Log.e(TAG, "snap left");
snapToScreen(curScreen - 1);
}
//快速向左滑屏,返回下一个屏幕)
else if(velocityX < -SNAP_VELOCITY && curScreen < (getChildCount()-1)){
Log.e(TAG, "snap right");
snapToScreen(curScreen + 1);
}
//以上为快速移动的 ,强制切换屏幕
else{
//我们是缓慢移动的,因此先判断是保留在本屏幕还是到下一屏幕
snapToDestination();
}
//回收VelocityTracker对象
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
//修正mTouchState值
mTouchState = TOUCH_STATE_REST ;
break;
case MotionEvent.ACTION_CANCEL:
mTouchState = TOUCH_STATE_REST ;
break;
}
return true ;
}
////我们是缓慢移动的,因此需要根据偏移值判断目标屏是哪个?
private void snapToDestination(){
//当前的偏移位置
int scrollX = getScrollX() ;
int scrollY = getScrollY() ;
Log.e(TAG, "### onTouchEvent snapToDestination ### scrollX is " + scrollX);
//判断是否超过下一屏的中间位置,如果达到就抵达下一屏,否则保持在原屏幕
//直接使用这个公式判断是哪一个屏幕 前后或者自己
//判断是否超过下一屏的中间位置,如果达到就抵达下一屏,否则保持在原屏幕
// 这样的一个简单公式意思是:假设当前滑屏偏移值即 scrollCurX 加上每个屏幕一半的宽度,除以每个屏幕的宽度就是
// 我们目标屏所在位置了。 假如每个屏幕宽度为320dip, 我们滑到了500dip处,很显然我们应该到达第二屏
int destScreen = (getScrollX() + MultiScreenActivity.screenWidth / 2 ) / MultiScreenActivity.screenWidth ;
Log.e(TAG, "### onTouchEvent ACTION_UP### dx destScreen " + destScreen);
snapToScreen(destScreen);
}
//真正的实现跳转屏幕的方法
private void snapToScreen(int whichScreen){
//简单的移到目标屏幕,可能是当前屏或者下一屏幕
//直接跳转过去,不太友好
//scrollTo(mLastScreen * MultiScreenActivity.screenWidth, 0);
//为了友好性,我们在增加一个动画效果
//需要再次滑动的距离 屏或者下一屏幕的继续滑动距离
curScreen = whichScreen ;
//防止屏幕越界,即超过屏幕数
if(curScreen > getChildCount() - 1)
curScreen = getChildCount() - 1 ;
//为了达到下一屏幕或者当前屏幕,我们需要继续滑动的距离.根据dx值,可能想左滑动,也可能像又滑动
int dx = curScreen * getWidth() - getScrollX() ;
Log.e(TAG, "### onTouchEvent ACTION_UP### dx is " + dx);
mScroller.startScroll(getScrollX(), 0, dx, 0,Math.abs(dx) * 2);
//由于触摸事件不会重新绘制View,所以此时需要手动刷新View 否则没效果
invalidate();
}
//开始滑动至下一屏
public void startMove(){
...
}
//理解停止滑动
public void stopMove(){
...
}
// measure过程
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
...
}
// layout过程
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
...
}
}