Android Scroll詳解(三):Android 繪制過程詳解

作者: 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ī)制。
  • mScrollXmScrollY是如何影響視圖內(nèi)容牡昆。
  • Android視圖繪制邏輯姚炕,包括相關(guān)API和Canvas的相關(guān)操作。

一切從Scroller使用開始

使用scroller的實(shí)例代碼丢烘,之后的講解流程就是scroller和computeScroll是如何調(diào)用的啦柱宦。
?在系列文章的第二篇中,我們具體學(xué)習(xí)了Scroller的使用方法播瞳。通過ScrollerflingViewcomputeScroll的配合掸刊,實(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)用invalidateViewRoot執(zhí)行performTraversals的原理忧侧,對android視圖架構(gòu)不是很熟悉的同學(xué)可以先閱讀一下《Android視圖架構(gòu)詳解》

invalidate時序圖

我們先來看一下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)mPrivateFlagsFLAG_DRAWNFLAG_HAS_BOUNDS位設(shè)置為1時,說明上一次請求執(zhí)行的UI繪制已經(jīng)完成碟贾,那么可以再次請求重新繪制币喧。FLAG_DRAWN位會在draw函數(shù)中會被置為1,而FLAG_HAS_BOUNDS會在setFrame函數(shù)中被設(shè)置為1袱耽。
  • mPrivateFlagsPFLAG_DRAWING_CACHE_VALID標(biāo)示視圖緩存是否有效杀餐,如果有效并且invalidateCache為true,那么可以請求重新繪制。
  • 另外兩個布爾判斷的具體含義并沒有分析清楚朱巨,大家感興趣的請自行研究史翘。

然后將mPrivateFlagsPFLAG_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)知道ViewGroupViewRoot都實(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;
}

通過上述代碼我們可以看到ViewGroupinvalidateChild函數(shù)通過循環(huán)不斷調(diào)用其父視圖的invalidateChildInParent,而且我們知道ViewRootDecorView的父視圖粗仓,也就是說ViewRoot是Android視圖樹狀結(jié)構(gòu)的根嫁怀。所以,最終ViewRootinvalidateChildInParent會被調(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();
    }
}

最終蚂斤,在ViewRootinvalidateChild函數(shù)中存捺,調(diào)用了scheduleTraversals,開啟了視圖重繪之旅。

我們都被ViewRoot騙了。

ViewRoot是Android視圖樹狀結(jié)構(gòu)的根節(jié)點(diǎn)捌治,并且它實(shí)現(xiàn)了ViewParent接口岗钩,是DecorView的父視圖。那么大家一定會認(rèn)為它就是一個View吧肖油。那我們就被它給騙了<嫦拧!ViewRoot本質(zhì)上是一個Handler森枪,我們可以看一下scheduleTraversalsperformTraversals的原理就知道了视搏。

    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ī)制宛畦。
?ViewRootperformTraversals中調(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)來分析繪制原理肾胯。

draw時序圖

?首先是DecorView的繪制相關(guān)的函數(shù)竖席。在ViewRootdraw方法中,直接調(diào)用了DecorViewdraw(Canvas canvas)函數(shù)敬肚,我們知道DecorViewFrameLayout的子類毕荐,其draw(Canvas canvas)函數(shù)是從View中繼承而來的。所以我們先來看Viewdraw(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é)自行查閱文章或者其他資料。通過上述代碼我們可以看到虽填,ViewdispatchDraw函數(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)用的姑躲,從而計算出新的mScrollXmScrollY,然后在平移畫布,產(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,然后整個流程就連接了起來诵闭,mScrollXmScrollY每個循環(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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末踩衩,一起剝皮案震驚了整個濱河市嚼鹉,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌驱富,老刑警劉巖锚赤,帶你破解...
    沈念sama閱讀 211,123評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異褐鸥,居然都是意外死亡线脚,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,031評論 2 384
  • 文/潘曉璐 我一進(jìn)店門叫榕,熙熙樓的掌柜王于貴愁眉苦臉地迎上來浑侥,“玉大人,你說我怎么就攤上這事晰绎≡⒙洌” “怎么了?”我有些...
    開封第一講書人閱讀 156,723評論 0 345
  • 文/不壞的土叔 我叫張陵荞下,是天一觀的道長伶选。 經(jīng)常有香客問我史飞,道長,這世上最難降的妖魔是什么仰税? 我笑而不...
    開封第一講書人閱讀 56,357評論 1 283
  • 正文 為了忘掉前任构资,我火速辦了婚禮,結(jié)果婚禮上陨簇,老公的妹妹穿的比我還像新娘吐绵。我一直安慰自己,他們只是感情好塞帐,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,412評論 5 384
  • 文/花漫 我一把揭開白布拦赠。 她就那樣靜靜地躺著,像睡著了一般葵姥。 火紅的嫁衣襯著肌膚如雪荷鼠。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,760評論 1 289
  • 那天榔幸,我揣著相機(jī)與錄音允乐,去河邊找鬼。 笑死削咆,一個胖子當(dāng)著我的面吹牛牍疏,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播拨齐,決...
    沈念sama閱讀 38,904評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼鳞陨,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了瞻惋?” 一聲冷哼從身側(cè)響起厦滤,我...
    開封第一講書人閱讀 37,672評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎歼狼,沒想到半個月后掏导,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,118評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡羽峰,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,456評論 2 325
  • 正文 我和宋清朗相戀三年趟咆,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片梅屉。...
    茶點(diǎn)故事閱讀 38,599評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡值纱,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出履植,到底是詐尸還是另有隱情计雌,我是刑警寧澤,帶...
    沈念sama閱讀 34,264評論 4 328
  • 正文 年R本政府宣布玫霎,位于F島的核電站凿滤,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏庶近。R本人自食惡果不足惜翁脆,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,857評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望鼻种。 院中可真熱鬧反番,春花似錦、人聲如沸叉钥。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,731評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽投队。三九已至枫疆,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間敷鸦,已是汗流浹背息楔。 一陣腳步聲響...
    開封第一講書人閱讀 31,956評論 1 264
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留扒披,地道東北人值依。 一個月前我還...
    沈念sama閱讀 46,286評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像碟案,于是被迫代替她去往敵國和親愿险。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,465評論 2 348

推薦閱讀更多精彩內(nèi)容