編輯:關於Android編程
本文轉載請注明原作者、文章來源,鏈接,版權歸原文作者所有。
本篇為Android Scroll系列文章的最後一篇,主要講解Android視圖繪制機制,由於本系列文章內容都是視圖滾動相關的,所以,本篇從視圖內容滾動的視角來梳理視圖繪制過程。
如果沒有看過本系列之前文章或者不太了解相關的知識,請大家閱讀一下一下的文章:
mScrollY
是如何影響視圖內容。 Android視圖繪制邏輯,包括相關API和Canvas
的相關操作。?為了節約大家的時間,本文內容主要如下:
Scroller
相關機制。 mScrollX
Scroller
使用開始?使用scroller的實例代碼,之後的講解流程就是scroller和computeScroll是如何調用的啦。
?在系列文章的第二篇中,我們具體學習了Scroller
的使用方法。通過Scroller
的fling
和View
的computeScroll
的配合,實現視圖滾動效果。實例代碼如下
.....
mScroller.fling(0,getScrollY(),0,speed,0,0,-500,10000)
invalidate();
.....
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(),
mScroller.getCurrY());
postInvalidate();
}
}
?本篇文章就帶大家探究一下這段代碼背後的原理和機制。
?這一節主要分析在View中調用invalidate
到ViewRoot
執行performTraversals
的原理,對android視圖架構不是很熟悉的同學可以先閱讀一下《Android視圖架構詳解》。
?我們先來看一下View
中的invalidate
代碼。<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxwcmUgY2xhc3M9"brush:java;">
public void invalidate() {
invalidate(true);
}
void invalidate(boolean invalidateCache) {
invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
}
void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
boolean fullInvalidate) {
.....
//DRAWN和HAS_BOUNDS是否被設置為1,說明上一次請求執行的UI繪制已經完成,那麼可以再次請求執行
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)) {
if (fullInvalidate) {
mLastIsOpaque = isOpaque();
mPrivateFlags &= ~PFLAG_DRAWN;
}
mPrivateFlags |= PFLAG_DIRTY;
if (invalidateCache) { //是否讓view的緩存都失效
mPrivateFlags |= PFLAG_INVALIDATED;
mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
}
// Propagate the damage rectangle to the parent view.
final AttachInfo ai = mAttachInfo;
final ViewParent p = mParent;
//通過ViewParent來執行操作,如果當前視圖是頂層視圖也就是DecorView的視圖,那麼它的
//mParent就是ViewRoot對象,所以是通過ViewRoot的對象來實現的。
if (p != null && ai != null && l < r && t < b) {
final Rect damage = ai.mTmpInvalRect;
damage.set(l, t, r, b);
p.invalidateChild(this, damage);//TODO:這是invalidate執行的主體
}
.....
}
}
?我們可以看到,調用invalidate()
會導致整個視圖進行刷新,並且會刷新緩存。
?然後我們再來詳細的研究一下invalidateInternal
中的代碼。我們先來著重看一下if
語句的判斷條件把。
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))
當mPrivateFlags
的FLAG_DRAWN
和FLAG_HAS_BOUNDS
位設置為1時,說明上一次請求執行的UI繪制已經完成,那麼可以再次請求重新繪制。FLAG_DRAWN
位會在draw
函數中會被置為1,而FLAG_HAS_BOUNDS
會在setFrame
函數中被設置為1。 mPrivateFlags
的PFLAG_DRAWING_CACHE_VALID
標示視圖緩存是否有效,如果有效並且invalidateCache
為true,那麼可以請求重新繪制。 另外兩個布爾判斷的具體含義並沒有分析清楚,大家感興趣的請自行研究。mPrivateFlags
的PFLAG_DIRTY
置為1。並且如果是要刷新緩存的話,將PFLAG_INVALIDATED
位設置為1,並且將PFLAG_DRAWING_CACHE_VALID
位設置為0,這一步和之前的if
判斷中後兩個布爾判斷相對應,可見,如果已經有一個invalidate
設置了上述兩個標志位,那麼下一個invalidate
就不會進行任何操作。ViewGroup
和ViewRoot
都實現了上述接口,那麼,根據Android視圖樹狀結構,ViewGroup
的相應方法會被調用。
public final void invalidateChild(View child, final Rect dirty) {
ViewParent parent = this;
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
....
// while一直向上遞歸
do {
......
parent = parent.invalidateChildInParent(location, dirty);
....
} while (parent != null);
}
}
public ViewParent invalidateChildInParent(final int[] location, final Rect dirty) {
if ((mPrivateFlags & PFLAG_DRAWN) == PFLAG_DRAWN ||
(mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID) {
if ((mGroupFlags & (FLAG_OPTIMIZE_INVALIDATE | FLAG_ANIMATION_DONE)) !=
FLAG_OPTIMIZE_INVALIDATE) {
......
return mParent;
} else {
.....
return mParent;
}
}
return null;
}
?通過上述代碼我們可以看到ViewGroup
的invalidateChild
函數通過循環不斷調用其父視圖的invalidateChildInParent
,而且我們知道ViewRoot
是DecorView
的父視圖,也就是說ViewRoot
是Android視圖樹狀結構的根。所以,最終ViewRoot
的invalidateChildInParent
會被調用。
//在ViewGroup的invalidateChildInParent中while循環,一直調用到這裡,然後在調用invalidateChild
public ViewParent invalidateChildInParent(final int[] location, final Rect dirty) {
invalidateChild(null, dirty);
return null;
}
public void invalidateChild(View child, Rect dirty) {
//先檢查線程,必須是主線程
checkThread();
.....
//如果mWillDrawSoon為true那麼就是消息隊列中已經有一個DO_TRAVERSAL的消息啦
if (!mWillDrawSoon) {
//直接調用了這個喽
scheduleTraversals();
}
}
?最終,在ViewRoot
的invalidateChild
函數中,調用了scheduleTraversals
,開啟了視圖重繪之旅。
ViewRoot
騙了?ViewRoot
是Android視圖樹狀結構的根節點,並且它實現了ViewParent
接口,是DecorView
的父視圖。那麼大家一定會認為它就是一個View
吧。那我們就被它給騙了!!ViewRoot
本質上是一個Handler
,我們可以看一下scheduleTraversals
到performTraversals
的原理就知道了。
public void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
sendEmptyMessage(DO_TRAVERSAL);
}
}
?在scheduleTraversals
中,ViewRoot
只是向自己發送了一個DO_TRAVERSAL
的空信息。
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
....
case DO_TRAVERSAL:
//這裡就是Handle處理travel信息的地方
if (mProfile) {
Debug.startMethodTracing("ViewRoot");
}
performTraversals();
if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
break;
.....
}
}
?然後我們在查看handleMessage
方法,發現在處理DO_TRAVERSAL
時,ViewRoot
調用了performTraversals
函數。
?在performTraversals
中,視圖要進行measure,layout,和draw三大步驟,篇幅有限,我們這裡只研究繪制相關的機制。
?ViewRoot
在performTraversals
中調用了自身的draw
方法,看吧,ViewRoot
偽裝的還挺像,連draw
方法都有。但是我們會發現,在draw
方法中,ViewRoot
實際上只調用了自己的mView
成員變量的draw
方法,而且我們都知道的是,mView
就是DecorView
,於是,繪制流程來到了真正的View視圖的根節點。
?接下來,我們就正式研究一下Android的繪制機制,我們沿著Android視圖的樹狀結構來分析繪制原理。
?首先是DecorView
的繪制相關的函數。在ViewRoot
的draw
方法中,直接調用了DecorView
的draw(Canvas canvas)
函數,我們知道DecorView
是FrameLayout
的子類,其draw(Canvas canvas)
函數是從View
中繼承而來的。所以我們先來看View
的draw(Canvas canvas)
方法。
// 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
的dispatchDraw
函數被調用了,它是向子視圖分發繪制指令和相關數據的方法。在View
中,上述函數是一個空函數,但是ViewGroup
中對這個函數進行了實現。
protected void dispatchDraw(Canvas canvas) {
....
final ArrayList preorderedList = usingRenderNodeProperties
? null : buildOrderedChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
for (int i = 0; i < childrenCount; i++) {
int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i;
final View child = (preorderedList == null)
? children[childIndex] : preorderedList.get(childIndex);
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
//在這裡drawChild
more |= drawChild(canvas, child, drawingTime);
}
}
....
}
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
//這裡就調用child的draw方法啦,而不是draw(canvas)方法!!!!!
return child.draw(canvas, this, drawingTime);
}
?通過上述代碼我們可以看到,ViewGroup
分別調用了自己的子View的draw
方法,需要特別注意的是,這個draw和之前draw方法不是同一個方法,他們的參數不同。於是,我們再次轉到View
的源碼中,看一下這個draw方法到底做了什麼。
boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
....
//進行計算滾動
if (!hasDisplayList) {
computeScroll();
sx = mScrollX;
sy = mScrollY;
}
...
//這裡進行了平移。
if (offsetForScroll) {
canvas.translate(mLeft - sx, mTop - sy);
}
.....
if (!layerRendered) {
if (!hasDisplayList) {
// Fast path for layouts with no backgrounds
if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
mPrivateFlags &= ~PFLAG_DIRTY_MASK;
dispatchDraw(canvas);
} else {
// 在這裡調用了draw
draw(canvas);
}
}
......
}
......
}
?首先,我們發現computeScroll
方法是在其中被調用的,從而計算出新的mScrollX
和mScrollY
,然後在平移畫布,產生內容平移效果。
?然後我們發現通過PFLAG_SKIP_DRAW
標志位的判斷,有些View是直接調用dispatchDraw
函數,說明它自己沒有需要繪制的內容,而有些View則是調用自己的draw
方法。我們應該都知道ViewGroup
默認是不進行繪制內容的吧,我們一般調用setNotWillDraw
方法來讓其可以繪制自身內容,通過調用setNotWillDraw
方法,會導致PFLAG_SKIP_DRAW
位被置為1,從而可以繪制自身內容。
?分析到這裡,我們就會發現draw函數沿著Android視圖樹狀結構被不斷調用,知道所有視圖都完成繪制。
?讀到這裡大家應該對Android視圖繪制流程有了基本的了解了吧,那麼,我們再來看一下文章開頭的例子。在computeScroll
方法中,我們調用了postInvalidate
方法,這又是什麼用意呢?
?其實,在computeScroll
中不掉用postInvalidate
好像也可以達到正確的效果,具體原因我不太了解,猜測應該是Android自動刷新界面可以代替postInvalidate
的效果吧。同學們如果知道其中具體原因,請告知我啊。
?在《Android Scroll詳解(一):基礎知識》中,我們已經講到
postInvalidate
其實就是調用了invalidate
,然後整個流程就連接了起來,mScrollX
和mScrollY
每個循環都會改變一點,然後導致界面滾動,最終形成界面Scroll效果。
?Android Scroll的系列文章就此結束了,希望大家從中學習到有用的知識。如果其中有任何錯誤或者容易誤解的地方,請大家及時通知我。謝謝各位讀者和同學。
如果listitem裡面包括button或者checkbox等控件,默認情況下listitem會失去焦點,導致無法響應item的事件,最常用的解決辦法是在listitem
react-native-easy-toast一款簡單易用的 Toast 組件,支持 Android&iOS。安裝1.在終端運行 npm i react-nati
Android開發中遇到要從相冊選擇圖片時,大多數人都會選擇調用Android自帶的相冊,畢竟這樣可以節約時間,又不用自己去處理圖片的問題,不過這樣也會產生一些問題,有些
Android mvp 架構的自述中我簡單的介紹了mvp,以及怎麼寫mvp。我自己也將mvp運用到了項目中,其實mvp並沒有固定的寫法,正確的去理解架構的思想,都可以有自