1.前言:
如果你要在自定义View中添加滑动效果,那么使用Scroller可能是一个不错的选择,今天我们就来介绍一下Scroller。
2.简单使用
先看一下实现的效果图:
Scroller的使用方式很简单,在使用之前,如果对
View.scrollTo(int x, int y)
方法不了解(这里有必要申明一下,View.scrollTo(int x, int y)
不是静态方法,之所以加上View.
是为了和下面Scroller
中的方法区分下来,下面如非特殊申明,都是该类下的普通方法),建议先看一下View.scrollTo(int x, int y),如下则是我们的使用介绍:
- 先创建一个自定义View类
- 然后继承View
- 创建一个方法,调用
Scroller.startScroll(x,x,x,x);
(这边参数省略了)和重新绘制方法View.invalidate()
- 在
View.computeScroll()
不断进行判断是否完成绘制,如果没有完成,还需调用滚动方法View.scrollTo(int x, int y)
和重新绘制方法View.invalidate()
最后我们还是来看一下代码:
public class ScrollerView extends View {
private Bitmap mBitmap;
private Paint mPaint;
private Scroller mScroller;
public ScrollerView(Context context) {
this(context,null);
}
public ScrollerView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context);
}
private void init(Context context) {
mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.beauty);
mPaint = new Paint();
mScroller = new Scroller(context);
}
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
canvas.drawBitmap(mBitmap,100,100,mPaint);
}
public void scroll(){
mScroller.startScroll(0,0,0,-400,10000);
invalidate();
}
@Override
public void computeScroll() {
super.computeScroll();
if(mScroller.computeScrollOffset()){
scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
// 刷新绘制的界面
invalidate();
}
}
}
代码还是很简单的。
3. 从过程中分析源码
为了更直观的学习Scroller的调用流程,我用流程图展现了出来,先看图:
档案管理流程图.jpg
- 我们首先调用
Scroller.startScroll(int startX, int startY, int dx, int dy, int 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;
}
- 然后我们需要手动调用
View.invalidate()
刷新我们的前面,因为我们的View不会主动刷新界面,我们都知道View.invalidate()
会通知我们的界面进行重绘,这个时候View. draw(Canvas canvas)
就会被调用。细心的你这个时候可能就发现了,我们的View.computeScroll()
方法没有参与进来!别急,别急,我们来看一下缩减后的View. draw(Canvas canvas)
的源码:
/**
* This method is called by ViewGroup.drawChild() to have each child
* view draw itself.
* This is where the View specializes rendering behavior based on layer type,
* and hardware acceleration.
*/
boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
...
if (!drawingWithRenderNode) {
computeScroll();
sx = mScrollX;
sy = mScrollY;
}
...
if (!drawingWithDrawingCache) {
if (drawingWithRenderNode) {
...
} else {
// Fast path for layouts with no backgrounds
if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
...
} else {
draw(canvas);
}
}
}
...
}
其实在通知界面重绘的时候是先调用我们上面的draw(Canvas canvas, ViewGroup parent, long drawingTime)
方法,然后在依次调用了View.computeScroll()
和View. draw(Canvas canvas)
方法,而View中的View.computeScroll()
都是空实现,所以需要我们继承的时候自己复写方法。
- 下面就是讲我们的重点部分了,通常我们在使用
Scroller
时,都会复写View.computeScroll()
方法,之后我们会调用Scroller.computeScrollOffset()
来判断滑动有没有完成,如下代码:
@Override
public void computeScroll() {
super.computeScroll();
if(mScroller.computeScrollOffset()){
scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
// 刷新绘制的界面
invalidate();
}
}
整个Scroller
的核心部分就是Scroller.computeScrollOffset()
方法了,我们还是来看源码:
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);
// 重点部分 根据Interpolator插值器计算在该时间段里移动的距离加上初始赋值赋值给mCurrX和mCurrY
mCurrX = mStartX + Math.round(x * mDeltaX);
mCurrY = mStartY + Math.round(x * mDeltaY);
break;
case FLING_MODE:
//滑动时,抬起手执行的惯性运动,通过复杂的运算获取当前的mCurrX 、mCurrY 值。
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;
}
可以看到,Scroller.computeScrollOffset()
并非简单的判断滑动是否完成,它还计算了当前应当滑动到的距离,最后在我们自定义的View
中,通过scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
完成实现滑动过程,接着调用View.invalidate()
重复界面刷新到绘制的过程,直到我们整个滑动过程完成。到这里,我们的整个过程就结束了。
4. 引用
Scroller 解析
<<Android开发艺术探索>>
网友评论