1. 程式人生 > >自定義元件開發一 View 的繪製流程

自定義元件開發一 View 的繪製流程

Activity 的組成結構

Activity對於安卓開發來說,是熟悉的不能再熟悉的,它是安卓四大元件之一,用來做介面顯示用的,那麼我相信,並不是所有的朋友都對Activity的組成結構有清晰的認識,這裡簡單聊聊Activity的組成。
實際介面展示的是Activity中的Window來完成的,Activity中有一個PhoneWindow物件,它繼承自Window,裡面有一個頂級的RootView 叫DecorView,DecorView 是我們建立的所有的View的根,DecorView 由三部分組成 ActionBar、標題區和內容區,其中內容區是我們用的比較多的,比如我們可以通過android.R.id.content 獲取到內容區,是一個FrameLayout。
DecorView 繼承自FrameLayout,我們在Activity 中呼叫SetContentView(layoutResID)

    /**
     * Set the activity content from a layout resource.  The resource will be
     * inflated, adding all top-level views to the activity.
     *
     * @param layoutResID Resource ID to be inflated.
     *
     * @see #setContentView(android.view.View)
     * @see #setContentView(android.view.View, android.view.ViewGroup.LayoutParams)
     */
public void setContentView(int layoutResID) { getWindow().setContentView(layoutResID); initWindowDecorActionBar(); }

其實就是傳遞給PhoneWindow

362     @Override
363     public void More ...setContentView(int layoutResID) {
364         // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
365 // decor, when theme attributes and the like are crystalized. Do not check the feature 366 // before this happens. 367 if (mContentParent == null) { 368 installDecor(); 369 } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) { 370 mContentParent.removeAllViews(); 371 } 372 373 if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) { 374 final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID, 375 getContext()); 376 transitionTo(newScene); 377 } else { 378 mLayoutInflater.inflate(layoutResID, mContentParent); 379 } 380 final Callback cb = getCallback(); 381 if (cb != null && !isDestroyed()) { 382 cb.onContentChanged(); 383 } 384 }

mContentParent 就是android.R.id.content 表示的那個內容區容器。最終我們setContentView 傳進來的View就新增到了mContentParent 上面。

這裡寫圖片描述

這張圖可以形象表面,上面所說的各部分關係:

Activity 類似於一個框架,負責容器生命週期及活動,視窗通過 Window 來管理;
Window 負責視窗管理(實際是子類 PhoneWindow),視窗的繪製和渲染交給 DecorView完成;
DecorView 是 View 樹的根,開發人員為 Activity 定義的 layout 將成為 DecorView 的子檢視 ContentParent 的子檢視;
layout.xml 是開發人員定義的佈局檔案,最終 inflate 為 DecorView 的子元件;

另外PhoneWinodw 中有兩個比較重要的物件WindowManager和ViewRootImpl。
WindowManager 處理觸控事件,ViewRootImpl去完成UI的繪製。
這裡寫圖片描述

View的繪製

ViewRootImpl 負責 Activity 整個 UI 的繪製,而繪製是 ViewRootImpl 的
performTraversals()方法。
裡面有三個方法:
這裡寫圖片描述

performMeasure():測量元件的大小;
performLayout():用於子元件的定位(放在視窗的什麼地方);
performDraw():將元件的外觀繪製出來;

測量元件

performMeasure()方法負責元件自身尺寸的測量,我們知道,在 layout 佈局檔案中,每一個
元件都必須設定 layout_width 和 layout_height 屬性。
屬性值有三種可選模式:wrap_content、match_parent 和數值,
performMeasure()方法根據設定的模式計算出元件的寬度和高度。大多數情況下模式為 match_parent 和數值的時候是不需要計算的,傳過來的就是父容器自己計算好的尺寸或是一個指定的精確值,只有當模式為 wrap_content 的時候才需要根據內容進行尺寸的測量。

private void performMeasure(int childWidthMeasureSpec, int
childHeightMeasureSpec) {
    try {
            mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        } finally {
    }
}

performMeasure方法測量元件呼叫了View的 measure方法

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    ……
    onMeasure(widthMeasureSpec, heightMeasureSpec);
    ……
}

onMeasure()一般我們需要重寫這個方法去給View的子類指定寬度和高度。

如果測量的是容器的尺寸,而容器的尺寸又依賴於子元件的大小,所以必須先測量容器中子
元件的大小,不然,測量出來的寬度和高度永遠為 0

確定元件的位置

performLayout()方法用於確定子元件的位置,所以,該方法只針對 ViewGroup 容器類。作為容器,必須為容器中的子 View 精確定義位置和大小。

private void performLayout(WindowManager.LayoutParams lp,
                        int desiredWindowWidth, int desiredWindowHeight){
    ……
    host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
    ……
    for (int i = 0; i < numValidRequests; ++i) {
        final View view = validLayoutRequesters.get(i);
        view.requestLayout();
    }
}

程式碼中的 host 是 View 樹中的根檢視(DecroView),也就是最外層容器,容器的位置安排在左上角(0, 0),其大小預設會填滿 mContentParent 容器。我們重點來看一下 layout()方法的原始碼

public void layout(int l, int t, int r, int b) {
    if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
        onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
        mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
    }
    int oldL = mLeft;
    int oldT = mTop;
    int oldB = mBottom;
    int oldR = mRight; 
    boolean changed = isLayoutModeOptical(mParent) ?
    setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
    if (changed || (mPrivateFlags &
        PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
        onLayout(changed, l, t, r, b);
    ……
    }
    ……
}

在 layout()方法中,在定位之前如果需要重新測量元件的大小,則先呼叫 onMeasure()方法,接下來執行 setOpticalFrame()或 setFrame()方法確定自身的位置與大小,此時只是儲存了相關的值,與具體的繪製無關。隨後,onLayout()方法被呼叫

protected void onLayout(boolean changed, int left, int top, int right, int          bottom) {

}

onLayout()方法在這裡的作用是當前元件為容器時,負責定位容器中的子元件

繪製元件

performDraw()方法執行元件的繪製功能,元件繪製是一個十分複雜的過程,不僅僅繪製組
件本身,還要繪製背景、滾動,繪製流程:

private void performDraw() {
final boolean fullRedrawNeeded = mFullRedrawNeeded;
mFullRedrawNeeded = false;
mIsDrawing = true;
try {
    draw(fullRedrawNeeded);
} finally {
    mIsDrawing = false;
}
……
}

在 performDraw()方法中呼叫了 draw()方法:

private void draw(boolean fullRedrawNeeded) {
……
if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
 return;
}
……
}

draw()方法又呼叫了 drawSoftware()方法:

private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
boolean scalingRequired, Rect dirty) {
……
final Canvas canvas;
final int left = dirty.left;
final int top = dirty.top;
final int right = dirty.right;
final int bottom = dirty.bottom;
canvas = mSurface.lockCanvas(dirty);
if (!canvas.isOpaque() || yoff != 0 || xoff != 0) {
    canvas.drawColor(0, PorterDuff.Mode.CLEAR);
}
mView.draw(canvas);
surface.unlockCanvasAndPost(canvas);
……
}

drawSoftware()方法中呼叫了 mView 的 draw()方法,前面說過,mView 是 Activity 介面中 View樹的根(DecroView),也是一個容器(具體來說就是一個 FrameLayout 佈局容器)FrameLayout 類的 draw()方法原始碼:

public void draw(Canvas canvas) {
super.draw(canvas);
……
final Drawable foreground = mForeground;
foreground.draw(canvas);
}

FrameLayout 類的 draw()方法做了兩件事,一是呼叫父類的 draw()方法繪製自己,二是將前景點陣圖畫在了 canvas 上。自然,super.draw(canvas)語句是我們關注的重點,FrameLayout 繼承自ViewGroup,遺憾的是 ViewGroup 並沒有重寫 draw()方法,也就是說,ViewGroup 的繪製完全重用了他的父類 View 的 draw()方法,不過,ViewGroup 中定義了一個名為 dispatchDraw()的方法,該方法在 View 中定義,在 ViewGroup 中實現,至於有什麼用,暫且賣個關子,我們先扒開 View的 draw()方法原始碼看看:

public void draw(Canvas canvas) {
    background.draw(canvas);
    if (!dirtyOpaque) onDraw(canvas);
    dispatchDraw(canvas);
    onDrawScrollBars(canvas);
}

View 類的 draw()方法是元件繪製的核心方法,主要做了下面幾件事:
繪製背景:background.draw(canvas)
繪製自己:onDraw(canvas)
繪製子檢視:dispatchDraw(canvas)
繪製滾動條:onDrawScrollBars(canvas)
background 是一個 Drawable 物件,直接繪製在 Canvas 上,並且與元件要繪製的內容互不干擾,很多時候,這個特徵能被某些場景利用,比如後面的“刮刮樂”就是一個很好的範例。
View 只是元件的抽象定義,他自己並不知道自己是什麼樣子,所以,View 定義了一個空方法 onDraw(),原始碼如下:

protected void onDraw(Canvas canvas) {

}

和前面講過的 onMeasure()與 onLayout()一樣,onDraw()方法同樣是預留給子類擴充套件的功能
介面,用於繪製元件自身。元件的外觀由該方法來決定。
dispatchDraw()方法也是一個空方法,原始碼如下:

protected void dispatchDraw(Canvas canvas) {

}

該方法服務於容器元件,容器中的子元件必須通過 dispatchDraw()方法進行繪製,所以,View雖然沒有實現該方法但他的子類 ViewGroup 實現了該方法:

protected void dispatchDraw(Canvas canvas) {
    ……
    final int count = mChildrenCount;
    final View[] children = mChildren;
        for (int i = 0; i < count; i++) {
            final View child = children[i];
            if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
                || child.getAnimation() != null) {
                    more |= drawChild(canvas, child, drawingTime);
                }
        }
    ……
}

在 dispatchDraw()方法中,迴圈遍歷每一個子元件,並呼叫 drawChild()方法繪製子元件,而子元件又呼叫 View 的 draw()方法繪製自己:

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

元件的繪製也是一個遞迴的過程,說到底 Activity 的 UI 介面的根一定是容器,根容器繪製
結束後開始繪製子元件,子元件如果是容器繼續往下遞迴繪製,否則將子元件繪製出來……直所有的元件正確繪製為止。
總體來說,UI 介面的繪製從開始到結束要經歷幾個過程:
測量大小,回撥 onMeasure()方法
元件定位,回撥 onLayout()方法
元件繪製,回撥 onDraw()方法
概括描述元件的繪製過程就是,首先通過PerformMeasure方法測量元件的大小,接PerformLayout來定位元件的位置,最後呼叫PerformDraw方法來繪製元件。

謝謝認真觀讀本文的每一位小夥伴,衷心歡迎小夥伴給我指出文中的錯誤,也歡迎小夥伴與我交流學習。
歡迎愛學習的小夥伴加群一起進步:230274309