之前写了一个滚动选择控件
WheelView,在这个控件中我设计了弹性滚动的实现机制,再了解View弹性滚动之前,我们先来学习一下View滚动机制的实现.
这里基于Android5.0版本的源码介绍View类中这两个函数的具体实现.
scrollTo源码如下:
/** * 对View设置滚动的x和y轴坐标. * @param x x轴滚动的终点坐标 * @param y y轴滚动的终点坐标 */
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();
}
}
}
scrollBy源码如下:
/** * 设置View的x轴和y轴的滚动增量. * @param x x轴的滚动增量 * @param y y轴的滚动增量 */
public void scrollBy(int x, int y) {
scrollTo(mScrollX + x, mScrollY + y);
}
从上述源码可以看出,scrollBy依赖于scrollTo的实现.他俩的区别是:
scrollBy的x和y是滚动增量,即在上次滚动的终点坐标上增加x和y,是相对滑动.(注意:x和y可能为负数)
scrollTo的x和y是滚动的终点坐标,是绝对滑动.
而且,scrollTo的实现也仅仅是修改了mScrollX和mScrollY的值,然后调用了invalidate方法重绘了View.那么,mScrollX和mScrollY的含义是什么呢?
同时,需要明确很重要的一点:View的滑动并非是View的滑动,而是View内容的滑动.
提供一个图示来理解View的滑动:
缺陷:
虽然调用View的scrollBy和scrollTo方法可以很方便的实现View的滚动,但是这种滚动是瞬间完成的(调用invalidate方法),没有弹性滑动的效果,为了达到弹性滑动的目的,我们开始介绍本篇文章的主角:Scroller.
在介绍Scroller之前,我们需要明确知道:
Scroller代码和View代码完全解耦,Scroller代码本身不会引起View的滑动,通过Scroller代码,我们可以平滑的获取当前View需要滑动的位置,然后调用View的scrollTo/scrollBy进行移动.
我们先来看一下Scroller的构造函数注释源码:
/** * 使用默认的滑动时间和插值器构造Scroller. */
public Scroller(Context context) {
this(context, null);
}
/** * 使用给定的插值器来构造Scroller. */
public Scroller(Context context, Interpolator interpolator) {
this(context, interpolator,
context.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.HONEYCOMB);
}
/** * 使用给定的插值器来构造Scroller.Android3.0以上的版本支持"flywheel"的行为. */
public Scroller(Context context, Interpolator interpolator, boolean flywheel) {
// 设置滑动停止标识位为true
mFinished = true;
// 构造插值器
if (interpolator == null) {
mInterpolator = new ViscousFluidInterpolator();
} else {
mInterpolator = interpolator;
}
// 获取屏幕的密度(每英寸的像素数)
mPpi = context.getResources().getDisplayMetrics().density * 160.0f;
// 计算摩擦力
mDeceleration = computeDeceleration(ViewConfiguration.getScrollFriction());
// 标记是否支持flying模式
mFlywheel = flywheel;
mPhysicalCoeff = computeDeceleration(0.84f); // look and feel tuning
}
Scroller支持两种模式的滑动,分别是:
接下来,针对这两种模式进行分别讲解.
我们直接看一下startScroll的源码做了哪些事情:
/** * 给定滚动起始点坐标,在指定的时间内滚动指定的偏移量. * 距离计算: * dx=view左边缘-view内容左边缘;dx为正,代表内容向左移动;dx为负,代表内容向右移动. * dy=view上边缘-view内容上边缘;dy为正,代表内容向上移动;dx为负,代表内容向下移动. * * @param startX x轴方向滚动起始点坐标. * @param startY y轴方向滚动起始点坐标. * @param dx x轴方向滚动距离. * @param dy y轴方向滚动距离. * @param duration 滚动持续的时间(默认滚动时间为250ms). */
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;
}
通过注释的源码,我们可以验证最初的结论:Scroller和View完全解耦,Scroller并不会直接控制View的滑动,它只是为View提供滑动的参数.具体参数包括:
之所以这里提前介绍computeScrollOffset函数,是因为View只有配合computeScrollOffset函数,才能实现真正的滑动.源码中跟SCROLL_MODE相关代码如下:
public boolean computeScrollOffset() {
// 如果已经结束,直接返回false.
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;
// 处理fling模式......
}
} else {
// 当时间结束时,直接将x和y坐标置为终止状态的x和y坐标,同时将终止标志位置为true.
mCurrX = mFinalX;
mCurrY = mFinalY;
mFinished = true;
}
return true;
}
可以看出,computeScrollOffset也只是根据时间偏移计算x轴和y轴应该到达的坐标.
介绍了SCROLL_MODE的具体实现,接下来就通过代码演示一下Scroller是如何和View进行互动的.这里提供一个例子,在40秒内将TextView的内容在x轴向右移动400:
private void initScrollCase() {
mImageView = (ImageView) findViewById(R.id.id_img_tv);
// 获取起始滑动点坐标
int startX = mImageView.getScrollX();
int startY = mImageView.getScrollY();
mScroller = new Scroller(getApplicationContext());
Log.e("zhengyi.wzy", "startX=" + startX + ", startY=" + startY);
mScroller.startScroll(startX, startY, -400, 0, 40000);
mImageView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
boolean isFinished = mScroller.computeScrollOffset();
if (!isFinished) {
return;
}
// 获取当前滑动点坐标
int x = mScroller.getCurrX();
int y = mScroller.getCurrY();
mImageView.scrollTo(x, y);
mHandler.postDelayed(this, 25);
}
}, 25);
}
});
}
千万注意:起始点是View.getScrollX()和View.getScrollY(),而不是View.getLeft()或者View.getTop()
Scroller还提供一种FLING模式,我认为它的中文翻译应该叫“抛掷模式”.fling的英文注释源码如下:
/** * Start scrolling based on a fling gesture. The distance travelled will * depend on the initial velocity of the fling. * * @param startX x轴的起始坐标. * @param startY y轴的起始坐标. * @param velocityX x轴方向的初始速率. * @param velocityY y轴方向的初始速率. * @param minX x轴终点最小值. * @param maxX x轴终点最大值. * @param minY y轴终点最小值. * @param maxY y轴终点最大值. */
public void fling(int startX, int startY, int velocityX, int velocityY,
int minX, int maxX, int minY, int maxY) {
// 如果上次滑动也是FLING_MODE并且滑动没有结束
if (mFlywheel && !mFinished) {
// 获取之前的总速率
float oldVel = getCurrVelocity();
float dx = (float) (mFinalX - mStartX);
float dy = (float) (mFinalY - mStartY);
float hyp = (float) Math.sqrt(dx * dx + dy * dy);
float ndx = dx / hyp;
float ndy = dy / hyp;
// 通过距离比例计算出x轴和y轴的速率
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;
}
}
// 设置模式为FLING_MODE
mMode = FLING_MODE;
// 设置结束标志位为false.
mFinished = false;
// 根据勾股定理获取总的速率
float velocity = (float) Math.sqrt(velocityX * velocityX + velocityY * velocityY);
mVelocity = velocity;
// 通过速率获取滑动的持续时间
mDuration = getSplineFlingDuration(velocity);
// 获取滑动起始时间
mStartTime = AnimationUtils.currentAnimationTimeMillis();
// 获取起始x轴和y轴坐标
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));
// 计算终点的x轴和y轴坐标
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);
}
跟FLING_MODE模式相关源码如下:
/** * 用来返回当前View需要移动到的x轴和y轴坐标. */
public boolean computeScrollOffset() {
// 如果已经结束,直接返回false.
if (mFinished) {
return false;
}
// 计算已经度过的时间.
int timePassed = (int) (AnimationUtils.currentAnimationTimeMillis() - mStartTime);
if (timePassed < mDuration) {
switch (mMode) {
// 处理fling模式
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 {
// 当时间结束时,直接将x和y坐标置为终止状态的x和y坐标,同时将终止标志位置为true.
mCurrX = mFinalX;
mCurrY = mFinalY;
mFinished = true;
}
return true;
}
FLING_MODE在通过速率计算当前的位置的代码我还不是特别清楚,主要是算法的实现,可能我物理太久没碰生疏了.但是用法都是统一的,至于FLING模式的使用场景,大家可以结果手势检测类(GestureDetector)去进行使用.