以前買了一本《Android 開(kāi)發(fā)藝術(shù)探索》,當(dāng)時(shí)看完也是感覺(jué)受益匪淺锡移,書(shū)上面也是留下了努力學(xué)習(xí)的筆記呕童,哈哈,結(jié)果不知道怎么搞丟了淆珊,也是艱難夺饲,最近又新買了一本,看起來(lái)還是感覺(jué)受益匪淺,哈哈往声。
先看一個(gè)簡(jiǎn)單的使用Scroller的例子
從上面的圖片中也可以看出來(lái)擂找,這里的滾動(dòng)是指View內(nèi)容的滾動(dòng)而非View本身位置的改變。
先來(lái)一張流程圖烁挟。
上面例子中使用到的自定義的TestSmoothScrollView婴洼,代碼如下
class TestSmoothScrollView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private var scroller: Scroller = Scroller(context)
private val paint = Paint()
private var color: Int = 0
init {
color = context.resources.getColor(R.color.colorAccent)
paint.color = color
paint.style = Paint.Style.FILL
}
override fun onDraw(canvas: Canvas) {
canvas.drawColor(color)
paint.color = context.resources.getColor(R.color.colorPrimary)
canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), paint)
}
/**
* 使用 scroller滾動(dòng)
*
* @param destX 在水平方滾動(dòng)到的目的地
* @param destY 豎直方向上滾動(dòng)的目的地
*/
fun smoothScrollTo(destX: Int, destY: Int) {
//要滾動(dòng)的距離
val deltaX = destX - scrollX
val deltaY = destY - scrollY
scroller.startScroll(scrollX, scrollY, deltaX, deltaY, 1000)
invalidate()
}
override fun computeScroll() {
if (scroller.computeScrollOffset()) {
scrollTo(scroller.currX, scroller.currY)
invalidate()
}
}
}
調(diào)用TestSmoothScrollView的smoothScrollTo方法即可實(shí)現(xiàn)滾動(dòng)。
btnStartScroll.setOnClickListener {
//向右下方向滾動(dòng)100像素
smoothScrollView.smoothScrollTo(-100, -100)
}
我們先來(lái)看一下TestSmoothScrollView的smoothScrollTo方法
/**
* 使用 scroller滾動(dòng)
*
* @param destX 在水平方滾動(dòng)到的目的地
* @param destY 豎直方向上滾動(dòng)的目的地
*/
fun smoothScrollTo(destX: Int, destY: Int) {
//計(jì)算出要滾動(dòng)的距離
val deltaX = destX - scrollX
val deltaY = destY - scrollY
//注釋1處
scroller.startScroll(scrollX, scrollY, deltaX, deltaY, 1000)
//注釋2處
invalidate()
}
首先我們根據(jù)View當(dāng)前的scrollX撼嗓,scrollY 和傳入的參數(shù)計(jì)算出水平和豎直方向上要滾動(dòng)的距離柬采。然后在注釋1處調(diào)用了Scroller的startScroll方法。
Scroller的startScroll方法
/**
* 通過(guò)提供一個(gè)起點(diǎn)且警,滾動(dòng)距離和滾動(dòng)時(shí)間開(kāi)始滾動(dòng)粉捻。
*
* @param startX 水平方向上的滾動(dòng)起點(diǎn),單位是像素斑芜。
* @param startY 豎直方向上的滾動(dòng)起點(diǎn)肩刃,單位是像素。
* @param dx 水平滾動(dòng)距離杏头,單位是像素盈包。正值會(huì)使View的內(nèi)容向左滾動(dòng)。
* @param dy 豎直方向上的滾動(dòng)距離醇王,單位是像素呢燥。正值會(huì)使View的內(nèi)容向上滾動(dòng)。
* @param 滾動(dòng)時(shí)間寓娩,單位是毫秒
*/
public void startScroll(int startX, int startY, int dx, int dy, int duration) {
//為mMode賦值為SCROLL_MODE
mMode = SCROLL_MODE;
mFinished = false;
mDuration = duration;
//開(kāi)始滾動(dòng)時(shí)間
mStartTime = AnimationUtils.currentAnimationTimeMillis();
mStartX = startX;
mStartY = startY;
mFinalX = startX + dx;
mFinalY = startY + dy;
mDeltaX = dx;
mDeltaY = dy;
//滾動(dòng)時(shí)間的倒數(shù)
mDurationReciprocal = 1.0f / (float) mDuration;
}
在Scroller的startScroll方法中叛氨,代碼很簡(jiǎn)單,只是保存了某些值棘伴。有幾個(gè)比較重要的點(diǎn)寞埠。
- 將mMode賦值為SCROLL_MODE
- 為開(kāi)始滾動(dòng)時(shí)間mStartTime賦值
- 計(jì)算滾動(dòng)時(shí)間的倒數(shù)mDurationReciprocal
但是我們發(fā)現(xiàn)這里并沒(méi)有讓View滾動(dòng)起來(lái)。那么View是怎么滾動(dòng)起來(lái)的呢焊夸,答案就是TestSmoothScrollView的smoothScrollTo方法的注釋2處仁连,調(diào)用了invalidate
方法。
調(diào)用invalidate以后淳地,會(huì)導(dǎo)致View重繪怖糊,View在重繪過(guò)程中又會(huì)調(diào)用computeScroll方法,而computeScroll又會(huì)從Scroller中獲取當(dāng)前的scrollX和scrollY然后通過(guò)scrollTo方法實(shí)現(xiàn)滾動(dòng)颇象。接著又調(diào)用invalidate方法進(jìn)行第二次重繪伍伤,如此反復(fù)直到滑動(dòng)到最終的位置。
調(diào)用invalidate以后遣钳,會(huì)導(dǎo)致TestSmoothScrollView重繪扰魂,TestSmoothScrollView的父View的drawChild()方法會(huì)調(diào)用下面的方法來(lái)讓TestSmoothScrollView繪制自己。
/**
* ViewGroup的drawChild()方法會(huì)調(diào)用該方法來(lái)讓每個(gè)子View來(lái)繪制自己。
*
* 在這里View會(huì)根據(jù) layer type 來(lái)指定 渲染行為和硬件加速劝评。
*/
boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
//...
if (drawingWithRenderNode) {
//注釋1處
renderNode = updateDisplayListIfDirty();
if (!renderNode.isValid()) {
renderNode = null;
drawingWithRenderNode = false;
}
}
//...
}
注釋1處姐直,調(diào)用updateDisplayListIfDirty方法。
public RenderNode updateDisplayListIfDirty() {
try {
if (layerType == LAYER_TYPE_SOFTWARE) {
//...
} else {
//注釋1處
computeScroll();
//注釋2處
canvas.translate(-mScrollX, -mScrollY);
//注釋3處
draw(canvas);
}
}
//...
}
注釋1處蒋畜,調(diào)用computeScroll方法声畏,我們重寫(xiě)了該方法,方法內(nèi)部會(huì)計(jì)算出當(dāng)前的mScrollX
和mScrollY
姻成。
注釋2處插龄,畫(huà)布偏移mScrollX
和mScrollY
,這才是實(shí)現(xiàn)內(nèi)容滾動(dòng)的根本原因?普埂>巍!
注釋3處才睹,在偏移了的畫(huà)布上繪制內(nèi)容徘跪,表現(xiàn)出來(lái)的結(jié)果就是我們的內(nèi)容偏移了。畫(huà)布每一幀都偏移一點(diǎn)琅攘,從而產(chǎn)生了滾動(dòng)的效果垮庐。
接下來(lái),我們看一看我們重寫(xiě)的computeScroll方法坞琴。
override fun computeScroll() {
//注釋1處突硝,
if (scroller.computeScrollOffset()) {
//注釋2處,調(diào)用scrollTo方法
scrollTo(scroller.currX, scroller.currY)
//繼續(xù)調(diào)用invalidate方法請(qǐng)求重繪置济。
invalidate()
}
}
我們看下注釋1處Scroller的computeScrollOffset方法
/**
* 如果你想知道新的位置,請(qǐng)調(diào)用這個(gè)方法锋八。如果該方法返回true浙于,說(shuō)明動(dòng)畫(huà)還沒(méi)結(jié)束。
*/
public boolean computeScrollOffset() {
if (mFinished) {
return false;
}
//計(jì)算流逝的時(shí)間
int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
//如果沒(méi)有結(jié)束
if (timePassed < mDuration) {
switch (mMode) {//注意挟纱,我們?cè)谏厦鏋閙Mode賦值為SCROLL_MODE
case SCROLL_MODE:
//插值器根據(jù)流逝的時(shí)間計(jì)算應(yīng)改變的百分比x
final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);
mCurrX = mStartX + Math.round(x * mDeltaX);
mCurrY = mStartY + Math.round(x * mDeltaY);
break;
//...
}
}
else {
//結(jié)束
mCurrX = mFinalX;
mCurrY = mFinalY;
mFinished = true;
}
return true;
}
在上面的方法中羞酗,如果已經(jīng)到了滾動(dòng)的結(jié)束時(shí)間,那么滾動(dòng)結(jié)束紊服,該方法返回false檀轨。
如果時(shí)間還沒(méi)有到滾動(dòng)的結(jié)束時(shí)間,步驟如下:
- 計(jì)算流逝的時(shí)間
- 插值器根據(jù)流逝的時(shí)間計(jì)算應(yīng)改變的百分比x
- 計(jì)算當(dāng)前應(yīng)該滾動(dòng)到的位置賦值給mCurrX欺嗤,mCurrY参萄。
- 返回true。
如果返回了true煎饼,則View會(huì)調(diào)用scrollTo方法保存當(dāng)前應(yīng)該到達(dá)位置讹挎,然后繼續(xù)調(diào)用invalidate方法請(qǐng)求重繪。
if (scroller.computeScrollOffset()) {
//調(diào)用scrollTo方法滾動(dòng)到當(dāng)前應(yīng)該到達(dá)位置currX,currY
scrollTo(scroller.currX, scroller.currY)
//繼續(xù)調(diào)用invalidate方法請(qǐng)求重繪筒溃。
invalidate()
}
View的scrollTo方法马篮。
public void scrollTo(int x, int y) {
if (mScrollX != x || mScrollY != y) {
int oldX = mScrollX;
int oldY = mScrollY;
//為mScrollX和mScrollY賦值
mScrollX = x;
mScrollY = y;
invalidateParentCaches();
onScrollChanged(mScrollX, mScrollY, oldX, oldY);
if (!awakenScrollBars()) {
postInvalidateOnAnimation();
}
}
}
Scroller的Fling
/**
* 依據(jù)一個(gè)fling手勢(shì)開(kāi)始滾動(dòng)。滾動(dòng)的距離依賴fling的初始速度怜奖。
*
* @param startX 滾動(dòng)起點(diǎn)的X坐標(biāo)
*
* @param startY 滾動(dòng)起點(diǎn)的Y坐標(biāo)
*
* @param velocityX 在X坐標(biāo)軸上的初始速度浑测,測(cè)量單位是像素/秒
*
* @param velocityY 在Y坐標(biāo)軸上的初始速度,測(cè)量單位是像素/秒
*
* @param minX 最小的X軸上的值歪玲。scroller的滾動(dòng)不會(huì)超過(guò)這個(gè)點(diǎn)
*
* @param maxX 最大的X軸上的值迁央。scroller的滾動(dòng)不會(huì)超過(guò)這個(gè)點(diǎn)。
*
* @param minY 最小的Y軸上的值读慎。scroller的滾動(dòng)不會(huì)超過(guò)這個(gè)點(diǎn)漱贱。
*
* @param maxY 最小的Y軸上的值。scroller的滾動(dòng)不會(huì)超過(guò)這個(gè)點(diǎn)夭委。
*
*/
public void fling(int startX, int startY, int velocityX, int velocityY,
int minX, int maxX, int minY, int maxY) {
//繼續(xù)一個(gè)未結(jié)束的滾動(dòng)或者fling
if (mFlywheel && !mFinished) {
//一個(gè)未結(jié)束的滾動(dòng)或者fling當(dāng)前的速度
float oldVel = getCurrVelocity();
float dx = (float) (mFinalX - mStartX);
float dy = (float) (mFinalY - mStartY);
//求x和y平方和的二次方根
float hyp = (float) Math.hypot(dx, dy);
float ndx = dx / hyp;
float ndy = dy / hyp;
//一個(gè)未結(jié)束的滾動(dòng)或者fling在X軸和Y軸上的當(dāng)前的速度
float oldVelocityX = ndx * oldVel;
float oldVelocityY = ndy * oldVel;
//如果一個(gè)未結(jié)束的滾動(dòng)或者fling和這次fling在X軸和Y軸上的速度方向都相同幅狮,則累加在X軸和Y軸上的速度
if (Math.signum(velocityX) == Math.signum(oldVelocityX) &&
Math.signum(velocityY) == Math.signum(oldVelocityY)) {
velocityX += oldVelocityX;
velocityY += oldVelocityY;
}
}
//當(dāng)前模式是fling
mMode = FLING_MODE;
mFinished = false;
//求velocityX和velocityX平方和的二次方根,也就是當(dāng)前速度
float velocity = (float) Math.hypot(velocityX, velocityY);
//保存當(dāng)前速度
mVelocity = velocity;
mDuration = getSplineFlingDuration(velocity);
mStartTime = AnimationUtils.currentAnimationTimeMillis();
mStartX = startX;
mStartY = startY;
float coeffX = velocity == 0 ? 1.0f : velocityX / velocity;
float coeffY = velocity == 0 ? 1.0f : velocityY / velocity;
double totalDistance = getSplineFlingDistance(velocity);
//計(jì)算總共要滾動(dòng)的距離(帶方向)
mDistance = (int) (totalDistance * Math.signum(velocity));
mMinX = minX;
mMaxX = maxX;
mMinY = minY;
mMaxY = maxY;
//最終要到達(dá)的X坐標(biāo)
mFinalX = startX + (int) Math.round(totalDistance * coeffX);
// Pin to mMinX <= mFinalX <= mMaxX
mFinalX = Math.min(mFinalX, mMaxX);
mFinalX = Math.max(mFinalX, mMinX);
//最終要到達(dá)的Y坐標(biāo)
mFinalY = startY + (int) Math.round(totalDistance * coeffY);
// Pin to mMinY <= mFinalY <= mMaxY
mFinalY = Math.min(mFinalY, mMaxY);
mFinalY = Math.max(mFinalY, mMinY);
}
fling方法中主要做了2件事:
如果有一個(gè)未結(jié)束的滾動(dòng)或者fling和這次fling在X軸和Y軸上的速度方向都相同株灸,則累加在X軸和Y軸上的速度崇摄。
計(jì)算出最新的速度
mVelocity
、滾動(dòng)時(shí)間mDuration
慌烧、滾動(dòng)距離mDistance
逐抑、起始坐標(biāo)mStartX
,mStartY
、終點(diǎn)坐標(biāo)mFinalX
屹蚊、mFinalY
厕氨。
下面還是要看computeScroll方法。
public boolean computeScrollOffset() {
if (mFinished) {
return false;
}
int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
//還沒(méi)結(jié)束
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;
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;
}
//根據(jù)時(shí)間的流逝計(jì)算出當(dāng)前速度
mCurrVelocity = velocityCoef * mDistance / mDuration * 1000.0f;
//計(jì)算出當(dāng)前X坐標(biāo)
mCurrX = mStartX + Math.round(distanceCoef * (mFinalX - mStartX));
// Pin to mMinX <= mCurrX <= mMaxX
mCurrX = Math.min(mCurrX, mMaxX);
mCurrX = Math.max(mCurrX, mMinX);
//計(jì)算出當(dāng)前Y坐標(biāo)
mCurrY = mStartY + Math.round(distanceCoef * (mFinalY - mStartY));
// Pin to mMinY <= mCurrY <= mMaxY
mCurrY = Math.min(mCurrY, mMaxY);
mCurrY = Math.max(mCurrY, mMinY);
//已經(jīng)結(jié)束
if (mCurrX == mFinalX && mCurrY == mFinalY) {
mFinished = true;
}
break;
}
}
else {
mCurrX = mFinalX;
mCurrY = mFinalY;
mFinished = true;
}
return true;
}
我們發(fā)現(xiàn)fling和滾動(dòng)的區(qū)別就在于當(dāng)前的坐標(biāo)mCurrX
和mCurrY
的計(jì)算方式而已命斧,其他并沒(méi)有什么區(qū)別。真正的滾動(dòng)還是依賴于我們重寫(xiě)方法來(lái)滾動(dòng)到計(jì)算出來(lái)的mCurrX
和mCurrY
嘱兼。
override fun computeScroll() {
if (scroller.computeScrollOffset()) {
//滾動(dòng)到當(dāng)前坐標(biāo)`mCurrX`和`mCurrY`
scrollTo(scroller.currX, scroller.currY)
invalidate()
}
}
參考鏈接:
*《Android 開(kāi)發(fā)藝術(shù)探索》