View的繪製流程

一、把DecorView添加到窗口上

找到繪製入口,先從ActivityThread的handleMessage方法開始。不過得先提一下handleMessage方法所在的類–H,它是Handler的子類,是主線程處理消息的。

handleMessage方法中的switch…case有好多種情況。其中LAUNCH_ACTIVITY就是啓動Activity。該case內調用hadnleLaunchActivity(r,null,“LAUNCH_ACTIVITY”)方法來啓動Activity。在handleLaunchActivity方法中:

  • ①先調用了performLaunchActivity方法
  • ②後調用了handleResumeActivity方法

重點看一下handleResumeActivity方法:

①調用了performResumeActivity方法,它會回調Activity生命週期的onResume()
在這裏插入圖片描述②繼續往下看,初始化了WindowManager.LayoutParams,這是窗口的佈局屬性對象。
在這裏插入圖片描述
③然後調用wm.add(decor,l)方法
在這裏插入圖片描述這個wm指的就是ViewManager:

ViewManager wm = a.getWindowManager();

而ViewManager是一個接口:
在這裏插入圖片描述通過a.getWindowManager();來找到這個Interface的實現類:
在這裏插入圖片描述經過搜索,發現在Activity類中找到了mWindowManager的身影:
在這裏插入圖片描述而mWindow的唯一實現類就是PhoneWindow,所以接下來要去PhoneWindow類中找getWindowManager方法:
在這裏插入圖片描述
在這裏插入圖片描述找到目標–mWindowManager,發現它通過createLocalWindowManager方法來實例化,返回的對象的類型爲WindowManagerImpl。
在這裏插入圖片描述矛頭 一轉,新目標成爲WindowManagerImpl了,我們要找它裏面的add()方法。結果裏面調用的是mGlobal.add()方法:
在這裏插入圖片描述在這裏插入圖片描述WindowManagerGlobal的addView()方法中:

①聲明瞭ViewRootImpl root,是通過以下構造方法將root進行的實例化:

root=new ViewRootImpl(view.getContext(),display);

②給view setLayoutParams(wparams)
③將view、root、wparams添加到了對應的集合中
④通過root.setView將view、wparams、panelParentView進行關聯
在這裏插入圖片描述1. setView()方法中調用了requestLayout()方法:
在這裏插入圖片描述方法內執行了checkThread()方法:
在這裏插入圖片描述2. 調用scheduleTraversales():執行某些操作
在這裏插入圖片描述mChoreographer.postCallback(…,mTraversalRunnable,…),這個mTraversalRunnable是一個Runnable,說明它的run()方法一定會被調用—>執行doTraversal()方法
在這裏插入圖片描述在這裏插入圖片描述剛纔做的事,就是把DecorView添加到窗口上:
在這裏插入圖片描述
activity創建後,通過wm調用addView(decorView,layoutParams)方法,參數分別爲頂層view、頂層view的佈局屬性。然後調用WindowManagerGlobal的addView方法來創建ViewRootImpl對象,並調用ViewRootImpl對象的setView(decorView,layoutParams,parentView)方法來將3個參數進行一個關聯。關聯成功後,ViewRootImpl就準備繪製。繪製開始是調用ViewRootImpl.requestLayout()方法–>scheduleTraversals()–>doTraversal()–>performTraversals()。真正的繪製,是交給performMeasure(childWidthMeasureSpec,childHeightMeasureSpec)
performLayouy(lp,mWidth,mHeight)以及performDraw()了。

二、測量–performMeasure方法

mView就是頂層的佈局容器–DecorView,調用了DecorView的measure方法
在這裏插入圖片描述
來到了View的measure方法,且方法被final修飾表示不能被重寫。
首先取緩存:

if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);

緩存取到後,調用onMeasure()方法。如果緩存沒讀到,就對其重新進行設置:

mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
        (long) mMeasuredHeight & 0xffffffffL); // suppress sign extension

看onMeasure()方法:
在這裏插入圖片描述在這裏插入圖片描述這個方法做的,就是對這倆個成員變量賦值,然後對標記位設置表示已經對成員變量賦值了。對測量的寬高measureWidth和measureHeight進行保存,來確定控件的寬、高。

寬、高的確定,取決於mView.measure(childWidthMeasureSpec,childHeightMeasureSpec);對測量原理的理解,和View類中measure(int widthMeasureSpec,int heightMeasureSpec)密不可分。在測量的過程中,需要測量的內容包括兩部分:模式 * 尺寸。模式 * 尺寸 被封裝到了MeasureSpec(32位的int型的值)類中,前2位表示模式-- SpecMode,後30位表示尺寸–SpecSize。

View類的靜態內部類MeasureSpec類中,定義了3個靜態常量:UNSPECIFIED、EXACTLY、AT_MOST。而makeMeasureSpec將size和mode打包爲MeasureSpec,通過makeMeasureSpec()方法中的else部分(我們可以指定mode和size)。
MODE_MASK是11000000000000000000000000000000,~MODE_MASK=00111111111111111111111111111111。
於是size&~MODE_MASK的結果就是111111111111111111111111111111(取後30位)。同理,mode & MODE_MASK的結果就是11。
mode+size—>MeasureSpec
後面的**getMode方法(取前2位)getSize方法(取後30位)**就是解包操作:MeasureSpec—>mode+size

public static class MeasureSpec{
    private static final int MODE_SHIFT = 30;//表示30位

    //將0平移30位:00000000000000000000000000000000
    //父容器不對View做任何限制,系統內部使用
    public static final int UNSPECIFIED = 0 << MODE_SHIFT;

    //將1平移30位:01000000000000000000000000000000
    //父容器檢測出View的大小,View的大小就是SpecSize。match_parent/具體值
    public static final int EXACTLY     = 1 << MODE_SHIFT;

    //將2平移30位:10000000000000000000000000000000
    //父容器指定一個可用大小,View的大小不能超過這個值。warp_content
    public static final int AT_MOST     = 2 << MODE_SHIFT;
    
    //表示可以將模式、尺寸轉換爲對應的MeasureSpec。方法參數分別爲
    //size、mode。該方法可以將這兩個參數打包生成MeasureSpec。
    public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
                                  @MeasureSpecMode int mode) {
        if (sUseBrokenMakeMeasureSpec) {
            return size + mode;
        } else {
            return (size & ~MODE_MASK) | (mode & MODE_MASK);
        }
    }
    
    //取前兩位
    public static int getMode(int measureSpec) {
        //noinspection ResourceType
        return (measureSpec & MODE_MASK);
    }
    //取後30位
    public static int getSize(int measureSpec) {
        return (measureSpec & ~MODE_MASK);
    }
}

既然size和mode都封裝到了MeasureSpec中,那麼曾經調用performMeasure(childWidthMeasureSpec,childHeightMeasureSpec)方法時(2275行),參數childWidthMeasureSpec和childHeightMeasureSpec這兩個值是如何確定的呢?
在這裏插入圖片描述getRootMeasureSpec(mWidth,lp.width)參數分別爲窗口的寬、頂層view(即DecorView)佈局屬性的寬。

根據頂層View即DecorView容器來判斷:

private static int getRootMeasureSpec(int windowSize, int rootDimension) {
    int measureSpec;
    switch (rootDimension) {

    //頂層容器充滿父容器,就調用打包方法MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY)。
    //窗口的寬、EXACTLY模式。
    case ViewGroup.LayoutParams.MATCH_PARENT:
        // Window can't resize. Force root view to be windowSize.
        measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
        break;
    //頂層容器是包裹類型的,模式就是AT_MOST。
    case ViewGroup.LayoutParams.WRAP_CONTENT:
        // Window can resize. Set max size for root view.
        //窗口可以調整,但是最大的值就是窗口本身的尺寸
        measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
        break;
    //固定尺寸類型,寬就是固定尺寸,模式就是EXACTLY。
    default:
        // Window wants to be an exact size. Force root view to be that size.
        measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
        break;
    }
    return measureSpec;
}

View的測量,要確定DecorView的MeasureSpec。DecorView的MeasureSpec由窗口大小和自身LayoutParams決定,並遵守以下規則:

  • LayoutParams.MATCH_PARENT:精確模式,窗口大小
  • LayoutParams.WARP_CONTENT:最大模式,最大爲窗口大小
  • 固定大小:精確模式,大小爲LayoutParams的大小

因此在performMeasure方法中調用mView.measure(childWidthMeasureSpec,childHeightMeasureSpec)時,mView代表的就是DecorView。
在這裏插入圖片描述mView.measure方法—>onMeasure(widthMeasureSpec,heightMeasureSpec)方法,於是DecorView的onMeasure()方法會被調用。然而DecorView繼承自FrameLayout,所以得看FrameLayout的onMeasure()方法。
在這裏插入圖片描述FrameLayout的onMeasure()方法,其中的兩個參數widthMeasureSpec和heightMeasureSpec就是當前容器的測量規格,當然這裏面也各自封裝了mode和size。
在這裏插入圖片描述FrameLayout的這個onMeasure()方法裏面,有一段是for循環遍歷每一個子view,調用 measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0)方法
在這裏插入圖片描述接下來,我們就要搞懂它是如何測量子view的測量規格的?注意一點,這裏的參數傳遞parentWidthMeasureSpec和parentHeightMeasureSpec是最初調用onMeasure()方法時傳遞過來的當前容器FrameLayout的測量規格。並將其繼續傳遞給了getChildMeasureSpec(parentWidthMeasureSpec,…)和getChildMeasureSpec(parentHeightMeasureSpec,…)方法

對於getChildMeasureSpec()方法,參數spec:父容器FrameLayout的測量規格;padding:父容器已經使用的空間;childDimension:子控件佈局參數對應的尺寸。
在這裏插入圖片描述首先會先去獲取父容器FrameLayout對應的mode和size:

int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);

然後判斷父容器的mode:
其中的賦值resultSize=size;只是暫時賦值,並非最終大小!

int size = Math.max(0, specSize - padding);

int resultSize = 0;
int resultMode = 0;
switch (specMode) {
// 父容器是精確類型
case MeasureSpec.EXACTLY:
    //子view的佈局參數寫的是固定大小,即childDimension >= 0,
    //那麼子view的size爲固定大小,mode爲EXACTLY精確模式
    if (childDimension >= 0) {
        resultSize = childDimension;
        resultMode = MeasureSpec.EXACTLY;
    } else if (childDimension == LayoutParams.MATCH_PARENT) {
        // 如果mode爲MATCH_PARENT,則size爲父容器的size,mode爲EXACTLY
        //因爲 要考慮padding即四周的
        resultSize = size;
        resultMode = MeasureSpec.EXACTLY;
    } else if (childDimension == LayoutParams.WRAP_CONTENT) {
        // Child wants to determine its own size. It can't be
        // bigger than us.
        //子view想自己決定尺寸,但是不能超過size。mode爲AT_MOST
        resultSize = size;
        resultMode = MeasureSpec.AT_MOST;
    }
    break;

// 父容器的模式爲AT_MOST,意味着父容器也不知道自己的寬高
case MeasureSpec.AT_MOST:
    //子view尺寸固定
    if (childDimension >= 0) {
        // 子view尺寸爲固定大小,模式爲EXACTLY
        resultSize = childDimension;
        resultMode = MeasureSpec.EXACTLY;
    } else if (childDimension == LayoutParams.MATCH_PARENT) {
        // Child wants to be our size, but our size is not fixed.
        // Constrain child to not be bigger than us.
        resultSize = size;
        resultMode = MeasureSpec.AT_MOST;
    } else if (childDimension == LayoutParams.WRAP_CONTENT) {
        // Child wants to determine its own size. It can't be
        // bigger than us.
        resultSize = size;
        resultMode = MeasureSpec.AT_MOST;
    }
    break;

// Parent asked to see how big we want to be
case MeasureSpec.UNSPECIFIED:
    if (childDimension >= 0) {
        // Child wants a specific size... let him have it
        resultSize = childDimension;
        resultMode = MeasureSpec.EXACTLY;
    } else if (childDimension == LayoutParams.MATCH_PARENT) {
        // Child wants to be our size... find out how big it should
        // be
        resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
        resultMode = MeasureSpec.UNSPECIFIED;
    } else if (childDimension == LayoutParams.WRAP_CONTENT) {
        // Child wants to determine its own size.... find out how
        // big it should be
        resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
        resultMode = MeasureSpec.UNSPECIFIED;
    }
    break;
}

得到“暫時”的resultSize和resultMode,最後 :

return MeasureSpec.makeMeasureSpec(resultSize, resultMode);

返回生成的MeasureSpec,供child.measure(childWidthMeasureSpec, childHeightMeasureSpec);方法調用。這裏明白一點:當容器測量時,先遍歷子view對子view進行測量(首先獲取子View的MeasureSpec,然後再調用子view的child.measure()方法將2個測量規格傳進來),如此父容器就能完成對子View的測量。

當FrameLayout的onMeasure()方法在for循環測量完子View後,就會經過計算確認最大的寬高:

// Check against our minimum height and width
maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());

然後調用setMeasuredDimension()方法來決定自身的寬高(只有確定了子view的寬高,才能繼續確定viewGroup的寬高)。這就是我們容器測量的一個過程。

總結一下ViewGroup的測量過程:

ViewGroup measure–>onMeasure(測量子控件的寬高)–>setMeasuredDimension—>setMeasuredDimensionRaw(保存自己的寬高)

子view的測量過程:

View measure–>onMeasure–>setMeasuredDimension—>setMeasuredDimensionRaw(保存自己的寬高) 其中子View的測量在調用setMeasuredDimension方法前,調用了getDefaultSize方法首先得到specMode和specSize,測量規格中的size賦值給了resultSize。也就說明了如果自定義view時不重寫onMeasure方法時,match_parent和warp_content效果是一樣的。
在這裏插入圖片描述這也就說明了爲什麼自定義View的時候,一定要重寫onMeasure方法。如果是自定義父容器,①測量子view寬高②設置父容器寬高;如果是自定義view,在onMeasure方法中要重新確認自己的寬高。
在這裏插入圖片描述

三、佈局–performLayout方法

佈局從ViewRootImpl類的performLayout方法開始!
其中參數lp:頂層佈局參數的屬性;mWidth、mHeight:頂層佈局的寬、高
在這裏插入圖片描述確定了頂層View爲host(DecorView):
在這裏插入圖片描述調用host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight())方法,其中參數host.getMeasuredWidth、host.getMeasuredHeight就是頂層view的測量寬、測量高
在這裏插入圖片描述然後來到了View類的layout()方法!

①目的就是爲了通過setFrame(left,top,right,bottom)方法確認view的左、上、右、下的值:
在這裏插入圖片描述setFrame()方法就是對左、上、右、下四個值進行賦值操作,當賦值完成後,view的位置就擺放成功了!
在這裏插入圖片描述②除了確認自身左、上、右、下四個方位之外,還調動了onLayout()方法,這是一個空方法,供子類來實現。如果是ViewGroup容器要在onLayout中進行擺放;如果只是一個view那麼只確認自己的位置就好了、無需重寫onLayout()方法。
在這裏插入圖片描述其中onLayout()是一個空方法,供子類去重寫實現的。如果是子view就不用重寫;如果是ViewGroup就得重寫onLayout()方法,並在方法內部擺放
在這裏插入圖片描述View的佈局:

  • 調用view.layout確定自身的位置,即確定mLeft、mTop、mRight、mBottom的值
  • 如果是ViewGroup類型,需要調用onLayout方法確定子view的位置

如果是ViewGroup類型,就要調用其onLayout()方法確定子View的位置。以FrameLayout爲例,看一下FrameLayout類中onLayout()方法的源碼:
在這裏插入圖片描述①layoutChildren()方法就是遍歷子View
在這裏插入圖片描述②遍for循環遍歷子View過程中,每循環一次就會調用child.layout()對子View進行位置的擺放,形成了一個遞歸操作。
在這裏插入圖片描述ViewGroup layout(確定自己的位置,4個點的位置)—>onLayout(進行子View的佈局)
View layout(來確定自己的位置,4個點的位置)

四、繪製–performDraw方法

View的繪製有以下內容:

  • 繪製背景:drawBackground(canvas)
  • 繪製自己:onDraw(canvas)
  • 繪製子View:dispatchDraw(canvas)
  • 繪製前景,滾動條等裝飾:onDrawForeground(canvas)

繪製從ViewRootImpl類的performDraw方法開始!
在這裏插入圖片描述performDraw()方法內部調用了draw()方法,draw()內調用了drawSoftware()方法
在這裏插入圖片描述drawSoftware()方法內部有一個關鍵的方法–mView.draw(canvas)方法:
在這裏插入圖片描述其中的mView就是DecorView,mView.draw(canvas)—>會調到View類的draw()方法:
在這裏插入圖片描述這裏寫了一堆註釋,繪製的步驟執行以下幾步:

  • ①繪製背景–drawBackground()方法
  • ②如果需要,進行圖層的保存
  • ③繪製view自己的內容
  • ④繪製子View。,如果是容器,就要實現子view的繪製
  • ⑤繪製裝飾;如滾動條等。若自定義view的過程中需要用到自定義view的繪製,就要重寫onDraw()方法;如果是一個容器,還需要做的就是去繪製子View。

這裏要查的就是ViewGroup的dispatchDraw()方法!
ViewGroup實現了dispatchDraw()方法,它的職責就是對子控件view的繪製。
在這裏插入圖片描述點進去看drawChild()方法:
在這裏插入圖片描述這裏需要注意的是:如果是ViewGroup容器,會在dispatchDraw內部做子View的繪製!

ViewGroup:

  • 繪製背景:drawBackground(canvas)
  • 繪製自己:onDraw(canvas)
  • 繪製子View:dispatchDraw(canvas)
  • 繪製前景、滾動條等裝飾:onDrawForeground(canvas)

View:

  • 繪製背景:drawBackground(canvas)
  • 繪製自己:onDraw(canvas)
  • 繪製前景、滾動條等裝飾:onDrawForeground(canvas)

五、總結

onMeasure—>onLayout(ViewGroup需要重寫該方法)—>onDraw(按需)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章