首页 > 代码库 > Scroller的应用--滑屏实现

Scroller的应用--滑屏实现

1、Scroller源码分析

下面是对Scroller源码的分析,并附有源码,如下:
package android.widget;

import android.content.Context;
import android.hardware.SensorManager;
import android.os.Build;
import android.util.FloatMath;
import android.view.ViewConfiguration;
import android.view.animation.AnimationUtils;
import android.view.animation.Interpolator;


/**
 * 这个类封装了滚动
 * <p>This class encapsulates scrolling. You can use scrollers ({@link Scroller}
 * or {@link OverScroller}) to collect the data you need to produce a scrolling
 * animation—for example, in response to a fling gesture. Scrollers track
 * scroll offsets for you over time, but they don‘t automatically apply those
 * positions to your view. It‘s your responsibility to get and apply new
 * coordinates at a rate that will make the scrolling animation look smooth.</p>
 *
 * <p>Here is a simple example:</p>
 * 简单实例
 * <pre> private Scroller mScroller = new Scroller(context);
 * ...
 * public void zoomIn() {
 *     // Revert(重复) any animation currently in progress
 *     mScroller.forceFinished(true);
 *     // Start scrolling by providing a starting point and
 *     // the distance to travel
 *     mScroller.startScroll(0, 0, 100, 0);
 *     // Invalidate to request a redraw
 *     invalidate();
 * }</pre>
 *
 * <p>To track the changing positions of the x/y coordinates, use
 * {@link #computeScrollOffset}. The method returns a boolean to indicate
 * whether the scroller is finished. If it isn‘t, it means that a fling or
 * programmatic pan operation is still in progress. You can use this method to
 * find the current offsets of the x and y coordinates, for example:</p>
 *
 * <pre>if (mScroller.computeScrollOffset()) {
 *     // Get current x and y positions
 *     int currX = mScroller.getCurrX();
 *     int currY = mScroller.getCurrY();
 *    ...
 * }</pre>
 */
public class Scroller  {
    private int mMode;  //分为SCROLL_MODE和FLING_MODE

    private int mStartX;//起始坐标点,X轴方向
    private int mStartY;//起始坐标点,Y轴方向
    private int mFinalX;//滑动的最终位置,X轴方向
    private int mFinalY;//滑动的最终位置,Y轴方向

    private int mMinX;
    private int mMaxX;
    private int mMinY;
    private int mMaxY;

    private int mCurrX;//当前坐标点  X轴, 即调用startScroll函数后,经过一定时间所达到的值  
    private int mCurrY;//当前坐标点  Y轴, 即调用startScroll函数后,经过一定时间所达到的值  
    private long mStartTime;
    private int mDuration;
    private float mDurationReciprocal;
    private float mDeltaX;//应该继续滑动的距离, X轴方向
    private float mDeltaY;//应该继续滑动的距离, Y轴方向  
    private boolean mFinished;//是否已经完成本次滑动操作, 如果完成则为 true  
	// 被用来修饰动画效果,定义动画的变化率,可以使存在的动画效果accelerated(加速),decelerated(减速),repeated(重复),bounced(弹跳)等
    private Interpolator mInterpolator;
    private boolean mFlywheel;

    private float mVelocity;
    private float mCurrVelocity;
    private int mDistance;
	
	//ViewConfiguration包含了方法和标准的常量用来设置UI的超时、大小和距离 
    private float mFlingFriction = ViewConfiguration.getScrollFriction();

    private static final int DEFAULT_DURATION = 250;//默认的动画时间
    private static final int SCROLL_MODE = 0;
    private static final int FLING_MODE = 1;

    private static float DECELERATION_RATE = (float) (Math.log(0.78) / Math.log(0.9));//滑动减速
    private static final float INFLEXION = 0.35f; // Tension lines cross at (INFLEXION, 1)
    private static final float START_TENSION = 0.5f;
    private static final float END_TENSION = 1.0f;
    private static final float P1 = START_TENSION * INFLEXION;
    private static final float P2 = 1.0f - END_TENSION * (1.0f - INFLEXION);

    private static final int NB_SAMPLES = 100;
    private static final float[] SPLINE_POSITION = new float[NB_SAMPLES + 1];
    private static final float[] SPLINE_TIME = new float[NB_SAMPLES + 1];

    private float mDeceleration;
    private final float mPpi;

    // A context-specific coefficient adjusted to physical values.
    private float mPhysicalCoeff;

    static {
        float x_min = 0.0f;
        float y_min = 0.0f;
        for (int i = 0; i < NB_SAMPLES; i++) {
            final float alpha = (float) i / NB_SAMPLES;

            float x_max = 1.0f;
            float x, tx, coef;
            while (true) {
                x = x_min + (x_max - x_min) / 2.0f;
                coef = 3.0f * x * (1.0f - x);
                tx = coef * ((1.0f - x) * P1 + x * P2) + x * x * x;
                if (Math.abs(tx - alpha) < 1E-5) break;
                if (tx > alpha) x_max = x;
                else x_min = x;
            }
            SPLINE_POSITION[i] = coef * ((1.0f - x) * START_TENSION + x) + x * x * x;

            float y_max = 1.0f;
            float y, dy;
            while (true) {
                y = y_min + (y_max - y_min) / 2.0f;
                coef = 3.0f * y * (1.0f - y);
                dy = coef * ((1.0f - y) * START_TENSION + y) + y * y * y;
                if (Math.abs(dy - alpha) < 1E-5) break;
                if (dy > alpha) y_max = y;
                else y_min = y;
            }
            SPLINE_TIME[i] = coef * ((1.0f - y) * P1 + y * P2) + y * y * y;
        }
        SPLINE_POSITION[NB_SAMPLES] = SPLINE_TIME[NB_SAMPLES] = 1.0f;

        // This controls the viscous fluid effect (how much of it)
        sViscousFluidScale = 8.0f;
        // must be set to 1.0 (used in viscousFluid())
        sViscousFluidNormalize = 1.0f;
        sViscousFluidNormalize = 1.0f / viscousFluid(1.0f);

    }

    private static float sViscousFluidScale;
    private static float sViscousFluidNormalize;

    /**
     * Create a Scroller with the default duration and interpolator.
     */
    public Scroller(Context context) {
        this(context, null);
    }

    /**
     * Create a Scroller with the specified interpolator. If the interpolator is
     * null, the default (viscous) interpolator will be used. "Flywheel" behavior will
     * be in effect for apps targeting Honeycomb or newer.
     */
    public Scroller(Context context, Interpolator interpolator) {
        this(context, interpolator,
                context.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.HONEYCOMB);
    }

    /**
     * Create a Scroller with the specified interpolator. If the interpolator is
     * null, the default (viscous) interpolator will be used. Specify whether or
     * not to support progressive "flywheel" behavior in flinging.
     */
    public Scroller(Context context, Interpolator interpolator, boolean flywheel) {
        mFinished = true;
        mInterpolator = interpolator;
        mPpi = context.getResources().getDisplayMetrics().density * 160.0f;
        mDeceleration = computeDeceleration(ViewConfiguration.getScrollFriction());
        mFlywheel = flywheel;

        mPhysicalCoeff = computeDeceleration(0.84f); // look and feel tuning
    }

    /**
     * The amount of friction applied to flings. The default value
     * is {@link ViewConfiguration#getScrollFriction}.
     * 
     * @param friction A scalar dimension-less value representing the coefficient of
     *         friction.
	 *摩擦,根据摩擦力,计算出减速
     */
    public final void setFriction(float friction) {
        mDeceleration = computeDeceleration(friction);
        mFlingFriction = friction;
    }
    //减速
    private float computeDeceleration(float friction) {
        return SensorManager.GRAVITY_EARTH   // g (m/s^2)
                      * 39.37f               // inch/meter
                      * mPpi                 // pixels per inch
                      * friction;
    }

    /**
     * 
     * Returns whether the scroller has finished scrolling.
     * 
     * @return True if the scroller has finished scrolling, false otherwise.
     */
    public final boolean isFinished() {
        return mFinished;
    }
    
    /**
     * Force the finished field to a particular value.
     *  //强制结束本次滑屏操作  
     * @param finished The new finished value.
     */
    public final void forceFinished(boolean finished) {
        mFinished = finished;
    }
    
    /**
     * Returns how long the scroll event will take, in milliseconds.
     * 
     * @return The duration of the scroll in milliseconds.
     */
    public final int getDuration() {
        return mDuration;
    }
    
    /**
     * Returns the current X offset in the scroll. 
     * 
     * @return The new X offset as an absolute distance from the origin.
     */
    public final int getCurrX() {
        return mCurrX;
    }
    
    /**
     * Returns the current Y offset in the scroll. 
     * 
     * @return The new Y offset as an absolute distance from the origin.
     */
    public final int getCurrY() {
        return mCurrY;
    }
    
    /**
     * Returns the current velocity.
     * 获取当前的速度,根据手势滑动还是自己滚动,如果是自己滚动,使用初始速度-减速,可能是负值
     * @return The original velocity less the deceleration. Result may be
     * negative.
     */
    public float getCurrVelocity() {
        return mMode == FLING_MODE ?
                mCurrVelocity : mVelocity - mDeceleration * timePassed() / 2000.0f;
    }

    /**
     * Returns the start X offset in the scroll. 
     * 
     * @return The start X offset as an absolute distance from the origin.
     */
    public final int getStartX() {
        return mStartX;
    }
    
    /**
     * Returns the start Y offset in the scroll. 
     * 
     * @return The start Y offset as an absolute distance from the origin.
     */
    public final int getStartY() {
        return mStartY;
    }
    
    /**
     * Returns where the scroll will end. Valid only for "fling" scrolls.
     * 
     * @return The final X offset as an absolute distance from the origin.
     */
    public final int getFinalX() {
        return mFinalX;
    }
    
    /**
     * Returns where the scroll will end. Valid only for "fling" scrolls.
     * 
     * @return The final Y offset as an absolute distance from the origin.
     */
    public final int getFinalY() {
        return mFinalY;
    }

    /**
     * Call this when you want to know the new location.  If it returns true,
     * the animation is not yet finished.
	 * 返回值为boolean,true说明滚动尚未完成,false说明滚动已经完成。这是一个很重要的方法,
	 * 通常放在View.computeScroll()中,用来判断是否滚动是否结束
     */ 
    public boolean computeScrollOffset() {
        if (mFinished) {//已经完成了本次动画控制,直接返回为false
            return false;
        }
		//动画使用的时间
        int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
    
        if (timePassed < mDuration) {
            switch (mMode) {
            case SCROLL_MODE:
                float x = timePassed * mDurationReciprocal;
    
                if (mInterpolator == null)
                    x = viscousFluid(x); 
                else
                    x = mInterpolator.getInterpolation(x);
    
                mCurrX = mStartX + Math.round(x * mDeltaX);
                mCurrY = mStartY + Math.round(x * mDeltaY);
                break;
            case FLING_MODE:
                final float t = (float) timePassed / mDuration;
                final int index = (int) (NB_SAMPLES * t);
                float distanceCoef = 1.f;
                float velocityCoef = 0.f;
                if (index < NB_SAMPLES) {
                    final float t_inf = (float) index / NB_SAMPLES;
                    final float t_sup = (float) (index + 1) / NB_SAMPLES;
                    final float d_inf = SPLINE_POSITION[index];
                    final float d_sup = SPLINE_POSITION[index + 1];
                    velocityCoef = (d_sup - d_inf) / (t_sup - t_inf);
                    distanceCoef = d_inf + (t - t_inf) * velocityCoef;
                }

                mCurrVelocity = velocityCoef * mDistance / mDuration * 1000.0f;
                
                mCurrX = mStartX + Math.round(distanceCoef * (mFinalX - mStartX));
                // Pin to mMinX <= mCurrX <= mMaxX
                mCurrX = Math.min(mCurrX, mMaxX);
                mCurrX = Math.max(mCurrX, mMinX);
                
                mCurrY = mStartY + Math.round(distanceCoef * (mFinalY - mStartY));
                // Pin to mMinY <= mCurrY <= mMaxY
                mCurrY = Math.min(mCurrY, mMaxY);
                mCurrY = Math.max(mCurrY, mMinY);

                if (mCurrX == mFinalX && mCurrY == mFinalY) {
                    mFinished = true;
                }

                break;
            }
        }
        else {
            mCurrX = mFinalX;
            mCurrY = mFinalY;
            mFinished = true;
        }
        return true;
    }
    
    /** 根据当前已经消逝的时间计算当前的坐标点,保存在mCurrX和mCurrY值中
     * Start scrolling by providing a starting point and the distance to travel.
     * The scroll will use the default value of 250 milliseconds for the
     * duration.
     * startX水平偏移量的起始位置,正号是向左滚动,
     * @param startX Starting horizontal scroll offset in pixels. Positive
     *        numbers will scroll the content to the left.
	 *	startY竖直偏移量起始位置,正号是向上滚动
     * @param startY Starting vertical scroll offset in pixels. Positive numbers
     *        will scroll the content up.
	 * dx水平滑动距离,+向左
     * @param dx Horizontal distance to travel. Positive numbers will scroll the
     *        content to the left.
	 * dy竖直滑动距离,+向上
     * @param dy Vertical distance to travel. Positive numbers will scroll the
     *        content up.
	 *开始一个动画控制,由(startX , startY)在duration时间内前进(dx,dy)个单位,到达坐标为
     *                 (startX+dx , startY+dy)处。
     */
    public void startScroll(int startX, int startY, int dx, int dy) {
        startScroll(startX, startY, dx, dy, DEFAULT_DURATION);
    }

    /**
     * Start scrolling by providing a starting point, the distance to travel,
     * and the duration of the scroll.
     * 滚动,startX, startY为开始滚动的位置,dx,dy为滚动的偏移量, duration为完成滚动的时间
     * @param startX Starting horizontal scroll offset in pixels. Positive
     *        numbers will scroll the content to the left.
     * @param startY Starting vertical scroll offset in pixels. Positive numbers
     *        will scroll the content up.
     * @param dx Horizontal distance to travel. Positive numbers will scroll the
     *        content to the left.
     * @param dy Vertical distance to travel. Positive numbers will scroll the
     *        content up.
     * @param duration Duration of the scroll in milliseconds.
     */
    public void startScroll(int startX, int startY, int dx, int dy, int duration) {
        mMode = SCROLL_MODE;
        mFinished = false;
        mDuration = duration;
        mStartTime = AnimationUtils.currentAnimationTimeMillis();
        mStartX = startX;
        mStartY = startY;
        mFinalX = startX + dx;
        mFinalY = startY + dy;
        mDeltaX = dx;
        mDeltaY = dy;
        mDurationReciprocal = 1.0f / (float) mDuration;
    }

    /**
     * Start scrolling based on a fling gesture. The distance travelled will
     * depend on the initial velocity of the fling.
     * 
     * @param startX Starting point of the scroll (X)
     * @param startY Starting point of the scroll (Y)
     * @param velocityX Initial velocity of the fling (X) measured in pixels per
     *        second.
     * @param velocityY Initial velocity of the fling (Y) measured in pixels per
     *        second
     * @param minX Minimum X value. The scroller will not scroll past this
     *        point.
     * @param maxX Maximum X value. The scroller will not scroll past this
     *        point.
     * @param minY Minimum Y value. The scroller will not scroll past this
     *        point.
     * @param maxY Maximum Y value. The scroller will not scroll past this
     *        point.
     */
    public void fling(int startX, int startY, int velocityX, int velocityY,
            int minX, int maxX, int minY, int maxY) {
        // Continue a scroll or fling in progress
        if (mFlywheel && !mFinished) {
            float oldVel = getCurrVelocity();

            float dx = (float) (mFinalX - mStartX);
            float dy = (float) (mFinalY - mStartY);
            float hyp = FloatMath.sqrt(dx * dx + dy * dy);

            float ndx = dx / hyp;
            float ndy = dy / hyp;

            float oldVelocityX = ndx * oldVel;
            float oldVelocityY = ndy * oldVel;
            if (Math.signum(velocityX) == Math.signum(oldVelocityX) &&
                    Math.signum(velocityY) == Math.signum(oldVelocityY)) {
                velocityX += oldVelocityX;
                velocityY += oldVelocityY;
            }
        }

        mMode = FLING_MODE;
        mFinished = false;

        float velocity = FloatMath.sqrt(velocityX * velocityX + velocityY * velocityY);
     
        mVelocity = velocity;
        mDuration = getSplineFlingDuration(velocity);
        mStartTime = AnimationUtils.currentAnimationTimeMillis();
        mStartX = startX;
        mStartY = startY;

        float coeffX = velocity == 0 ? 1.0f : velocityX / velocity;
        float coeffY = velocity == 0 ? 1.0f : velocityY / velocity;

        double totalDistance = getSplineFlingDistance(velocity);
        mDistance = (int) (totalDistance * Math.signum(velocity));
        
        mMinX = minX;
        mMaxX = maxX;
        mMinY = minY;
        mMaxY = maxY;

        mFinalX = startX + (int) Math.round(totalDistance * coeffX);
        // Pin to mMinX <= mFinalX <= mMaxX
        mFinalX = Math.min(mFinalX, mMaxX);
        mFinalX = Math.max(mFinalX, mMinX);
        
        mFinalY = startY + (int) Math.round(totalDistance * coeffY);
        // Pin to mMinY <= mFinalY <= mMaxY
        mFinalY = Math.min(mFinalY, mMaxY);
        mFinalY = Math.max(mFinalY, mMinY);
    }
    
    private double getSplineDeceleration(float velocity) {
        return Math.log(INFLEXION * Math.abs(velocity) / (mFlingFriction * mPhysicalCoeff));
    }

    private int getSplineFlingDuration(float velocity) {
        final double l = getSplineDeceleration(velocity);
        final double decelMinusOne = DECELERATION_RATE - 1.0;
        return (int) (1000.0 * Math.exp(l / decelMinusOne));
    }

    private double getSplineFlingDistance(float velocity) {
        final double l = getSplineDeceleration(velocity);
        final double decelMinusOne = DECELERATION_RATE - 1.0;
        return mFlingFriction * mPhysicalCoeff * Math.exp(DECELERATION_RATE / decelMinusOne * l);
    }

    static float viscousFluid(float x)
    {
        x *= sViscousFluidScale;
        if (x < 1.0f) {
            x -= (1.0f - (float)Math.exp(-x));
        } else {
            float start = 0.36787944117f;   // 1/e == exp(-1)
            x = 1.0f - (float)Math.exp(1.0f - x);
            x = start + x * (1.0f - start);
        }
        x *= sViscousFluidNormalize;
        return x;
    }
    
    /**
     * Stops the animation. Contrary to {@link #forceFinished(boolean)},
     * aborting the animating cause the scroller to move to the final x and y
     * position
     * 终止动画,直接滑动到指定位置
     * @see #forceFinished(boolean)
     */
    public void abortAnimation() {
        mCurrX = mFinalX;
        mCurrY = mFinalY;
        mFinished = true;
    }
    
    /**
     * Extend the scroll animation. This allows a running animation to scroll
     * further and longer, when used with {@link #setFinalX(int)} or {@link #setFinalY(int)}.
     * 延长滚动时间
     * @param extend Additional time to scroll in milliseconds.
     * @see #setFinalX(int)
     * @see #setFinalY(int)
     */
    public void extendDuration(int extend) {
        int passed = timePassed();
        mDuration = passed + extend;
        mDurationReciprocal = 1.0f / mDuration;
        mFinished = false;
    }

    /**
     * Returns the time elapsed since the beginning of the scrolling.
     * 获得滚动经历的时间
     * @return The elapsed time in milliseconds.
     */
    public int timePassed() {
        return (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
    }

    /**
     * Sets the final position (X) for this scroller.
     *设置mScroller最终停留的水平位置,没有动画效果,直接跳到目标位置
     * @param newX The new X offset as an absolute distance from the origin.
     * @see #extendDuration(int)
     * @see #setFinalY(int)
     */
    public void setFinalX(int newX) {
        mFinalX = newX;
        mDeltaX = mFinalX - mStartX;
        mFinished = false;
    }

    /**
     * Sets the final position (Y) for this scroller.
     *设置mScroller最终停留的竖直位置,没有动画效果,直接跳到目标位置
     * @param newY The new Y offset as an absolute distance from the origin.
     * @see #extendDuration(int)
     * @see #setFinalX(int)
     */
    public void setFinalY(int newY) {
        mFinalY = newY;
        mDeltaY = mFinalY - mStartY;
        mFinished = false;
    }

    /**
     * @hide
     */
    public boolean isScrollingInDirection(float xvel, float yvel) {
        return !mFinished && Math.signum(xvel) == Math.signum(mFinalX - mStartX) &&
                Math.signum(yvel) == Math.signum(mFinalY - mStartY);
    }
}

2、Scroller调用关系

对于Scroller的调用,我先使用下面一张图片来阐述:


 Scroller的调用过程以及View的重绘:
  1 调用public void startScroll(int startX, int startY, int dx, int dy)
    该方法为scroll做一些准备工作.
    比如设置了移动的起始坐标,滑动的距离和方向以及持续时间等.
    该方法并不是真正的滑动scroll的开始,感觉叫prepareScroll()更贴切些.
    
  2 调用invalidate()或者postInvalidate()使View(ViewGroup)树重绘
    重绘会调用View的draw()方法
    draw()一共有六步: 
     Draw traversal performs several drawing steps which must be executed   
    in the appropriate order:   
    1. Draw the background   
    2. If necessary, save the canvas‘ layers to prepare for fading   
    3. Draw view‘s content   
    4. Draw children   
    5. If necessary, draw the fading edges and restore layers   
    6. Draw decorations (scrollbars for instance)
    其中最重要的是第三步和第四步
    第三步会去调用onDraw()绘制内容
    第四步会去调用dispatchDraw()绘制子View
    重绘分两种情况:
    2.1 ViewGroup的重绘
        在完成第三步onDraw()以后,进入第四步ViewGroup重写了
        父类View的dispatchDraw()绘制子View,于是这样继续调用:
        dispatchDraw()-->drawChild()-->child.computeScroll();
    2.2 View的重绘
        当View调用invalidate()方法时,会导致整个View树进行
        从上至下的一次重绘.比如从最外层的Layout到里层的Layout,直到每个子View.
        在重绘View树时ViewGroup和View时按理都会经过onMeasure()和onLayout()以及
        onDraw()方法.当然系统会判断这三个方法是否都必须执行,如果没有必要就不会调用.
        看到这里就明白了:当这个子View的父容器重绘时,也会调用上面提到的线路:
        onDraw()-->dispatchDraw()-->drawChild()-->child.computeScroll();
        于是子View(比如此处举例的ButtonSubClass类)中重写的computeScroll()方法
        就会被调用到.
        
  3 View树的重绘会调用到View中的computeScroll()方法
  
  4 在computeScroll()方法中
    在View的源码中可以看到public void computeScroll(){}是一个空方法.
    具体的实现需要自己来写.在该方法中我们可调用scrollTo()或scrollBy()
    来实现移动.该方法才是实现移动的核心.
    4.1 利用Scroller的mScroller.computeScrollOffset()判断移动过程是否完成
        注意:该方法是Scroller中的方法而不是View中的!!!!!!
        public boolean computeScrollOffset(){ }
        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.
        返回true时表示还移动还没有完成.
    4.2 若动画没有结束,则调用:scrollTo(By)();
        使其滑动scrolling
        
  5 再次调用invalidate().
    调用invalidate()方法那么又会重绘View树.
    从而跳转到第3步,如此循环,直到computeScrollOffset返回false
    
  通俗的理解:
  从上可见Scroller执行流程里面的三个核心方法
  mScroller.startScroll()
  mScroller.computeScrollOffset()
  view.computeScroll()
  1 在mScroller.startScroll()中为滑动做了一些初始化准备.
    比如:起始坐标,滑动的距离和方向以及持续时间(有默认值)等.
    其实除了这些,在该方法内还做了些其他事情:
    比较重要的一点是设置了动画开始时间.
  
  2 computeScrollOffset()方法主要是根据当前已经消逝的时间
    来计算当前的坐标点并且保存在mCurrX和mCurrY值中.
    因为在mScroller.startScroll()中设置了动画时间,那么
    在computeScrollOffset()方法中依据已经消逝的时间就很容易
    得到当前时刻应该所处的位置并将其保存在变量mCurrX和mCurrY中.
    除此之外该方法还可判断动画是否已经结束.
    
    所以在该示例中:
    @Override
    public void computeScroll() {
       super.computeScroll();
       if (mScroller.computeScrollOffset()) {
           scrollTo(mScroller.getCurrX(), 0);
           invalidate();
       }
    }
    先执行mScroller.computeScrollOffset()判断了滑动是否结束
    2.1 返回false,滑动已经结束.
    2.2 返回true,滑动还没有结束.
        并且在该方法内部也计算了最新的坐标值mCurrX和mCurrY.
        就是说在当前时刻应该滑动到哪里了.
        既然computeScrollOffset()如此贴心,盛情难却啊!
        于是我们就覆写View的computeScroll()方法,
        调用scrollTo(By)滑动到那里

3、Android中View绘制流程以及invalidate()等相关方法分析

关于这部分内容,大家可以查看Android中View绘制流程以及invalidate()等相关方法分析 这篇文章分析,里面详细介绍了onMeasure、onLayout、draw以及他们之间的关系,这里引用里面的一张图:


4、VelocityTracker、ViewConfiguration

VelocityTracker从字面意思理解那就是速度追踪器了,在滑动效果的开发中通常都是要使用该类计算出当前手势的初始速度,对应的方法是velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity))并通过getXVelocity或getYVelocity方法得到对应的速度值initialVelocity,并将获得的速度值传递给Scroller类的fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY) 方法进行控件滚动时各种位置坐标数值的计算,API中对fling 方法的解释是基于一个fling手势开始滑动动作,滑动的距离将由所获得的初始速度initialVelocity来决定。关于ViewConfiguration 的使用主要使用了该类的下面三个方法:

configuration.getScaledTouchSlop() //获得能够进行手势滑动的距离
configuration.getScaledMinimumFlingVelocity()//获得允许执行一个fling手势动作的最小速度值
configuration.getScaledMaximumFlingVelocity()//获得允许执行一个fling手势动作的最大速度值

需要重写的方法至少要包含下面几个方法:

onTouchEvent(MotionEvent event)//有手势操作必然少不了这个方法了

computeScroll()//必要时由父控件调用请求或通知其一个子节点需要更新它的mScrollX和mScrollY的值。典型的例子就是在一个子节点正在使用Scroller进行滑动动画时将会被执行。所以,从该方法的注释来看,继承这个方法的话一般都会有Scroller对象出现。

VelocityTracker的初始化以及资源释放的方法:
private void obtainVelocityTracker(MotionEvent event) {
        if (mVelocityTracker == null) {
                mVelocityTracker = VelocityTracker.obtain();
        }
        mVelocityTracker.addMovement(event);
}

private void releaseVelocityTracker() {
        if (mVelocityTracker != null) {
                mVelocityTracker.recycle();
                mVelocityTracker = null;
        }
}

5、实例开发

package com.jwzhangjie.scrollview;

import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Scroller;

/**
 * 
 * @author jwzhangjie
 */
public class MultiViewGroup extends ViewGroup {

	private VelocityTracker mVelocityTracker; // 用于判断甩动手势
	private static final int SNAP_VELOCITY = 600; // X轴速度基值,大于该值时进行切换
	private Scroller mScroller;// 滑动控制
	private int mCurScreen; // 当前页面为第几屏
	private int mDefaultScreen = 0;
	private float mLastMotionX;// 记住上次触摸屏的位置
	private int deltaX;

	private OnViewChangeListener mOnViewChangeListener;

	public MultiViewGroup(Context context) {
		this(context, null);
	}

	public MultiViewGroup(Context context, AttributeSet attrs) {
		super(context, attrs);
		init(getContext());
	}

	private void init(Context context) {
		mScroller = new Scroller(context);
		mCurScreen = mDefaultScreen;
	}

	@Override
	public void computeScroll() {
		if (mScroller.computeScrollOffset()) {// 会更新Scroller中的当前x,y位置
			scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
			postInvalidate();
		}
	}

	@Override
	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
		super.onMeasure(widthMeasureSpec, heightMeasureSpec);
		int width = MeasureSpec.getSize(widthMeasureSpec);
		int count = getChildCount();
		for (int i = 0; i < count; i++) {
			measureChild(getChildAt(i), widthMeasureSpec, heightMeasureSpec);
			getChildAt(i).measure(widthMeasureSpec, heightMeasureSpec);
		}
		scrollTo(mCurScreen * width, 0);// 移动到第一页位置
	}

	@Override
	protected void onLayout(boolean changed, int l, int t, int r, int b) {
		int margeLeft = 0;
		int size = getChildCount();
		for (int i = 0; i < size; i++) {
			View view = getChildAt(i);
			if (view.getVisibility() != View.GONE) {
				int childWidth = view.getMeasuredWidth();
				// 将内部子孩子横排排列
				view.layout(margeLeft, 0, margeLeft + childWidth,
						view.getMeasuredHeight());
				margeLeft += childWidth;
			}
		}
	}

	@Override
	public boolean onTouchEvent(MotionEvent event) {
		int action = event.getAction();
		float x = event.getX();
		switch (action) {
		case MotionEvent.ACTION_DOWN:
			obtainVelocityTracker(event);
			if (!mScroller.isFinished()) {
				mScroller.abortAnimation();
			}
			mLastMotionX = x;
			break;
		case MotionEvent.ACTION_MOVE:
			deltaX = (int) (mLastMotionX - x);
			if (canMoveDis(deltaX)) {
				obtainVelocityTracker(event);
				mLastMotionX = x;
				// 正向或者负向移动,屏幕跟随手指移动
				scrollBy(deltaX, 0);
			}
			break;
		case MotionEvent.ACTION_UP:
		case MotionEvent.ACTION_CANCEL:
			// 当手指离开屏幕时,记录下mVelocityTracker的记录,并取得X轴滑动速度
			obtainVelocityTracker(event);
			mVelocityTracker.computeCurrentVelocity(1000);
			float velocityX = mVelocityTracker.getXVelocity();
			// 当X轴滑动速度大于SNAP_VELOCITY
			// velocityX为正值说明手指向右滑动,为负值说明手指向左滑动
			if (velocityX > SNAP_VELOCITY && mCurScreen > 0) {
				// Fling enough to move left
				snapToScreen(mCurScreen - 1);
			} else if (velocityX < -SNAP_VELOCITY
					&& mCurScreen < getChildCount() - 1) {
				// Fling enough to move right
				snapToScreen(mCurScreen + 1);
			} else {
				snapToDestination();
			}
			releaseVelocityTracker();
			break;
		}
		// super.onTouchEvent(event);
		return true;// 这里一定要返回true,不然只接受down
	}

	/**
	 * 边界检测
	 * 
	 * @param deltaX
	 * @return
	 */
	private boolean canMoveDis(int deltaX) {
		int scrollX = getScrollX();
		// deltaX<0说明手指向右划
		if (deltaX < 0) {
			if (scrollX <= 0) {
				return false;
			} else if (deltaX + scrollX < 0) {
				scrollTo(0, 0);
				return false;
			}
		}
		// deltaX>0说明手指向左划
		int leftX = (getChildCount() - 1) * getWidth();
		if (deltaX > 0) {
			if (scrollX >= leftX) {
				return false;
			} else if (scrollX + deltaX > leftX) {
				scrollTo(leftX, 0);
				return false;
			}
		}
		return true;
	}

	/**
	 * 使屏幕移动到第whichScreen+1屏
	 * 
	 * @param whichScreen
	 */
	public void snapToScreen(int whichScreen) {
		int scrollX = getScrollX();
		if (scrollX != (whichScreen * getWidth())) {
			int delta = whichScreen * getWidth() - scrollX;
			mScroller.startScroll(scrollX, 0, delta, 0, Math.abs(delta) * 2);
			mCurScreen = whichScreen;
			invalidate();
			if (mOnViewChangeListener != null) {
				mOnViewChangeListener.OnViewChange(mCurScreen);
			}
		}
	}

	/**
	 * 当不需要滑动时,会调用该方法
	 */
	private void snapToDestination() {
		int screenWidth = getWidth();
		int whichScreen = (getScrollX() + (screenWidth / 2)) / screenWidth;
		snapToScreen(whichScreen);
	}

	private void obtainVelocityTracker(MotionEvent event) {
		if (mVelocityTracker == null) {
			mVelocityTracker = VelocityTracker.obtain();
		}
		mVelocityTracker.addMovement(event);
	}

	private void releaseVelocityTracker() {
		if (mVelocityTracker != null) {
			mVelocityTracker.recycle();
			mVelocityTracker = null;
		}
	}

	public void SetOnViewChangeListener(OnViewChangeListener listener) {
		mOnViewChangeListener = listener;
	}

	public interface OnViewChangeListener {
		public void OnViewChange(int page);
	}
}

package com.jwzhangjie.scrollview;

import com.jwzhangjie.scrollview.MultiViewGroup.OnViewChangeListener;

import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import android.view.View;
import android.widget.Toast;

public class MultiActivity extends FragmentActivity implements
		OnViewChangeListener {

	private MultiViewGroup multiViewGroup;
	private int allScreen;
	private int curreScreen = 0;

	@Override
	protected void onCreate(Bundle bundle) {
		super.onCreate(bundle);
		setContentView(R.layout.activity_main);
		multiViewGroup = (MultiViewGroup) findViewById(R.id.screenParent);
		multiViewGroup.SetOnViewChangeListener(this);
		allScreen = multiViewGroup.getChildCount() - 1;
	}

	public void nextScreen(View view) {
		if (curreScreen < allScreen) {
			curreScreen++;
		} else {
			curreScreen = 0;

		}
		multiViewGroup.snapToScreen(curreScreen);
	}

	public void preScreen(View view) {
		if (curreScreen > 0) {
			curreScreen--;
		} else {
			curreScreen = allScreen;
		}
		multiViewGroup.snapToScreen(curreScreen);
	}

	@Override
	public void OnViewChange(int page) {
		Toast.makeText(getApplicationContext(),
				getString(R.string.currePage, page), Toast.LENGTH_SHORT).show();
	}

}

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <Button
        android:id="@+id/nextPage"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="10dip"
        android:background="@drawable/bg_item_num_3_button"
        android:onClick="nextScreen"
        android:text="下一页" />

    <Button
        android:id="@+id/prePage"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentRight="true"
        android:layout_margin="10dip"
        android:background="@drawable/bg_item_num_3_button"
        android:onClick="preScreen"
        android:text="上一页" />

    <com.jwzhangjie.scrollview.MultiViewGroup
        android:id="@+id/screenParent"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_below="@id/nextPage" >

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="#f00" >

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="第一页" />
        </LinearLayout>

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="#0f0" >

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="第二页" />
        </LinearLayout>

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="#00f" >

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="第三页" />
        </LinearLayout>
    </com.jwzhangjie.scrollview.MultiViewGroup>

</RelativeLayout>

下载地址:https://github.com/jwzhangjie/MultiViewGroup

6、引用

1、Android中View绘制流程以及invalidate()等相关方法分析
2、Android 带你从源码的角度解析Scroller的滚动实现原理
3、 Android中滑屏实现----手把手教你如何实现触摸滑屏以及Scroller类详解

4、Android学习Scroller(五)——详解Scroller调用过程以及View的重绘

Scroller的应用--滑屏实现