编程语言
首页 > 编程语言> > Scroller类的源码分析以及使用

Scroller类的源码分析以及使用

作者:互联网

Scroller类是用于处理滚动效果的一个类,我们平时使用的ViewPager,可以触摸左右滑动页面,其内部就是使用了Scroller。由于Scroller类是配合View或者ViewGroup的子类来使用的,所以,在了解Scroller类之前,我们先了解一下View的scrollTo方法和scrollBy方法,下面看看这两个方法的源码:

    /**
     * Set the scrolled position of your view. This will cause a call to
     * {@link #onScrollChanged(int, int, int, int)} and the view will be
     * invalidated.
     * @param x the x position to scroll to
     * @param y the y position to scroll to
     */
    public void scrollTo(int x, int y) {
        if (mScrollX != x || mScrollY != y) {
            int oldX = mScrollX;
            int oldY = mScrollY;
            mScrollX = x;
            mScrollY = y;
            invalidateParentCaches();
            onScrollChanged(mScrollX, mScrollY, oldX, oldY);
            if (!awakenScrollBars()) {
                postInvalidateOnAnimation();
            }
        }
    }

    /**
     * Move the scrolled position of your view. This will cause a call to
     * {@link #onScrollChanged(int, int, int, int)} and the view will be
     * invalidated.
     * @param x the amount of pixels to scroll by horizontally
     * @param y the amount of pixels to scroll by vertically
     */
    public void scrollBy(int x, int y) {
        scrollTo(mScrollX + x, mScrollY + y);
    }

从源码中可以看出,scrollBy方法其实最终也是调用的scrollTo方法,对于View,如果调用这两个方法,其实就是移动View中的内容,注意,是移动view中的内容,不是移动View自身的位置。如果是ViewGroup调用这两个方法,则是移动ViewGroup中的子View的位置。scrollTo(int x, int y)方法是,将内容移动到(x,y)这个坐标点处,如果view的内容已经移动到了(x,y)这个坐标点处后,即使再次调用这个方法,View中的内容也不会再次移动了。scrollBy(int x,int y)方法将View中的内容相对于当前位置,在X轴方向移动x段距离,在y轴方向移动y段距离。有时候我们在使用scrollTo(x,y)和scrollBy(dx,dy)方法时,传入的数据是正负值时,移动的方向是相反的,其实这个可以这样理解,假设,我们想让View中的内容向右移动,则可以将手机屏幕作为一个参考的坐标系,以手机屏幕作为基准,由于屏幕是不能移动的,如果想让View中的内容向右移动100px,则只能是让View的画布向右移动,画布的初始位置就是每个View内部的mScrollX和mScrollY的默认值,即(0,0),也就是屏幕的左上角的位置,如果画布向右移动100px的距离后,屏幕左上角在X轴的坐标此时就要变成-100,这时,我们就只需要向scrollTo或者scrollBy方法中传入-100即可。总结:当想使用scrollTo或者scrollBy方法对View的内容进行移动时,想要移动多少,数字是正还是负,这时可以想一下,屏幕的左边界在X轴上的坐标点,这个坐标点是负数,则传入负数,这个坐标点是正数就传入正数。可以结合下面这个图去理解或者参考这篇文章Android getScrollX()详解
在这里插入图片描述
图中的淡金色的框代表屏幕,第一行的屏幕的左边框的位置就是初始位置,当我们想将View的内容向右移动100px,由于屏幕是不能移动的,则只能移动画布,此时,画布的就向右移动100px,此时屏幕的左边界的坐标是-100,这个值就是getScrollX的值,也是我们要移动时,传入scrollTo或者scrooBy方法中的值,如果,我们想将View中的内容向左移动100px,由于屏幕不能移动,则只能是画布向左移动100px,此时,屏幕的左边界的坐标是100,所以,我们要给scrollTo或scrollBy方法出入的值就是100。大家可以按照这个思路来理解。

讲解完View的scrollTo方法和scrollBy方法后,我们在看看Scroller类的,首先看看Scroller类的构造方法:

/**
     * 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;
        if (interpolator == null) {
            mInterpolator = new ViscousFluidInterpolator();
        } else {
            mInterpolator = interpolator;
        }
        mPpi = context.getResources().getDisplayMetrics().density * 160.0f;
        mDeceleration = computeDeceleration(ViewConfiguration.getScrollFriction());
        mFlywheel = flywheel;

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

以看到,无论使用一个参数的还是两个参数的构造方法,最终都还是会调用三个参数的构造方法,一般我们在项目中,都是使用一个构造方法,这时,虽然我们没有指定interpolator,系统会为我们创建一个ViscousFluidInterpolator类型的interpolator。一般,我们在项目中,使用Scroller的startScroll方法,配合View的invalidate方法,并重写View的computeScroll方法,具体示例代码如下:
在某个需要处理弹性滑动的地方,调用以下代码:

    mScroller.startScroll(getScrollX(), 0, dx2, 0);
    invalidate();

下面是重写的View的computeScroll方法的具体代码

    @Override
    public void computeScroll() {
        super.computeScroll();
        if(mScroller.computeScrollOffset()){
            scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
            invalidate();
        }
    }

下面我们看看,Scroller的startScroll方法的源码:

public void startScroll(int startX, int startY, int dx, int dy) {
        startScroll(startX, startY, dx, dy, DEFAULT_DURATION);
    }

其内部继续调用了其重载方法,接着看这个重载的方法:

    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;
    }

从这个重载的方法可以看出,startScroll方法并未做任何的滚动操作,只是将传入的参数进行了保存。所以,我们在项目中,只是调用startScroll方法时,并不能起到滚动的效果,还需要使用View的invalidate方法,因为,这个这个方法会使View进行重绘,View的draw方法就会调用,draw方法中,也会调用computeScroll方法,这时,我们重写的computeScroll方法的逻辑就能执行,这样scrollTo方法才会执行,这样就起到了滚动的效果,接着在调用invalidate方法,这时,View 的draw方法再次被调用,这样,View的computeScroll方法又回被调用这时,scrollTo方法在次执行,这样View中的内容就再次的滚动,通过一连串的这个递归调用,每次滚动一点点距离,整个过程衔接起来就完成了平滑的滚动的效果。既然是递归的过程,肯定是要有个终点的,否则,就成了死循环了,所以我们在重写computeScroll方法的时候,加了一个if(mScroller.computeScrollOffset())的条件判断,让我们来看看Scroller类中的这个方法具体实现:

public boolean computeScrollOffset() {
        if (mFinished) {
            return false;
        }

        int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
    
        if (timePassed < mDuration) {
            switch (mMode) {
            case SCROLL_MODE:
                final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);
                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;
    }

可以看到,只要是mFinished不为true,则这个方法的返回值一直是true的,也就是说,只要我们设置要移动的位置还未移动到,这个mFinished就不会是true,则这个方法就一直返回true,那么computeSCroll中的if条件总是成立的,这样就会不停的递归,最终完成移动到指定位置的滚动效果。在mFinished为false时,也就是还未滚动到指定位置时,会先计算一个timePassed,如果这个时间小于指定的滚动时间(这个指定的滚动时间,我们可以自己设定,如果自己不设置,系统会给一个默认值250毫秒)则会走SCROLL_MODE这个case,这个case中,会根据timePassed,计算出最新的mCurrX和mCurrY,这样我们在computeScroll方法中,通过mScroller.getCurrX(),获取到的就是最新的mCurrX的值,这样,scrllTo方法,才会将view的内容滚动到最新的的位置。

总结:Scroller的移动的原理就是,调用startScroll方法,告诉系统,要移动的起始点,以及移动的距离,还有完成这个移动的距离的时间,并且配合View的computeScroll方法,通过调用View的invalidate方法进行重绘,这样computeScroll方法就会执行,根据持续时间,来计算出最新需要移动的位置,然后移动到这个最新的位置,通过递归这个过程完成滚动的效果。

关于TouchSlop:
ViewConfiguration.get(context).getScaledTouchSlop();
ViewConfiguration.get(context).getScaledPagingTouchSlop();
getScaledPagingTouchSlop()获取的常量是getScaledTouchSlop()获取的常量值的2倍。
这两个方法获取的常量值,是用于判断,手指在屏幕上滑动了多大的距离,才算是
用户想开始在屏幕上进行滑动的的依据。

标签:分析,Scroller,int,scrollTo,源码,移动,方法,View
来源: https://blog.csdn.net/hujin2017/article/details/100561100