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