前几天接到的任务中涉及到控件移动的问题,以前都是通过更新控件内容+补间动画来做的,但发觉应付连续多次请求时,有时刷新不及时导致延迟,画面出现重叠的情况。苦思许久,最后在同事的帮助下参考源码中Launcher应用的方法完美搞定。在这里必须分享一下……

在一切开始之前有必要先说说三个知识点:

1.scrollTo()和scrollBy()方法

如何移动控件,Android Framework中已经给我们留了接口。源码中控件基类View.java类的定义中有如下片段:

/**
* The offset, in pixels, by which the content of this view is scrolled
* horizontally.
* {@hide}
*/
@ViewDebug.ExportedProperty
protected int mScrollX;
/**
* The offset, in pixels, by which the content of this view is scrolled
* vertically.
* {@hide}
*/
@ViewDebug.ExportedProperty
protected int mScrollY;
/**
* 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;
onScrollChanged(mScrollX, mScrollY, oldX, oldY);
if (!awakenScrollBars()) {
invalidate();
}
}
}
/**
* 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);
}

“mScrollX”,“mScrollY”可以记录当前控件在父窗体中的偏移量,而调用scrollTo()和scrollBy()你就可以轻松的移动你的控件了。

2.Scroller类

尝试上面的方法,你很快就会发现它的用户体验非常差了,每次你的view会瞬间移动到指定的位置,给人一顿一顿的感觉。怎样才能控制view的偏移过程,从而使偏移更流畅,更完美呢?这一点并不需要我们做太多工作,因为Android Framework已经为我们提供了Scroller.java类,将一个迁移的动作变成了一个可控的过程,代码片段如下:

private int mCurrX;
private int mCurrY;
……
/**
* 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.
*/
public boolean computeScrollOffset() {
if (mFinished) {
return false;
}
int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
if (timePassed 
switch (mMode) {
case SCROLL_MODE:
float x = (float)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:
float timePassedSeconds = timePassed / 1000.0f;
float distance = (mVelocity * timePassedSeconds)
- (mDeceleration * timePassedSeconds * timePassedSeconds / 2.0f);
mCurrX = mStartX + Math.round(distance * mCoeffX);
// Pin to mMinX <= mCurrX <= mMaxX
mCurrX = Math.min(mCurrX, mMaxX);
mCurrX = Math.max(mCurrX, mMinX);
mCurrY = mStartY + Math.round(distance * mCoeffY);
// Pin to mMinY <= mCurrY <= mMaxY
mCurrY = Math.min(mCurrY, mMaxY);
mCurrY = Math.max(mCurrY, mMinY);
break;
}
}
else {
mCurrX = mFinalX;
mCurrY = mFinalY;
mFinished = true;
}
return true;
}
/**
* Start scrolling by providing a starting point and the distance to travel.
*
* @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;
// This controls the viscous fluid effect (how much of it)
mViscousFluidScale = 8.0f;
// must be set to 1.0 (used in viscousFluid())
mViscousFluidNormalize = 1.0f;
mViscousFluidNormalize = 1.0f / viscousFluid(1.0f);
}

忽略细节,重点关注其中比较重要的两个方法的作用:

public boolean computeScrollOffset()

根据当前已经消逝的时间计算当前的坐标点,保存在mCurrX和mCurrY值中

public void startScroll(int startX, int startY, int dx, int dy, int duration)

开始一个动画控制,由(startX , startY)在duration时间内前进(dx,dy)个单位,到达坐标为(startX+dx , startY+dy)处。

3. computeScroll()方法

如何去控制这个流程,Android Framework提供了computeScroll()方法,方法位于ViewGroup.java类中。为了实现偏移控制,一般自定义控件都需要重载该方法。其调用过程位于View绘制流程draw()过程中,用于由父View调用用来请求子View根据偏移值mScrollX,mScrollY重新绘制

了解了以上的知识点,相信实现这个功能已经不是难事了,大概的思路如下;

首先,自定义一个控件继承于View,添加一个Scroller类型的成员变量mScroller;

其次,调用mScroller的startScroll()去产生一个偏移控制,手动调用invalid()方法去重新绘制控件;

最后,重写的computeScroll()方法,通过mScroller的computeScrollOffset()方法,根据当前已经逝去的时间,获取当前应该偏移的坐标;调用scrollBy()方法去逐步偏移,调用postInvalidate()方法刷新控件;

大致的代码如下,仅供参考:

public void nextMenuItem(){
……//一系列更新子View的逻辑操作
//更新结束后,使用动画控制偏移过程, 3s内到位
mScroller.startScroll(0, 0, menuItemWidth, 0,3000);
//重绘控件
invalidate();
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) { //如果返回true,表示动画还没有结束
//产生平滑的动画效果,根据当前偏移量,每次滚动一点
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
//此时同样也需要刷新View,否则效果可能有误差
postInvalidate();
} else { //如果返回false,表示startScroll完成
Log.i(tag, " scoller has finished -----");
}