作者: ztelur
聯(lián)系方式:segmentfault湿刽,csdn兽泣,github
本文轉(zhuǎn)載請注明原作者怀读、文章來源诉位,鏈接,版權(quán)歸原文作者所有菜枷。
本篇為Android Scroll系列文章的最后一篇苍糠,主要講解Android視圖繪制機(jī)制,由于本系列文章內(nèi)容都是視圖滾動相關(guān)的啤誊,所以岳瞭,本篇從視圖內(nèi)容滾動的視角來梳理視圖繪制過程。
?如果沒有看過本系列之前文章或者不太了解相關(guān)的知識蚊锹,請大家閱讀一下一下的文章:
為了節(jié)約大家的時間瞳筏,本文內(nèi)容主要如下:
-
Scroller
相關(guān)機(jī)制。 -
mScrollX
和mScrollY
是如何影響視圖內(nèi)容牡昆。 - Android視圖繪制邏輯姚炕,包括相關(guān)API和
Canvas
的相關(guān)操作。
一切從Scroller
使用開始
使用scroller的實(shí)例代碼丢烘,之后的講解流程就是scroller和computeScroll是如何調(diào)用的啦柱宦。
?在系列文章的第二篇中,我們具體學(xué)習(xí)了Scroller
的使用方法播瞳。通過Scroller
的fling
和View
的computeScroll
的配合掸刊,實(shí)現(xiàn)視圖滾動效果。實(shí)例代碼如下
.....
mScroller.fling(0,getScrollY(),0,speed,0,0,-500,10000)
invalidate();
.....
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(),
mScroller.getCurrY());
postInvalidate();
}
}
本篇文章就帶大家探究一下這段代碼背后的原理和機(jī)制赢乓。
Invalidate的尋父之路
這一節(jié)主要分析在View中調(diào)用invalidate
到ViewRoot
執(zhí)行performTraversals
的原理忧侧,對android視圖架構(gòu)不是很熟悉的同學(xué)可以先閱讀一下《Android視圖架構(gòu)詳解》。
我們先來看一下View
中的invalidate
代碼骏全。
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是否被設(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)) {
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來執(zhí)行操作姜贡,如果當(dāng)前視圖是頂層視圖也就是DecorView的視圖试吁,那么它的
//mParent就是ViewRoot對象,所以是通過ViewRoot的對象來實(shí)現(xiàn)的。
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執(zhí)行的主體
}
.....
}
}
我們可以看到熄捍,調(diào)用invalidate()
會導(dǎo)致整個視圖進(jìn)行刷新烛恤,并且會刷新緩存。
?然后我們再來詳細(xì)的研究一下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))
- 當(dāng)
mPrivateFlags
的FLAG_DRAWN
和FLAG_HAS_BOUNDS
位設(shè)置為1時,說明上一次請求執(zhí)行的UI繪制已經(jīng)完成碟贾,那么可以再次請求重新繪制币喧。FLAG_DRAWN
位會在draw
函數(shù)中會被置為1,而FLAG_HAS_BOUNDS
會在setFrame
函數(shù)中被設(shè)置為1袱耽。 -
mPrivateFlags
的PFLAG_DRAWING_CACHE_VALID
標(biāo)示視圖緩存是否有效杀餐,如果有效并且invalidateCache
為true,那么可以請求重新繪制。 - 另外兩個布爾判斷的具體含義并沒有分析清楚朱巨,大家感興趣的請自行研究史翘。
然后將mPrivateFlags
的PFLAG_DIRTY
置為1。并且如果是要刷新緩存的話冀续,將PFLAG_INVALIDATED
位設(shè)置為1琼讽,并且將PFLAG_DRAWING_CACHE_VALID
位設(shè)置為0,這一步和之前的if
判斷中后兩個布爾判斷相對應(yīng),可見洪唐,如果已經(jīng)有一個invalidate
設(shè)置了上述兩個標(biāo)志位钻蹬,那么下一個invalidate
就不會進(jìn)行任何操作。
?接著桐罕,調(diào)用ViewParent接口的invalidateChild函數(shù)脉让,在《Android視圖架構(gòu)詳解》桂敛,我們已經(jīng)知道ViewGroup
和ViewRoot
都實(shí)現(xiàn)了上述接口功炮,那么,根據(jù)Android視圖樹狀結(jié)構(gòu)术唬,ViewGroup
的相應(yīng)方法會被調(diào)用薪伏。
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
函數(shù)通過循環(huán)不斷調(diào)用其父視圖的invalidateChildInParent
,而且我們知道ViewRoot
是DecorView
的父視圖粗仓,也就是說ViewRoot
是Android視圖樹狀結(jié)構(gòu)的根嫁怀。所以,最終ViewRoot
的invalidateChildInParent
會被調(diào)用借浊。
//在ViewGroup的invalidateChildInParent中while循環(huán)塘淑,一直調(diào)用到這里,然后在調(diào)用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那么就是消息隊(duì)列中已經(jīng)有一個DO_TRAVERSAL的消息啦
if (!mWillDrawSoon) {
//直接調(diào)用了這個嘍
scheduleTraversals();
}
}
最終蚂斤,在ViewRoot
的invalidateChild
函數(shù)中存捺,調(diào)用了scheduleTraversals
,開啟了視圖重繪之旅。
我們都被ViewRoot騙了。
ViewRoot
是Android視圖樹狀結(jié)構(gòu)的根節(jié)點(diǎn)捌治,并且它實(shí)現(xiàn)了ViewParent
接口岗钩,是DecorView
的父視圖。那么大家一定會認(rèn)為它就是一個View
吧肖油。那我們就被它給騙了<嫦拧!ViewRoot
本質(zhì)上是一個Handler
森枪,我們可以看一下scheduleTraversals
到performTraversals
的原理就知道了视搏。
public void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
sendEmptyMessage(DO_TRAVERSAL);
}
}
在scheduleTraversals
中,ViewRoot
只是向自己發(fā)送了一個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
方法凶朗,發(fā)現(xiàn)在處理DO_TRAVERSAL
時,ViewRoot
調(diào)用了performTraversals
函數(shù)显拳。
?在performTraversals
中棚愤,視圖要進(jìn)行measure,layout,和draw三大步驟,篇幅有限杂数,我們這里只研究繪制相關(guān)的機(jī)制宛畦。
?ViewRoot
在performTraversals
中調(diào)用了自身的draw
方法,看吧揍移,ViewRoot
偽裝的還挺像次和,連draw
方法都有。但是我們會發(fā)現(xiàn)那伐,在draw
方法中踏施,ViewRoot
實(shí)際上只調(diào)用了自己的mView
成員變量的draw
方法,而且我們都知道的是罕邀,mView
就是DecorView
畅形,于是,繪制流程來到了真正的View視圖的根節(jié)點(diǎn)诉探。
大家都來畫的canvas
接下來日熬,我們就正式研究一下Android的繪制機(jī)制,我們沿著Android視圖的樹狀結(jié)構(gòu)來分析繪制原理肾胯。
?首先是
DecorView
的繪制相關(guān)的函數(shù)竖席。在ViewRoot
的draw
方法中,直接調(diào)用了DecorView
的draw(Canvas canvas)
函數(shù)敬肚,我們知道DecorView
是FrameLayout
的子類毕荐,其draw(Canvas canvas)
函數(shù)是從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);
......
}
關(guān)于視圖的組成部分憎亚,我在之前的文章中已經(jīng)講述過來,請不太熟悉這部分內(nèi)容的同學(xué)自行查閱文章或者其他資料。通過上述代碼我們可以看到虽填,View
的dispatchDraw
函數(shù)被調(diào)用了丁恭,它是向子視圖分發(fā)繪制指令和相關(guān)數(shù)據(jù)的方法。在View
中斋日,上述函數(shù)是一個空函數(shù)牲览,但是ViewGroup
中對這個函數(shù)進(jìn)行了實(shí)現(xiàn)。
protected void dispatchDraw(Canvas canvas) {
....
final ArrayList<View> 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) {
//這里就調(diào)用child的draw方法啦恶守,而不是draw(canvas)方法5谙住!M酶邸S购痢!
return child.draw(canvas, this, drawingTime);
}
通過上述代碼我們可以看到衫樊,ViewGroup
分別調(diào)用了自己的子View的draw
方法飒赃,需要特別注意的是,這個draw和之前draw方法不是同一個方法科侈,他們的參數(shù)不同载佳。于是,我們再次轉(zhuǎn)到View
的源碼中臀栈,看一下這個draw方法到底做了什么蔫慧。
boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
....
//進(jìn)行計算滾動
if (!hasDisplayList) {
computeScroll();
sx = mScrollX;
sy = mScrollY;
}
...
//這里進(jìn)行了平移。
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 {
// 在這里調(diào)用了draw
draw(canvas);
}
}
......
}
......
}
首先权薯,我們發(fā)現(xiàn)computeScroll
方法是在其中被調(diào)用的姑躲,從而計算出新的mScrollX
和mScrollY
,然后在平移畫布,產(chǎn)生內(nèi)容平移效果盟蚣。
?然后我們發(fā)現(xiàn)通過PFLAG_SKIP_DRAW
標(biāo)志位的判斷黍析,有些View是直接調(diào)用dispatchDraw
函數(shù),說明它自己沒有需要繪制的內(nèi)容刁俭,而有些View則是調(diào)用自己的draw
方法橄仍。我們應(yīng)該都知道ViewGroup
默認(rèn)是不進(jìn)行繪制內(nèi)容的吧,我們一般調(diào)用setNotWillDraw
方法來讓其可以繪制自身內(nèi)容牍戚,通過調(diào)用setNotWillDraw
方法,會導(dǎo)致PFLAG_SKIP_DRAW
位被置為1虑粥,從而可以繪制自身內(nèi)容如孝。
?分析到這里,我們就會發(fā)現(xiàn)draw函數(shù)沿著Android視圖樹狀結(jié)構(gòu)被不斷調(diào)用娩贷,知道所有視圖都完成繪制第晰。
把一切連接起來的computeScroll
讀到這里大家應(yīng)該對Android視圖繪制流程有了基本的了解了吧,那么,我們再來看一下文章開頭的例子茁瘦。在computeScroll
方法中品抽,我們調(diào)用了postInvalidate
方法,這又是什么用意呢甜熔?
?其實(shí)圆恤,在computeScroll
中不掉用postInvalidate
好像也可以達(dá)到正確的效果,具體原因我不太了解腔稀,猜測應(yīng)該是Android自動刷新界面可以代替postInvalidate
的效果吧盆昙。同學(xué)們?nèi)绻榔渲芯唧w原因,請告知我啊焊虏。
?在《Android Scroll詳解(一):基礎(chǔ)知識》中淡喜,我們已經(jīng)講到
postInvalidate
其實(shí)就是調(diào)用了invalidate
,然后整個流程就連接了起來诵闭,mScrollX
和mScrollY
每個循環(huán)都會改變一點(diǎn)炼团,然后導(dǎo)致界面滾動,最終形成界面Scroll效果疏尿。
后記
Android Scroll的系列文章就此結(jié)束了们镜,希望大家從中學(xué)習(xí)到有用的知識。如果其中有任何錯誤或者容易誤解的地方润歉,請大家及時通知我模狭。謝謝各位讀者和同學(xué)。
http://www.cppblog.com/fwxjj/archive/2013/01/13/197231.html
http://blog.csdn.net/luoshengyang/article/details/8372924