View类的scrollTo()和scrollBy()方法
1. scrollTo()方法
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();
}
}
}
scrollTo()方法是实现View滚动的核心,调用该方法使得View相对于其初始位置滚动某段距离。在该方法内部将输入参数x,y分别赋值给用于表示View在X方向滚动距离的mScrollX和表示View在Y方向滚动距离的mScrollY,然后调用onScrollChanged()并且刷新重绘View。在后续的操作中调用view.getScrollX()或view.getScrollY()可以很容易地得到mScrollX和mScrollY。
2. scrollBy()方法
public void scrollBy(int x, int y) {
scrollTo(mScrollX + x, mScrollY + y);
}
scrollBy()方法的源码非常简洁,它仅仅是再次调用了scrollTo()。
直白地说:它只是把输入参数x,y累加到了mScrollX和mScrollY上而已。
所以,scrollBy()方法是在mScrollX和mScrollY的基础上滚动的。
3. computeScroll()方法
public void computeScroll() {
}
computeScroll()是一个空的方法,需要子类去重写该方法来实现逻辑,到底该方法在哪里被触发呢。
computeScroll()是一个空的方法!这就是说我们需要根据自己的业务逻辑重写该方法。
其实,该方法的注释已经告诉我们了:如果使用Scroller使得View发生滚动,那么可以在该方法中处理与滑动相关的业务和数据,
比如调用scrollTo()或者scrollBy()使得View发生滚动;
比如获取变量mScrollX、mScrollY、mCurrX、mCurrY的值。
在此有一点需要注意,在处理这些业务和数据之前我们通常需要先利用computeScrollOffset()判断一下滑动是否停止然后再进行相关操作。
mScrollX和mScrollY
mScrollX和mScrollY用于描述View的内容在水平方向或垂直方向滚动的距离。
什么是View的内容呢?比如,对于一个TextView而言,文本就是它的内容;对于一个ViewGroup而言,子View就是它的内容。
故在此,我们请务必注意:scrollTo()和scrollBy()滚动的是View的内容,而不是将View做整体的移动。
小结:
mScrollX和mScrollY分别表示View在X、Y方向的滚动距离
scrollTo( )表示View相对于其初始位置滚动某段距离。
由于View的初始位置是不变的,所以如果利用相同输入参数多次调用scrollTo()方法,View只会出现一次滚动的效果而不是多次。
scrollBy( )表示在mScrollX和mScrollY的基础上继续滚动。
Scroller类
参考:Android学习Scroller(五)——详解Scroller调用过程以及View的重绘
/**
* Scroller原理:
* 为了让View或者ViewGroup的内容发生移动,我们常用scrollTo()和scrollBy()方法.
* 但这两个方法执行的速度都很快,瞬间完成了移动感觉比较生硬.
* 为了使View或者ViewGroup的内容发生移动时比较平滑或者有其他的移动渐变效果可采用Scroller来实现.
* 在具体实现时,我们继承并重写View或者ViewGroup时可生成一个Scroller,
* 由它来具体掌控移动过程和结合插值器Interpolator调用scrollTo()和scrollBy()方法.
*
*
* Scroller的两个主要构造方法:
* 1 public Scroller(Context context) {}
* 2 public Scroller(Context context, Interpolator interpolator){}
* 采用第一个构造方法时,在移动中会采用一个默认的插值器Interpolator
* 也可采用第二个构造方法,为移动过程指定一个插值器Interpolator
*
*
* Scroller的调用过程以及View的重绘:
* 1 调用public void startScroll(int startX, int startY, int dx, int dy)
* 该方法为scroll做一些准备工作.
* 比如设置了移动的起始坐标,滑动的距离和方向以及持续时间等.
* 该方法并不是真正的滑动scroll的开始,感觉叫prepareScroll()更贴切些.
*
* 2 调用invalidate()或者postInvalidate()使View(ViewGroup)树重绘
* 重绘会调用View的draw()方法
* draw()一共有六步:
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
* 1. Draw the background
* 2. If necessary, save the canvas' layers to prepare for fading
* 3. Draw view's content
* 4. Draw children
* 5. If necessary, draw the fading edges and restore layers
* 6. Draw decorations (scrollbars for instance)
* 其中最重要的是第三步和第四步
* 第三步会去调用onDraw()绘制内容
* 第四步会去调用dispatchDraw()绘制子View
* 重绘分两种情况:
* 2.1 ViewGroup的重绘
* 在完成第三步onDraw()以后,进入第四步ViewGroup重写了
* 父类View的dispatchDraw()绘制子View,于是这样继续调用:
* dispatchDraw()-->drawChild()-->child.computeScroll();
* 2.2 View的重绘
* 我们注意到在2提到的"调用invalidate()".那么对于View它又是怎么
* 调用到了computeScroll()呢?View没有子View的.所以在View的源码里可以
* 看到dispatchDraw()是一个空方法.所以它的调用路径和ViewGroup是不一样的.
* 在此不禁要问:如果一个ButtonSubClass extends Button 当mButtonSubClass
* 执行mButtonSubClass.scrollTo()方法时怎么触发了ButtonSubClass类中重写
* 的computeScroll()方法???
* 在这里我也比较疑惑,只有借助网上的资料和源码去从invalidate()看起.
* 总的来说是这样的:当View调用invalidate()方法时,会导致整个View树进行
* 从上至下的一次重绘.比如从最外层的Layout到里层的Layout,直到每个子View.
* 在重绘View树时ViewGroup和View时按理都会经过onMeasure()和onLayout()以及
* onDraw()方法.当然系统会判断这三个方法是否都必须执行,如果没有必要就不会调用.
* 看到这里就明白了:当这个子View的父容器重绘时,也会调用上面提到的线路:
* onDraw()-->dispatchDraw()-->drawChild()-->child.computeScroll();
* 于是子View(比如此处举例的ButtonSubClass类)中重写的computeScroll()方法
* 就会被调用到.
*
* 3 View树的重绘会调用到View中的computeScroll()方法
*
* 4 在computeScroll()方法中
* 在View的源码中可以看到public void computeScroll(){}是一个空方法.
* 具体的实现需要自己来写.在该方法中我们可调用scrollTo()或scrollBy()
* 来实现移动.该方法才是实现移动的核心.
* 4.1 利用Scroller的mScroller.computeScrollOffset()判断移动过程是否完成
* 注意:该方法是Scroller中的方法而不是View中的!!!!!!
* public boolean computeScrollOffset(){ }
* 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.
* 返回true时表示还移动还没有完成.
* 4.2 若动画没有结束,则调用:scrollTo(By)();
* 使其滑动scrolling
*
* 5 再次调用invalidate().
* 调用invalidate()方法那么又会重绘View树.
* 从而跳转到第3步,如此循环,直到computeScrollOffset返回false
*
* 通俗的理解:
* 从上可见Scroller执行流程里面的三个核心方法
* mScroller.startScroll()
* mScroller.computeScrollOffset()
* view.computeScroll()
* 1 在mScroller.startScroll()中为滑动做了一些初始化准备.
* 比如:起始坐标,滑动的距离和方向以及持续时间(有默认值)等.
* 其实除了这些,在该方法内还做了些其他事情:
* 比较重要的一点是设置了动画开始时间.
*
* 2 computeScrollOffset()方法主要是根据当前已经消逝的时间
* 来计算当前的坐标点并且保存在mCurrX和mCurrY值中.
* 因为在mScroller.startScroll()中设置了动画时间,那么
* 在computeScrollOffset()方法中依据已经消逝的时间就很容易
* 得到当前时刻应该所处的位置并将其保存在变量mCurrX和mCurrY中.
* 除此之外该方法还可判断动画是否已经结束.
*
*
* @Override
* public void computeScroll() {
* super.computeScroll();
* if (mScroller.computeScrollOffset()) {
* scrollTo(mScroller.getCurrX(), 0);
* invalidate();
* }
* }
* 先执行mScroller.computeScrollOffset()判断了滑动是否结束
* 2.1 返回false,滑动已经结束.
* 2.2 返回true,滑动还没有结束.
* 并且在该方法内部也计算了最新的坐标值mCurrX和mCurrY.
* 就是说在当前时刻应该滑动到哪里了.
* 既然computeScrollOffset()如此贴心,盛情难却啊!
* 于是我们就覆写View的computeScroll()方法,
* 调用scrollTo(By)滑动到那里!满足它的一番苦心吧.
*
*
* 备注说明:
* 1 示例没有做边界判断和一些优化,在这方面有bug.
* 重点是学习Scroller的流程
* 2 不用纠结getCurrX()与getScrollX()有什么差别,二者得到的值一样.
* 但要注意它们是属于不同类里的.
* getCurrX()-------> Scroller.getCurrX()
* getScrollX()-----> View.getScrollX()
*
*
*/
如图:
View滚动的实现原理
我们先调用Scroller的startScroll()方法来进行一些滚动的初始化设置,
然后迫使View进行绘制,我们调用View的invalidate()或postInvalidate()就可以重新绘制View,
绘制View的时候会触发computeScroll()方法,我们重写computeScroll(),在computeScroll()里面先调用Scroller的computeScrollOffset()方法来判断滚动有没有结束,如果滚动没有结束我们就调用scrollTo()方法来进行滚动,该scrollTo()方法虽然会重新绘制View,但是我们还是要手动调用下invalidate()或者postInvalidate()来触发界面重绘,重新绘制View又触发computeScroll(),所以就进入一个循环阶段,这样子就实现了在某个时间段里面滚动某段距离的一个平滑的滚动效果
也许有人会问,干嘛还要调用来调用去最后在调用scrollTo()方法,还不如直接调用scrollTo()方法来实现滚动,其实直接调用是可以,只不过scrollTo()是瞬间滚动的,给人的用户体验不太好,所以Android提供了Scroller类实现平滑滚动的效果。
Scroller的方法没有进行滑动,而是初始化了一堆成员变量;譬如滚动模式、开始时间、持续时间等,也就是说他们都只是工具方法而已,实质的滑动其实是需要我们在他后面手动调运View的invalidate()进行刷新,然后在View进行刷新时又会调运自己的View.computeScroll()方法,在View.computeScroll()方法中进行Scroller.computeScrollOffset()判断与触发View的滑动方法。
package android.widget;
import android.content.Context;
import android.hardware.SensorManager;
import android.os.Build;
import android.view.ViewConfiguration;
import android.view.animation.AnimationUtils;
import android.view.animation.Interpolator;
public class Scroller {
private final Interpolator mInterpolator;
private int mMode;
private int mStartX;
private int mStartY;
private int mFinalX;
private int mFinalY;
private int mMinX;
private int mMaxX;
private int mMinY;
private int mMaxY;
private int mCurrX;
private int mCurrY;
//X轴方向的偏移量和Y轴方向的偏移量,这个是一个相对距离,
//相对的不是屏幕的原点,而是View的左边缘
//向右/下滑动 mScrollX、y就为负数,向左、上滑动mScrollX、y为正数
//单位:像素
private long mStartTime;
private int mDuration;
private float mDurationReciprocal;
private float mDeltaX;
private float mDeltaY;
private boolean mFinished;
private boolean mFlywheel;
private float mVelocity;
private float mCurrVelocity;
private int mDistance;
private float mFlingFriction = ViewConfiguration.getScrollFriction();
private static final int DEFAULT_DURATION = 250;
/**
* Create a Scroller with the default duration and interpolator.
*/
public Scroller(Context context) {
this(context, null);
}
/**
* Create a Scroller with the specified interpolator. If the interpolator is
* null, the default (viscous) interpolator will be used. "Flywheel" behavior will
* be in effect for apps targeting Honeycomb or newer.
*/
public Scroller(Context context, Interpolator interpolator) {
this(context, interpolator,
context.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.HONEYCOMB);
}
/**
* Create a Scroller with the specified interpolator. If the interpolator is
* null, the default (viscous) interpolator will be used. Specify whether or
* not to support progressive "flywheel" behavior in flinging.
*/
public Scroller(Context context, Interpolator interpolator, boolean flywheel) {
mFinished = true;
if (interpolator == null) {
mInterpolator = new ViscousFluidInterpolator();
} else {
mInterpolator = interpolator;
}
mPpi = context.getResources().getDisplayMetrics().density * 160.0f;
mDeceleration = computeDeceleration(ViewConfiguration.getScrollFriction());
mFlywheel = flywheel;
mPhysicalCoeff = computeDeceleration(0.84f); // look and feel tuning
}
/**
* The amount of friction applied to flings. The default value
* is {@link ViewConfiguration#getScrollFriction}.
*
* @param friction A scalar dimension-less value representing the coefficient of
* friction.
//设置滚动持续时间
*/
public final void setFriction(float friction) {
mDeceleration = computeDeceleration(friction);
mFlingFriction = friction;
}
private float computeDeceleration(float friction) {
return SensorManager.GRAVITY_EARTH // g (m/s^2)
* 39.37f // inch/meter
* mPpi // pixels per inch
* friction;
}
/**
*
* Returns whether the scroller has finished scrolling.
*
* @return True if the scroller has finished scrolling, false otherwise.
//返回滚动是否结束
*/
public final boolean isFinished() {
return mFinished;
}
/**
* Force the finished field to a particular value.
*
* @param finished The new finished value.
//强制终止滚动
*/
public final void forceFinished(boolean finished) {
mFinished = finished;
}
/**
* Returns how long the scroll event will take, in milliseconds.
*
* @return The duration of the scroll in milliseconds.
//返回滚动持续时间
*/
public final int getDuration() {
return mDuration;
}
/**
* Returns the current X offset in the scroll.
*
* @return The new X offset as an absolute distance from the origin.
//返回当前滚动的x偏移量
*/
public final int getCurrX() {
return mCurrX;
}
/**
* Returns the current Y offset in the scroll.
*
* @return The new Y offset as an absolute distance from the origin.
//返回当前滚动的y偏移量
*/
public final int getCurrY() {
return mCurrY;
}
/**
* Returns the current velocity.
*
* @return The original velocity less the deceleration. Result may be
* negative.
//返回当前的速度
*/
public float getCurrVelocity() {
return mMode == FLING_MODE ?
mCurrVelocity : mVelocity - mDeceleration * timePassed() / 2000.0f;
}
/**
* Returns the start X offset in the scroll.
*
* @return The start X offset as an absolute distance from the origin.
//返回滚动起始点x偏移量
*/
public final int getStartX() {
return mStartX;
}
/**
* Returns the start Y offset in the scroll.
*
* @return The start Y offset as an absolute distance from the origin.
//返回滚动起始点y偏移量
*/
public final int getStartY() {
return mStartY;
}
/**
* Returns where the scroll will end. Valid only for "fling" scrolls.
*
* @return The final X offset as an absolute distance from the origin.
//返回滚动结束x偏移量
*/
public final int getFinalX() {
return mFinalX;
}
/**
* Returns where the scroll will end. Valid only for "fling" scrolls.
*
* @return The final Y offset as an absolute distance from the origin.
//返回滚动结束y偏移量
*/
public final int getFinalY() {
return mFinalY;
}
/**
* Call this when you want to know the new location. If it returns true,
* the animation is not yet finished.
* //判断滚动是否还在继续,true继续,false结束.
//该方法的作用其实就是实时计算滚动的偏移量(也是一个工具方法),同时判断滚动是否结束(true代表没结束,false代表结束)。
*/
public boolean computeScrollOffset() {
if (mFinished) {//mFinished为true表示已经完成了滑动,直接返回为false
return false;
}
//mStartTime为开始时的时间戳,timePassed就是当前滑动持续时间
int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
//mDuration为我们设置的持续时间,当当前已滑动耗时timePassed小于总设置持续时间时才进入if
if (timePassed < mDuration) {
//mMode有两种,如果调运startScroll()则为SCROLL_MODE模式,调运fling()则为FLING_MODE模式
switch (mMode) {
//根据Interpolator插值器计算在该时间段里移动的距离赋值给mCurrX和mCurrY
case SCROLL_MODE:
final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);
mCurrX = mStartX + Math.round(x * mDeltaX);
mCurrY = mStartY + Math.round(x * mDeltaY);
break;
//各种数学运算获取mCurrY、mCurrX,实质类似上面SCROLL_MODE,只是这里时惯性的
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 {
//认为滑动结束,mFinished置位true,标记结束,下一次再触发该方法时一进来就判断返回false了
mCurrX = mFinalX;
mCurrY = mFinalY;
mFinished = true;
}
return true;
}
/**
//在我们想要滚动的地方调用,准备开始滚动,默认滚动时间为DEFAULT_DURATION. //滑动到指定位置
*/
public void startScroll(int startX, int startY, int dx, int dy) {
startScroll(startX, startY, dx, dy, DEFAULT_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;
}
}
}