Draw過程

一、調(diào)用流程

1. ViewRootImpl # performTraversals()

performTraversals()調(diào)用了performDraw(),三大流程的起點都是performTraversals()。

// 沒有取消draw也沒有創(chuàng)建新的平面 第一次traversals時newSurface為true
if (!cancelDraw && !newSurface) {
    // ...
    // 開始draw流程
    performDraw();
} else {
    if (isViewVisible) {
        // Try again
        // 如果是可見的, 就再調(diào)用一次traversals
        scheduleTraversals();
        // ...
    }
    // ...
}

2. ViewRootImpl # performDraw()

調(diào)用到ViewRootImpl的draw()。

private void performDraw() {
    // ...
    // mFullRedrawNeeded表示是否需要完全重繪
    final boolean fullRedrawNeeded = mFullRedrawNeeded;
    mFullRedrawNeeded = false;
    mIsDrawing = true;
    Trace.traceBegin(Trace.TRACE_TAG_VIEW, "draw");
    try {
        // 調(diào)用draw()
        draw(fullRedrawNeeded);
    } finally {
        mIsDrawing = false;
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
    // ...
}

3. ViewRootImpl # draw()

這個方法非常長,主要是去處理繪制區(qū)域、坐標等準備工作,之后調(diào)用drawSoftware()。

private void draw(boolean fullRedrawNeeded) {
    // ...
    // 獲取需要繪制的區(qū)域
    final Rect dirty = mDirty;
    // ...
    // 判斷是否需要完全繪制
    if (fullRedrawNeeded) {
        mAttachInfo.mIgnoreDirtyState = true;
        // 如果需要就將區(qū)域設(shè)置為屏幕的所有區(qū)域
        dirty.set(0, 0, (int) (mWidth * appScale + 0.5f), (int) (mHeight * appScale + 0.5f));
    }
    // ...
    if (!dirty.isEmpty() || mIsAnimating || accessibilityFocusDirty) {
        if (mAttachInfo.mThreadedRenderer != null && mAttachInfo.mThreadedRenderer.isEnabled()) {
            // ...
        } else {
            // ...
            // 調(diào)用drawSoftware()繪制
            if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
                return;
            }
        }
    }
    // ...
}

4. ViewRootImpl # drawSoftware()

  • 初始canvas對象
  • 調(diào)用DecorView的draw()
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
        boolean scalingRequired, Rect dirty) {
    final Canvas canvas;
    try {
        final int left = dirty.left;
        final int top = dirty.top;
        final int right = dirty.right;
        final int bottom = dirty.bottom;
        
        // 創(chuàng)建一個繪制區(qū)域的canvas對象
        canvas = mSurface.lockCanvas(dirty);
        
        // 判斷l(xiāng)ockCanvas有沒有改變dirty的頂點值
        if (left != dirty.left || top != dirty.top || right != dirty.right
                || bottom != dirty.bottom) {
            attachInfo.mIgnoreDirtyState = true;
        }
        // 設(shè)置畫布密度
        canvas.setDensity(mDensity);
    } catch (Surface.OutOfResourcesException e) {
       // ...
    }
    try {
        // ...
        // 先清空畫布
        if (!canvas.isOpaque() || yoff != 0 || xoff != 0) {
            canvas.drawColor(0, PorterDuff.Mode.CLEAR);
        }
        dirty.setEmpty();
        mIsAnimating = false;
        mView.mPrivateFlags |= View.PFLAG_DRAWN;
        
        try {
            // 設(shè)置畫布偏移值
            canvas.translate(-xoff, -yoff);
            if (mTranslator != null) {
                mTranslator.translateCanvas(canvas);
            }
            canvas.setScreenDensity(scalingRequired ? mNoncompatDensity : 0);
            attachInfo.mSetIgnoreDirtyState = false;
            // 調(diào)用draw() 從DecorView開始繪制流程
            mView.draw(canvas);
            // ...
        }
        // ...
    } 
    // ...
    return true;
}

5. DecorView # draw()

  • 先調(diào)用父類的draw()去繪制。
  • 如果有菜單背景的drawable的畫就畫上。
@Override
public void draw(Canvas canvas) {
    super.draw(canvas);
    if (mMenuBackground != null) {
        mMenuBackground.draw(canvas);
    }
}

6. View # draw()

FrameLayout和ViewGroup都沒有重寫draw(),所以就調(diào)用到了View的draw()。View的draw()也很長,但是注釋寫的分步很清楚。

/*
 * 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)
 */

繪制背景、繪制自身、繪制子View、繪制陰影、繪制其它的裝飾。到這里就調(diào)用到Draw向子View分發(fā)的過程了。下面就仔細的看看這個方法。

二、View的draw()的實現(xiàn)

根據(jù)注釋可以將draw()分成六個階段。

  • 繪制背景
  • 存儲圖層
  • 繪制自身
  • 繪制子View
  • 繪制陰影并恢復(fù)圖層
  • 繪制滾動條等效果

1. 繪制背景

View # draw()

如果不為透明,就調(diào)用drawBackground()去繪制背景。

final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
        (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);

// 判斷是否為透明
if (!dirtyOpaque) {
    // 調(diào)用drawBackgroud()去繪制背景
    drawBackground(canvas);
}
View # drawBackground()
  • 設(shè)置背景圖片邊界
  • 判斷View是否有移動
    • N:直接繪制背景圖片
    • Y:移動到View的偏移位置,繪制背景,再移動回來。
private void drawBackground(Canvas canvas) {
    final Drawable background = mBackground;
    if (background == null) {
        return;
    }
    // 設(shè)置背景圖片的邊界位置
    setBackgroundBounds();
    
    // 硬件加速相關(guān)......
    
    final int scrollX = mScrollX;
    final int scrollY = mScrollY;
    // 判斷View是否滑動
    if ((scrollX | scrollY) == 0) {
        // 沒有滑動就繪制背景圖片
        background.draw(canvas);
    } else {
        // 如果移動了,就先移動canvas,繪制背景,canvas再移回
        canvas.translate(scrollX, scrollY);
        background.draw(canvas);
        canvas.translate(-scrollX, -scrollY);
    }
}
View # setBackgroundBounds()

調(diào)用drawable的setBounds()設(shè)置邊界,傳參是由layout過程中生成的頂點值組合的,就相當于是View的頂點。

void setBackgroundBounds() {
    if (mBackgroundSizeChanged && mBackground != null) {
        // 設(shè)置背景圖的四個坐標,這四個值就是View的四個頂點值
        mBackground.setBounds(0, 0, mRight - mLeft, mBottom - mTop);
        mBackgroundSizeChanged = false;
        rebuildOutline();
    }
}

2. 繪制自身

View # draw()
  • 如果不需要繪制陰影,就直接進入繪制自身的步驟。
  • 如果View不是透明的,就調(diào)用onDraw()去繪制自身。
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
// 如果不需要去繪制陰影,就可以跳過存儲圖層和繪制陰影的步驟
if (!verticalEdges && !horizontalEdges) {
    // Step 3, draw the content
    // 繪制自身
    if (!dirtyOpaque) onDraw(canvas);
    // ...
}
View # onDraw()

空實現(xiàn),沒有做統(tǒng)一實現(xiàn),自定義View時需要自己去實現(xiàn)。

protected void onDraw(Canvas canvas) {
}

3. 繪制子View分發(fā)過程

View # draw()

在繪制自己完成后就去分發(fā)調(diào)用子View的繪制過程。

if (!verticalEdges && !horizontalEdges) {
    // ...
    dispatchDraw(canvas);
    // ...
}
ViewGroup # dispatchDraw()

在View中是空實現(xiàn),所以來看ViewGroup中的實現(xiàn)。會遍歷所有的子View,如果子View可見或是存在動畫,就調(diào)用drawChld()。

@Override
protected void dispatchDraw(Canvas canvas) {
    boolean usingRenderNodeProperties = canvas.isRecordingFor(mRenderNode);
    final int childrenCount = mChildrenCount;
    final View[] children = mChildren;
    int flags = mGroupFlags;
    // ...
    // 遍歷子View
    for (int i = 0; i < childrenCount; i++) {
        // ...
        final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
        final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
        if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
            // 調(diào)用drawChild去傳遞
            more |= drawChild(canvas, child, drawingTime);
        }
    }
    // ...
}
ViewGroup # drawChild()

直接調(diào)用了子View的draw(),實現(xiàn)了傳遞。

protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
    return child.draw(canvas, this, drawingTime);
}
View # draw()

這個draw()有三個參數(shù),和之前分析的一個參數(shù)的draw()不同,注釋上說得很清楚,這個draw()是ViewGroup.drawChild()調(diào)用去繪制子View的。和一個參數(shù)最大不同是,這個draw()判斷了是繪制緩存內(nèi)容還是去調(diào)用一個參數(shù)的draw()繪制。

boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
    // ...
    if (!drawingWithDrawingCache) {
        // 如果不使用緩存
        if (drawingWithRenderNode) {
            // 硬件緩存
            mPrivateFlags &= ~PFLAG_DIRTY_MASK;
            ((DisplayListCanvas) canvas).drawRenderNode(renderNode);
        } else {
            // Fast path for layouts with no backgrounds
            if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
                mPrivateFlags &= ~PFLAG_DIRTY_MASK;
                // 如果需要跳過,就直接去調(diào)用分發(fā)給這個View的子View的方法
                dispatchDraw(canvas);
            } else {
                // 調(diào)用普通的draw()
                draw(canvas);
            }
        }
    } else if (cache != null) {
        // 如果使用緩存并且緩存不為空
        mPrivateFlags &= ~PFLAG_DIRTY_MASK;
        if (layerType == LAYER_TYPE_NONE || mLayerPaint == null) {
            // no layer paint, use temporary paint to draw bitmap
            Paint cachePaint = parent.mCachePaint;
            if (cachePaint == null) {
                cachePaint = new Paint();
                cachePaint.setDither(false);
                parent.mCachePaint = cachePaint;
            }
            cachePaint.setAlpha((int) (alpha * 255));
            // 繪制緩存的內(nèi)容
            canvas.drawBitmap(cache, 0.0f, 0.0f, cachePaint);
        } else {
            // use layer paint to draw the bitmap, merging the two alphas, but also restore
            int layerPaintAlpha = mLayerPaint.getAlpha();
            if (alpha < 1) {
                mLayerPaint.setAlpha((int) (alpha * layerPaintAlpha));
            }
            // 使用圖層的Paint去繪制緩存內(nèi)容
            canvas.drawBitmap(cache, 0.0f, 0.0f, mLayerPaint);
            if (alpha < 1) {
                mLayerPaint.setAlpha(layerPaintAlpha);
            }
        }
    }
    // ...
}

4. 繪制裝飾物

View # draw()
if (!verticalEdges && !horizontalEdges) {
    // ...
    onDrawForeground(canvas);
    // ...
}
View # onDrawForeground()
public void onDrawForeground(Canvas canvas) {
    // 繪制滾動指示器
    onDrawScrollIndicators(canvas);
    // 繪制滾動條
    onDrawScrollBars(canvas);
    final Drawable foreground = mForegroundInfo != null ? mForegroundInfo.mDrawable : null;
    if (foreground != null) {
        // ...
        // 如果有前景圖片,就繪制它
        foreground.draw(canvas);
    }
}

5. 如果需要繪制邊框陰影

前面那些步驟走完后就能直接return了。如果需要繪制陰影的話,則不會進入前面分析的方法塊。而是執(zhí)行下面的步驟。注釋上說這種情況不常見。

View # draw()
  • 同樣地繪制背景
  • 去計算陰影帶來的變量影響,對陰影分類去保存圖層
  • 同樣地繪制自身
  • 同樣地分發(fā)繪制流程
  • 分類去繪制陰影
  • 加載保存的圖層
  • 同樣地繪制裝飾
// 共同的步驟,繪制背景圖片
if (!dirtyOpaque) {
    drawBackground(canvas);
}
// 不需要繪制陰影的情況
if (!verticalEdges && !horizontalEdges) {
    // ...
    return;
}

// 如果需要繪制陰影

// 根據(jù)陰影情況去改變參數(shù)......

if (solidColor == 0) {
    final int flags = Canvas.HAS_ALPHA_LAYER_SAVE_FLAG;
    // 保存圖層
    if (drawTop) {
        canvas.saveLayer(left, top, right, top + length, null, flags);
    }
    // ...
} else {
    scrollabilityCache.setFadeColor(solidColor);
}
// 繪制自身
if (!dirtyOpaque) onDraw(canvas);
// 繪制子View
dispatchDraw(canvas);
// ...
// 繪制陰影
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);
}
// ...
// 取出保存的圖層
canvas.restoreToCount(saveCount);
// ...
// 繪制裝飾
onDrawForeground(canvas);
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,431評論 6 544
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,637評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,555評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,900評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,629評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,976評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,976評論 3 448
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,139評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,686評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 41,411評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,641評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,129評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,820評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,233評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,567評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,362評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,604評論 2 380

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