原作者: ztelur
本文僅供個人學(xué)習(xí),不用于任何形式商業(yè)目的,轉(zhuǎn)載請注明原作者、文章來源、翻譯作者及簡書鏈接,版權(quán)歸原文作者所有。
在前邊的文章中,我們已經(jīng)對Android觸摸事件處理有了大致的了解,并且詳細探討了MotionEvent的相關(guān)用法。對之前文章中的知識還不是很了解的同學(xué),請閱讀《Android MotionEvent詳解》
?今天,我們就來探討一下Android中界面滾動效果的相關(guān)機制,本篇文章主要講解一下滾動相關(guān)的知識點,之后的文章會涉及實際的代碼和原理。希望大家閱讀完這篇文章之后,能夠了解或者掌握一下知識:
- Android 視圖的組成部分
-
mScrollX和mScrollY對視圖顯示的影響 -
scrollTo和scrollBy的使用 -
invalidate和postInvalidate的區(qū)別
View的mScrollX和mScrollY
我們都知道,View中有兩個重要的成員變量,mScrollX,mScrollY.它們分別代表視圖內(nèi)容(view content)水平方向和豎直方向的滾動距離。我們可以通過setScrollX和setScrollY來個函數(shù)來改變它們的值,從而來滾動視圖的內(nèi)容。
?在這里需要強調(diào)的是,mScrollX和mScrollY會導(dǎo)致視圖內(nèi)容(view content)變化,但是不會影響視圖背景(background)。
?看到這里同學(xué)們或許會有寫疑問,視圖的內(nèi)容和背景有什么區(qū)別呢?視圖還有哪些組成部分呢?
?我們可以從View的draw方法中得知View的組成部分。
// http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/5.1.1_r1/android/view/View.java#View
public void draw(Canvas canvas) {
........
/*
* 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)
*/
// Step 1, draw the background, if needed
if (!dirtyOpaque) {
drawBackground(canvas);
}
.......
// Step 2, save the canvas' layers
.......
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
// Step 5, draw the fade effect and restore layers
.......
if (drawTop) {
matrix.setScale(1, fadeHeight * topFadeStrength);
matrix.postTranslate(left, top);
fade.setLocalMatrix(matrix);
p.setShader(fade);
canvas.drawRect(left, top, right, top + length, p);
}
.....
// Step 6, draw decorations (scrollbars)
onDrawScrollBars(canvas);
......
}
View顯示內(nèi)容由一下幾個部分組成:
- 背景(background)
- 本身的內(nèi)容(content)
- 子視圖
- 邊界漸變效果(fade effect),上下左右四個邊界都可能會有漸變效果,代碼中只顯示了上邊界的漸變效果繪制。
- 邊框或者裝飾效果(decorations),比如滾動條
舉個例子吧,我們都知道在布局文件中,TextView有兩個比較重要的屬性:background,text。background可以設(shè)置TextView的背景,而text則是設(shè)置要繪制字體內(nèi)容。
<TextView
android:layout_width="wrap_content"
android:background="@drawable/ic_launcher"
android:text="Test"
android:layout_height="wrap_content" />
mScrollX和mScrollY對除了本身內(nèi)容外的部分的繪制都有影響。只是不會影響視圖背景的繪制。
滾動的方向性
我們都知道,在Android的視圖中,布局相關(guān)的數(shù)值都是有方向性的,比如mLeft,mTop。

由上圖我們可以知道,Android視圖坐標(biāo)的原點在屏幕的左上方,x軸正方向是向右,y軸正方向是向下。
?所以,當(dāng)你將mLeft和mTop的數(shù)值加10并且重繪視圖時,視圖會向右下移動。
?那么mScrollY和mScrollX也在這樣一個坐標(biāo)域中嗎?它們的正方向和mTop和mLeft是一樣的嗎?是的,它們屬于同一個坐標(biāo)域,方向性相同。
?但是如果你將mScrollX和mScrollY的數(shù)值都增大10,然后調(diào)用invalidate()重新繪制界面的話,你會發(fā)現(xiàn)視圖中的內(nèi)容都向左上角移動啦!
?這是怎么回事呢?從概念上你可以先這樣解:mScrollX和mScrollY改變導(dǎo)致View的可視區(qū)域的移動,并不是導(dǎo)致View的視圖區(qū)域的移動。
?View的視圖區(qū)域相當(dāng)于無限大的,你可以在onDraw函數(shù)中的canvas中繪制任意大的圖像,但是你會發(fā)現(xiàn),最終屏幕上顯示出來的只會是一部分,因為View自身還有大小概念,也就是measure和layout時,視圖會被設(shè)置長寬還有界面中位置,這樣的話,視圖可視區(qū)域就被確定啦。
?做一個形象的比喻。View的可視區(qū)域就是一面墻上的窗戶,View的視圖區(qū)域就相當(dāng)于墻后邊的優(yōu)美景色。墻外風(fēng)光無線,但是你只能看到窗戶中的景色。如果窗戶變大啦,外邊風(fēng)景不變,你看到的景色就大了一點;如果窗戶向右下角移動了一段距離,你就會發(fā)現(xiàn)外邊的景色好像是向左上角"移動"了一段距離。

ScrollTo 和 ScrollBy
這兩個函數(shù)是用來滾動視圖的API
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();
}
}
}
public void scrollBy(int x, int y) {
scrollTo(mScrollX + x, mScrollY + y);
}
大家看源代碼很容易就理解了二者的作用和區(qū)別:scrollTo就是直接改變mScrollX和mScrollY;而scrollBy則是給mScrollX和mScrollY加上增量。
invalidate和postInvalidate
上邊這兩個函數(shù)都是請求視圖重新繪制的API,但是二者的使用有些區(qū)別。
?invalidate必須在主線程(UI Thread)中調(diào)用,而postInvalidate可以在非主線程(Non UI Thread)中調(diào)用。
?除此之外,二者還有點小區(qū)別。
?調(diào)用invalidate時,它會檢查上一次請求的UI重繪是否完成,如果沒有完成的話,那么它就什么都不做。
void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
boolean fullInvalidate) {
.....
//DRAWN和HAS_BOUNDS是否被設(shè)置為1,說明上一次請求執(zhí)行的UI繪制已經(jīng)完成,那么可以再次請求執(zhí)行
if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)) == (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)
|| (invalidateCache && (mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID)
|| (mPrivateFlags & PFLAG_INVALIDATED) != PFLAG_INVALIDATED
|| (fullInvalidate && isOpaque() != mLastIsOpaque)) {
......
final AttachInfo ai = mAttachInfo;
final ViewParent p = mParent;
final Rect damage = ai.mTmpInvalRect;
damage.set(l, t, r, b);
p.invalidateChild(this, damage);//TODO:這是invalidate執(zhí)行的主體
.....
}
}
而postInvalidate則不會這樣,它是向主線程發(fā)送個Message,然后handleMessage時,調(diào)用了invalidate()函數(shù)。
//View.java
public void postInvalidateDelayed(long delayMilliseconds) {
... attachInfo.mViewRootImpl.dispatchInvalidateDelayed(this, delayMilliseconds);
...
}
// ViewRootImpl 發(fā)送Message
public void dispatchInvalidateDelayed(View view, long delayMilliseconds) {
Message msg = mHandler.obtainMessage(MSG_INVALIDATE, view);
mHandler.sendMessageDelayed(msg, delayMilliseconds);
}
// ViewRootImpl 處理Message
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_INVALIDATE:
((View) msg.obj).invalidate();
break;
}
}
所以,二者的調(diào)用時機還是有區(qū)別的,就比如使用Scroller進行視圖滾動時,二者的調(diào)用就有所不同。
后續(xù)
之后還有會兩篇博文,一篇是《Android Scroll詳解(二):OverScroller實戰(zhàn)》講解具體代碼實現(xiàn),另外一篇是《Android Scroll詳解(三):Android 繪制過程詳解》主要是從滾動角度理解Android繪制過程,請大家多多關(guān)注啊。
參考文章