Scroller的使用,让View随心所欲的移动

来源:互联网 发布:python统计字母个数 编辑:程序博客网 时间:2024/05/01 17:01

Scroller的使用,让View随心所欲的移动吧

效果图:





Scroller能控制View自由的移动,相对于其他让View移动的api,例如ViewDragHelper,
我个人倾向于使用Scroller,因为他使用起来简单,感觉更灵活一些。

值得注意的是,在一个View使用Scroller是不能让这个View移动的,
需要在ViewGroup或者他的子类里使用Scroller,才能让ViewGroup里的所有子View一起移动起来。

下面说Scroller让ViewGroup的所有子类移动的方法:



1,首先,在ViewGroup中创建Scroller对象。

    public class ScrollViewGroup extends FrameLayout {        private static final String TAG = ScrollViewGroup.class.getSimpleName();        private Scroller mScroller;        private int mHeight;        private int mWidth;        public ScrollViewGroup(Context context) {        super(context);        init();    }    public ScrollViewGroup(Context context, AttributeSet attrs) {        super(context, attrs);        init();    }    public ScrollViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {        super(context, attrs, defStyleAttr);        init();    }    private void init() {        mScroller = new Scroller(getContext());    }



2,其次,复写View的 computeScroll() 方法,代码为模版代码,复制使用即可。

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



3,然后,使用Scroller,让ViewGroup的子View移动起来。

    public void smoothScrollTo(int startX, int startY, int dx, int dy, int duration) {        mScroller.startScroll(startX, startY, dx, dy, duration);        invalidate();    }



就这三步骤,就能让ViewGroup里的所有子View移动了。



下面说怎么玩Scroller



看这行代码:

        mScroller.startScroll(startX, startY, dx, dy, duration);        invalidate();


startX是这个ViewGroup开始的X坐标,就是ViewGroup左上角的X坐标。而startY对应的是Y坐标。意思是,你想让ViewGroup初始点在哪里。
dx是X坐标的偏移量,你想让这个ViewGroup里的子View在X轴方向移动多少距离。而dy是Y坐标的偏移量。
duration是完成这一次移动所需要的时间。



如图:


例如,让ViewGroup里的子View,向下移动整个ViewGroup的高度,也就是上面动态图的向下移动down


1,首先,获取到ViewGroup的高度。

    @Override    protected void onSizeChanged(int w, int h, int oldw, int oldh) {        super.onSizeChanged(w, h, oldw, oldh);        mHeight = getMeasuredHeight();        mWidth = getMeasuredWidth();        Log.i(TAG, "height:" + mHeight + " ,width:" + mWidth);    }


2,其次,设置参数,使用Scroller完成移动。

    public void down() {        int startX = 0;        int startY = 0;        int dx = 0;        int dy = -mHeight;        int duration = 2000;        smoothScrollTo(startX, startY, dx, dy, duration);    }


值得注意的是,开始点0,0,也就是手机屏幕左上角那一点,移动Y方向的偏移量为 -mHeight,没错,是负的ViewGroup的高度。

这样,就可以让ViewGroup里的所有子View向下移动了。

接下来,来分析Scroller是如何完成移动的工作的。


看这两行代码:

        mScroller.startScroll(startX, startY, dx, dy, duration);        invalidate();


CTRL+B,进入到startScroll方法里:

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


可以看到,方法里把传递进来的参数赋值了一下,开始坐标,偏移量,使用的时间,还有移动后的坐标mFinalX,Y


再看这个方法:

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


模版代码,一直可以这样写。进入到computeScrollOffset里:

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


可以看到,当mFinished为true时返回false,也就是不会继续走computeScroll里的代码了,在继续看:

    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;


可以看出,mCurrX ,Y 的值是有开始坐标不断的增加的。增加的与x有关,注意这里的timePassed ,可以看出,这是一个不断接近duration的值,
mStartTime 在之前:

 mStartTime = AnimationUtils.currentAnimationTimeMillis();


为一个时间戳,开始的时间。
timePassed 是一个随着时间流逝的值,根据这个差值进行计算,累加到mCurrX ,Y,达到一个在一段时间内,不断累加的效果,设计得挺妙的。



然后:

            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());            postInvalidate();


再然后:

    public final int getCurrX() {        return mCurrX;    }


可以看到使用了增加之后的mCurrX ,Y,调用scrollTo来完成ViewGroup的子View移动的效果。

scrollTo是一下子移动到某一点,并没有动态,循序渐进的效果。那为什么会出现动态的效果呢?


答案就是这个方法:

postInvalidate


重新绘制View,也就是调用View的draw方法进行重新绘制视图。

查看View源码找到draw方法:

 boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {


发现里面有调用:

        if (!drawingWithRenderNode) {            computeScroll();            sx = mScrollX;            sy = mScrollY;        }


说明, postInvalidate调用了draw再调用了computeScroll,然而computeScroll里又调用了scrollTo,移动的偏移量又是一点一点增大的值。

噢,我明白了,原来是通过不断的重绘来调用scrollTo让ViewGroup子View一点一点的移动,最终达到动态的效果,就像帧动画一样。


然而,什么时候结束移动呢?

看这个方法里:

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


发现了:

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


让mCurrX ,Y一点一点增加,增加到终点坐标之后,mFinished等于true,return false,就不会再绘制,就不再scrollTo了,移动停止了。

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


Scroller的移动,如何实现,使用,分析一下原理,也就到这里了。总体感觉设计移动的增量使用时间戳的方式的确很精妙,可以借鉴学习,通过重绘与scrollTo的调用来完成动态效果,控制移动结束也很精妙,学习了。


要到两点了,就到这里吧。Demo下载:我的Github


2016年6月20日 1:53:48

1 0
原创粉丝点击