自定義View:測量measure,布局layout,繪制draw


1. 什么是View

Android的官方文檔中是這樣描述的:表示了用戶界面的基本構建模塊。一個View占用了屏幕上的一個矩形區域並且負責界面繪制和事件處理。

手機屏幕上所有看得見摸得着的都是View。這一點對所有圖形系統來說都一樣,例如iOS的UIView。

2. View和Activity的區別

我們之前學習過android的四大組件,Activity是四大組件中唯一一個用來和用戶進行交互的組件。可以說Activity就是android的視圖層。

如果再細化,Activity相當於視圖層中的控制層,是用來控制和管理View的,真正用來顯示和處理事件的實際上是View。

每個Activity內部都有一個Window對象, Window對象包含了一個DecorView(實際上就是FrameLayout),我們通過setContentView給Activity設置顯示的View實際上都是加到了DecorView中。

3. View種類

android提供了種類豐富的View來應對各種需求,例如提供文字顯示的TextView,提供點擊事件的Button,提供圖片顯示的ImageView,還有各種布局文件,例如Relativilayout,Linearlayout等等。他們都是繼承自View。

view

4. ViewGroup

ViewGroup繼承自View,並實現了兩個接口ViewParent和ViewManager。

ViewManager提供了三個抽象方法addView,removeView,updateViewLayout。用來添加、刪除、更新布局。

ViewParent主要提供了一系列操作子View的方法例如焦點的切換,顯示區域的控制等等。

5. 為什么要有ViewGroup?

實際上所有的事情View都能做,包括顯示復雜的界面,我們只需要設計一個復雜的View即可。例如短信通知的icon,一個可以顯示圖片又可以顯示文字的View,我們后期學習了View的draw方法后,可以輕松的設計一個View來達到這個效果,但是這樣不僅復雜,而且重用性較差,還會因為一點小改動而重復的創造輪子,這顯然不符合程序員偷懶的原則,所以我們可以完全把ImageView和TextView組合到一起就可以了,這個時候我們就需要一個容器,ViewGroup,來裝這兩個View。

ViewGroup和View最大的不同是可以組合多個View,那么多個View在一起,該如何擺放,這就是ViewGroup需要解決的問題。

6. View樹

我們看到的界面,都是以一個ViewGroup作為根View,通過往ViewGroup中添加子View(可以是View,也可以是ViewGroup),來組合出各具特色的界面。

這種從根到葉的組合方式,我們可以看做成一個View樹。(類似於XML),而View的顯示和事件處理,都是依賴於這個View樹。

繪制和事件處理的起始點,都是從根View開始一級一級的往下傳遞。我們從任意一層發起繪制,都將反饋到根View,然后再從上往下傳遞。

之前我們說過根View就是Window中的DecorView,也就是一個FrameLayout。

6.1 View樹示意圖

view

對SystemUI,也就是我們常說的StatusBar顯示在哪兒呢,其實SystemUI是一個單獨的App,隨着系統啟動而啟動,將會啟動一個系統級服務,接收我們提交的通知,該應用也會有一個window,並且級別比我們普通應用的window要高,所以會顯示在我們的應用的外面,只不過該window的高度比較小。

7. View的測量、布局、繪制過程

整個android系統 CS架構,view被展示到界面上需要經過3個步驟

  • 需要花多大:measure –> onMeasure –> setMeasuredDimension
  • 畫在什么地方:layout –> setFrame –> onLayout
  • 怎么畫:draw –> > onDraw –> dispatchDraw

7.1 顯示一個View需要經過哪些步驟

  • Measure測量一個View的大小
  • Layout擺放一個View的位置
  • Draw畫出View的顯示內容

其中measure和layout方法都是final的,無法重寫,雖然draw不是final的,但是也不建議重寫該方法。這三個方法都已經寫好了View的邏輯,如果我們想實現自身的邏輯,而又不破壞View的工作流程,可以重寫onMeasure、onLayout、onDraw方法。

7.2 如何發起一個View樹的測量/布局/繪制流程

通過調用requestLayout/requestFocus都將發起一個View樹的測量。測量完畢后會進行布局,布局完畢后就會繪制。

如果View的大小沒有發生改變,布局也沒有變化,只是顯示的內容發生了變化,則可以通過invalidate來請求繪制,此時將不會測量和布局,直接從繪制開始。

7.3 View內部的mPrivateFlags變量

View中有一個私有int變量mPrivateFlags,用於保存View的狀態,int型32位,通過0/1可以保存32個狀態的true或者false,采用這種方式可以有效的減少內存占用,提高運算效率。

當某一個View發起了測量請求時,將會把mPrivateFlags中的某一位從0變為1,同時請求父View,父View也會把自身的該值從0變為1,同時也將會把其他子View的值從0變為1。這樣一層一層傳遞,最終傳到到DecorView,DecorView的父View是ViewRoot,所以最終都將由ViewRoot來進行處理。

ViewRoot收到請求后,將會從上至下開始遍歷,檢查標記,只要有相對應的標記就執行測量/布局/繪制

當Activity被創建時,會相應的創建一個Window對象,Window對象創建時會獲取應用的WindowManager(注意,這是應用的窗口管理者,不是系統的)。

Activity被創建后,會調用Activity的onCreate方法。我們通過設置setContentView就會調用到Window中的setContextView,從而初始化DecorView。

所以我們需要隱藏標題欄什么的,都需要在DecorView初始化之前進行設置。

DecorView初始化之后將會被添加到WindowManager中,同時WindowManager中會為新添加的DecorView創建一個對應的ViewRoot,並把DecorView設置給ViewRoot。

所以根View就是DecorView,因為DecorView的父親是ViewRoot,實現自ViewParent接口,但是沒有繼承自View,所以根本不是一個View。

從系統的命名來看,WindowManger繼承自ViewManager,而添加到WindowManager中的是DecorView,不是Window,都說明了其實真正意義上的window就是View。

在ViewRoot的構造方法中會通過getWindowSession來獲取WindowManagerService系統服務的遠程對象(這才是系統級的)。

當ViewRoot的setView方法中將會調用requestLayout進行第一次視圖測量請求。同時sWindowSession.add自身內部的一個W對象,以此達到和WindowManagerService的關聯。

W是一個Binder對象。可以實現跨進程的通信了,並且是一個雙方都掌握着主動調用的跨進程通信方式。

7.4 常用的標記位

  • FORCE_LAYOUT 請求繪制,將從measure開始,,並增加LAYOUT_REQUIRED標記
  • 持有LAYOUT_REQUIRED標記的View將會被執行layout,完畢后會去掉LAYOUT_REQUIRED和FORCE_LAYOUT
  • DRAWN帶有該標簽的將不會被draw,注意,這和上面兩個不一致,當draw完畢后會加上該標簽,當沒有該標簽才會被draw。

還有一些其他的標記位,大家可以自行閱讀源碼。

7.5 測量/布局/繪制流程

view

測量事件最終傳遞到decorView的父親ViewRoot那里,由它的函數performTraversals來執行,聽名字就知道是執行遍歷了。

首先它會檢測之前設置的標記為來確定是否需要測量大小,是,就會直接執行decorView的measture方法,該方法內部會測量完自身后,將會繼續遍歷所有子View,直到每一個設置有標記的子View都測量完。

然后它會檢測是否需要布局,是,將會執行decorView的layout方法進行,該方法內部也會遍歷所有設置有標記位子View。

8. measure 測量

8.1 測量流程

measure

測量View是在measure()方法中,而measure()方法是final修飾的,不允許重寫,但是在measure()方法中回調了onMeasure()方法,所以我們自定義View的時候需要重寫onMeasure()方法,在該方法中實現測量的邏輯

  • 如果是普通View,則直接通過setMeasureDimension()方法設置大小即可
  • 如果是ViewGroup,則需要循環遍歷所有子View,調用子View的measure()方法,測量每個子View的大小,等所有的子View都測量完畢,最后通過setMeasureDimension()設置ViewGroup自身的大小

8.2 LayoutParams

每個View都包含一個ViewGroup.LayoutParams類或者其派生類,LayoutParams中包含了View和它的父View之間的關系,而View大小正是View和它的父View共同決定的。

我們設置View的大小,有match_parent、wrap_content和具體的dip值。

match_parent對應值為-1、wrap_conten對應值為-2,具體dip對應其設定的值。在測量時,View的父類從Layout中讀出寬高值,根據不同的值設置不同的計算模式。

布局文件中所有layout_開頭的在代碼中都是需要通過LayoutParams來設置。

當我們通過addView添加一個子View時,如果它沒有LayoutParams或者是LayoutParams的類型不匹配,那么將會創建一個默認的LayoutParams。

通過布局文件進行layout_width,layout_height進行設定。通過代碼設置,需要一個LayoutParams來描述。

View view = new View(this); LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT,LayoutParams.WRAP_CONTENT); view.setLayoutParams(lp);
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

8.3 measure

/** * This is called to find out how big a view should be. The parent * supplies constraint information in the width and height parameters. * The actual measurement work of a view is performed in * {@link #onMeasure(int, int)}, called by this method. Therefore, only * {@link #onMeasure(int, int)} can and must be overridden by subclasses. * @param widthMeasureSpec Horizontal space requirements as imposed by the * parent * @param heightMeasureSpec Vertical space requirements as imposed by the * parent * * @see #onMeasure(int, int) */ public final void measure(int widthMeasureSpec, int heightMeasureSpec) { ... onMeasure(widthMeasureSpec, heightMeasureSpec); ... }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

measure是final修飾的方法,不可被重寫。在外部調用時,直接調用view.measure(int wSpec, int hSpec)。measure中調用了onMeasure。自定義view時,重寫onMeasure即可

8.4 onMeasure

measure是一個final方法,用來測量View自身的大小,View類該方法體邏輯比較簡單,只是根據判斷條件決定是否需要調用onMeasure。方法接受兩個參數,分別就是通過MeasureSpec類合成測量模式和大小的寬與高。

實際上View的大小是無限大的,measure測量出來的大小只是為了layout時父View分配給它的顯示區,也就是后來draw時畫布的剪裁大小,和touch事件分發時計算落點是否在它上面。

onMeasure通過父View傳遞過來的大小和模式,以及自身的背景圖片的大小得出自身最終的大小,通過setMeasuredDimension()方法設置給mMeasuredWidth和mMeasuredHeight。

普通View的onMeasure邏輯大同小異,基本都是測量自身內容和背景,然后根據父View傳遞過來的MeasureSpec進行最終的大小判定,例如TextView會根據文字的長度,文字的大小,文字行高,文字的行寬,顯示方式,背景圖片,以及父View傳遞過來的模式和大小最終確定自身的大小。

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); }
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

8.4 ViewGroup的onMeasure

ViewGroup是個抽象類,本身沒有實現onMeasure,但是他的子類都有各自的實現,通常他們都是通過measureChildWithMargins函數或者其他類似於measureChild的函數來遍歷測量子View,被GONE的子View將不參與測量,當所有的子View都測量完畢后,才根據父View傳遞過來的模式和大小來最終決定自身的大小。

在測量子View時,會先獲取子View的LayoutParams,從中取出寬高,如果是大於0,將會以精確的模式加上其值組合成MeasureSpec傳遞子View,如果是小於0,將會把自身的大小或者剩余的大小傳遞給子View,其模式判定在前面已經講過。

ViewGroup一般都在測量完所有子View后才會調用setMeasuredDimension()設置自身大小。

如果是一個View,重寫onMeasure時要注意:如果在使用自定義view時,用了wrap_content。那么在onMeasure中就要調用setMeasuredDimension,來指定view的寬高。如果使用的fill_parent或者一個具體的dp值。那么直接使用super.onMeasure即可。

如果是一個ViewGroup,重寫onMeasure時要注意:首先,結合上面兩條,來測量自身的寬高。然后,需要測量子View的寬高。測量子view的方式有: 
​ 
getChildAt(int index),可以拿到index上的子view。通過getChildCount得到子view的數目,再循環遍歷出子view。接着,subView.measure(int wSpec, int hSpec),使用子view自身的測量方法

或者調用viewGroup的測量子view的方法:

//某一個子view,多寬,多高, 內部減去了viewGroup的padding值 measureChild(subView, int wSpec, int hSpec); //所有子view 都是 多寬,多高, 內部調用了measureChild方法 measureChildren(int wSpec, int hSpec); //某一個子view,多寬,多高, 內部減去了viewGroup的padding值、margin值和傳入的寬高wUsed、hUsed measureChildWithMargins(subView, intwSpec, int wUsed, int hSpec, int hUsed); 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

Tips:自定義ViewGroup的時候,通常繼承FrameLayout,這樣就不必實現onMeasure()方法,讓FrameLayout幫我們實現測量的工作,我們實現onlayout()即可

  • onFinishInflate() 
    當布局加載完成的時候的回調,自定義View的時候我們可以在該方法中獲取View的寬高

  • onSizeChange() 
    當view的大小發生變化的時候的回調

    • requestLayout() 
      重新布局,包括測量measure和布局onlayout
  • resolveSize(int size, int measureSpec) 
    算出來的size和測量出來的spec那個合適用那個

public static int resolveSize(int size, int measureSpec) { return resolveSizeAndState(size, measureSpec, 0) & MEASURED_SIZE_MASK; } public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) { final int specMode = MeasureSpec.getMode(measureSpec); final int specSize = MeasureSpec.getSize(measureSpec); final int result; switch (specMode) { case MeasureSpec.AT_MOST: if (specSize < size) { result = specSize | MEASURED_STATE_TOO_SMALL; } else { result = size; } break; case MeasureSpec.EXACTLY: result = specSize; break; case MeasureSpec.UNSPECIFIED: default: result = size; } return result | (childMeasuredState & MEASURED_STATE_MASK); }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

8.5 setMeasuredDimension

8.6 MeasureSpec

一個MeasureSpec封裝了從父容器傳遞給子容器的布局要求,更精確的說法應該這個MeasureSpec是由父View的MeasureSpec和子View的LayoutParams通過簡單的計算得出一個針對子View的測量要求,這個測量要求就是MeasureSpec

這是一個含mode和size的結合體,不需要我們來具體的關心。當在測量時,可以調用MeasureSpec.getSize|getMode 得到相應的size和mode。然后使用MeasureSpec.makeMeasureSpec(size,mode); 來創建MeasureSpec對象。那么mode是怎么來的呢?是根據使用該自定義view時的layoutWith|height參數決定的,所以不能自己隨便new一個。而size可以自己指定,也可以直接使用 measureSpec.getSize。

MeasureSpe描述了父View對子View大小的期望。里面包含了測量模式和大小。

MeasureSpe類把測量模式和大小組合到一個int型的數值中,其中高2位表示模式,低30位表示大小。

我們可以通過以下方式從MeasureSpec中提取模式和大小,該方法內部是采用位移計算。

int specMode = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec);
  • 1
  • 2
  • 1
  • 2

也可以通過MeasureSpec的靜態方法把大小和模式合成,該方法內部只是簡單的相加。

MeasureSpec.makeMeasureSpec(specSize,specMode);
  • 1
  • 1

采用這種方式,是為了提升效率,因為onMeasure在繪制過程中會被大量遞歸調用。

MeasureSpec中的測量模式有以下三種

  • EXACTLY(-1):精確的,表達了父View期望子View的大小就是父View通過MeasureSpec傳遞過來的大小。
  • AT_MOST(-2):最多的,表達了父View期望子View通過測量自身的大小來決定自己的大小,但是最多不要超過MeasureSpec傳遞過來的大小。
  • UNSPECIFIED(0):未指明,通常這時候MeasureSpec傳遞過來的大小也是0,說明父View不對子View的大小做任何期望,隨子View自己決定。

通常情況下,我們應該遵守這種規則,當然如果也特殊需求也可以不遵守。但是不遵守該方式,在后面的layout中父View給你的視圖大小仍然是它給的期望值。

8.6.1 常用方法

  • MeasureSpec.getSize(widthMeasureSpec) 獲取view的寬 
    -MeasureSpec.getMode(int measurespec) 獲取測量模式 
    -MeasureSpec.makeMeasureSpec(size,mode) 組裝32位的測量策略,高2位:mode,低30位:size

8.6.2 測量策略

  • MeasureSpec.AT_MOST 
    表示子布局被限制在一個最大值內,一般當childView設置其寬、高為wrap_content時,ViewGroup會將其設置為AT_MOST

  • MeasureSpec.EXACTLY 
    表示設置了精確的值,一般當childView設置其寬、高為精確值、match_parent時,ViewGroup會將其設置為EXACTLY

  • MeasureSpec.UNSPECIFIED 
    表示子布局想要多大就多大,一般出現在AadapterView的item的heightMode中、ScrollView的childView的heightMode中;此種模式比較少見

8.6.3 獲取View的寬高

getMeasuredHeight(),測量后的高度,實際高度。獲取測量完的高度,只要在onMeasure方法執行完,就可以用它獲取到寬高,在自定義控件內部多使用這個使用view.measure(0,0)方法可以主動通知系統去測量,然后就可以直接使用它獲取寬高

getHeight(),顯示的高度。必須在onLayout方法執行完后,才能獲得寬高

8.6.4 measure(0,0)

view.measure(0,0)主動通知系統去測量

View view = new View(context); view.measure(0,0);//等價於下面的代碼 // MeasureSpec.UNSPECIFIED = 0 int widthMeasureSpec = MeasureSpec.makeMeasureSpec(0,MeasureSpec.UNSPECIFIED); int heightMeasureSpec = MeasureSpec.makeMeasureSpec(0,MeasureSpec.UNSPECIFIED); view.measure(widthMeasureSpec,heightMeasureSpec); int measureWidth = view.getMeasuredWidth();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
view.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { int width = view.getWidth(); } }); 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

8.6.5 getMeasuredWidth()與getWidth()的區別

首先getMeasureWidth()方法在measure()過程結束后就可以獲取到了,而getWidth()方法要在layout()過程結束后才能獲取到。

getMeasureWidth()方法中的值是通過setMeasuredDimension()方法來進行設置的,而getWidth()方法中的值則是通過layout(left,top,right,bottom)方法設置的。

getWidth():只有調用了onLayout()方法,getWidth()才賦值,顯示的寬度

getMeasureWidth():獲取測量完的寬度,只要在onMeasure()方法執行完,就可以用它獲取到高度,實際的寬度

8.6.6 getHeight()和getMeasuredHeight()的區別

getMeasuredHeight():獲取測量完的高度,只要在onMeasure方法執行完,就可以用它獲取到寬高,在自定義控件內部多使用這個。使用view.measure(0,0)方法可以主動通知系統去測量,然后就可以直接使用它獲取寬高

getHeight():必須在onLayout方法執行完后,才能獲得寬高

view.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() { @Override public void onGlobalLayout() { headerView.getViewTreeObserver().removeGlobalOnLayoutListener(this); int headerViewHeight = headerView.getHeight(); //直接可以獲取寬高 } }); 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

8.6.7 getChildMeasureSpec

getChildMeasureSpec( )的總體思路就是通過其父視圖提供的MeasureSpec參數得到specMode和specSize,並根據計算出來的specMode以及子視圖的childDimension(layout_width和layout_height中定義的)來計算自身的measureSpec,如果其本身包含子視圖,則計算出來的measureSpec將作為調用其子視圖measure函數的參數,同時也作為自身調用setMeasuredDimension的參數,如果其不包含子視圖則默認情況下最終會調用onMeasure的默認實現,並最終調用到setMeasuredDimension,而該函數的參數正是這里計算出來的

getChildMeasureSpec

/** * Does the hard part of measureChildren: figuring out the MeasureSpec to * pass to a particular child. This method figures out the right MeasureSpec * for one dimension (height or width) of one child view. * * The goal is to combine information from our MeasureSpec with the * LayoutParams of the child to get the best possible results. For example, * if the this view knows its size (because its MeasureSpec has a mode of * EXACTLY), and the child has indicated in its LayoutParams that it wants * to be the same size as the parent, the parent should ask the child to * layout given an exact size. * * @param spec The requirements for this view * @param padding The padding of this view for the current dimension and * margins, if applicable * @param childDimension How big the child wants to be in the current * dimension * @return a MeasureSpec integer for the child */ public static int getChildMeasureSpec(int spec, int padding, int childDimension) { int specMode = MeasureSpec.getMode(spec); int specSize = MeasureSpec.getSize(spec); int size = Math.max(0, specSize - padding); int resultSize = 0; int resultMode = 0; switch (specMode) { // Parent has imposed an exact size on us case MeasureSpec.EXACTLY: if (childDimension >= 0) { resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { // Child wants to be our size. So be it. 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. resultSize = size; resultMode = MeasureSpec.AT_MOST; } break; // Parent has imposed a maximum size on us case MeasureSpec.AT_MOST: if (childDimension >= 0) { // Child wants a specific size... so be it 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; } //noinspection ResourceType return MeasureSpec.makeMeasureSpec(resultSize, resultMode); }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87

8.7 measureChild

/** * Ask one of the children of this view to measure itself, taking into * account both the MeasureSpec requirements for this view and its padding. * The heavy lifting is done in getChildMeasureSpec. * * @param child The child to measure * @param parentWidthMeasureSpec The width requirements for this view * @param parentHeightMeasureSpec The height requirements for this view */ protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) { final LayoutParams lp = child.getLayoutParams(); final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft + mPaddingRight, lp.width); final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, mPaddingTop + mPaddingBottom, lp.height); child.measure(childWidthMeasureSpec, childHeightMeasureSpec); }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

8.8 measureChildren

/** * Ask all of the children of this view to measure themselves, taking into * account both the MeasureSpec requirements for this view and its padding. * We skip children that are in the GONE state The heavy lifting is done in * getChildMeasureSpec. * * @param widthMeasureSpec The width requirements for this view * @param heightMeasureSpec The height requirements for this view */ protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) { final int size = mChildrenCount; final View[] children = mChildren; for (int i = 0; i < size; ++i) { final View child = children[i]; if ((child.mViewFlags & VISIBILITY_MASK) != GONE) { measureChild(child, widthMeasureSpec, heightMeasureSpec); } } }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

8.9 measureChildWithMargins

 /** * Ask one of the children of this view to measure itself, taking into * account both the MeasureSpec requirements for this view and its padding * and margins. The child must have MarginLayoutParams The heavy lifting is * done in getChildMeasureSpec. * * @param child The child to measure * @param parentWidthMeasureSpec The width requirements for this view * @param widthUsed Extra space that has been used up by the parent * horizontally (possibly by other children of the parent) * @param parentHeightMeasureSpec The height requirements for this view * @param heightUsed Extra space that has been used up by the parent * vertically (possibly by other children of the parent) */ protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) { final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin + widthUsed, lp.width); final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin + heightUsed, lp.height); child.measure(childWidthMeasureSpec, childHeightMeasureSpec); }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

9. layout 布局

layout

Layout方法中接受四個參數,是由父View提供,指定了子View在父View中的左、上、右、下的位置。父View在指定子View的位置時通常會根據子View在measure中測量的大小來決定。

子View的位置通常還受有其他屬性左右,例如父View的orientation,gravity,自身的margin等等,特別是RelativeLayout,影響布局的因素非常多。

9.1 setFrame

setFrame方法是一個隱藏方法,所以作為應用層程序員來說,無法重寫該方法。該方法體內部通過比對本次的l、t、r、b四個值與上次是否相同來判斷自身的位置和大小是否發生了改變。

如果發生了改變,將會調用invalidate請求重繪。

記錄本次的l、t、r、b,用於下次比對。

如果大小發生了變化,onSizeChanged方法,該方法在大多數View中都是空實現,程序員可以重寫該方法用於監聽View大小發生變化的事件,在可以滾動的視圖中重載了該方法,用於重新根據大小計算出需要滾動的值,以便顯示之前顯示的區域。

9.2 View的layout()

public final void layout(int l, int t, int r, int b) { ..... //設置View位於父視圖的坐標軸 boolean changed = setFrame(l, t, r, b); //判斷View的位置是否發生過變化,看有必要進行重新layout嗎 if (changed || (mPrivateFlags & LAYOUT_REQUIRED) == LAYOUT_REQUIRED) { if (ViewDebug.TRACE_HIERARCHY) { ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_LAYOUT); } //調用onLayout(changed, l, t, r, b); 函數 onLayout(changed, l, t, r, b); mPrivateFlags &= ~LAYOUT_REQUIRED; } mPrivateFlags &= ~FORCE_LAYOUT; ..... }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

9.3 onLayout()

setFrame(l, t, r, b) 設置View位於父視圖的坐標軸

onLayout是ViewGroup用來決定子View擺放位置的,各種布局的差異都在該方法中得到了體現。

onLayout比layout多一個參數,changed,該參數是在setFrame通過比對上次的位置得出是否發生了變化,通常該參數沒有被使用的意義,因為父View位置和大小不變,並不能代表子View的位置和大小沒有發生改變。

int childCount = getChildCount() ; for(int i=0 ;i<childCount ;i++){ View child = getChildAt(i) ; //整個layout()過程就是個遞歸過程 child.layout(l, t, r, b) ; } public final int getMeasuredWidth() { return mMeasuredWidth & MEASURED_SIZE_MASK; } public final int getWidth() { return mRight - mLeft; }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

View中:

public void layout(int l,int t,int r,int b) { ... onLayout ... } //changed 表示是否有新的位置或尺寸 protected void onLayout(boolean changed,int left,int top,int right,int bottom) { //空實現 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

ViewGroup中:

public final void layout(int l,int t,int r,int b) { ... super.layout(l, t, r, b); ... } //changed 表示是否有新的位置或尺寸 protected abstract void onLayout(boolean changed, int l,int t, int r,int b);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

說明:

  • 自定義一個view時,建議重寫onLayout,以設定它的位置。 
    在外部調用時,調用layout(),觸發設定位置。

  • 自定義一個viewGroup時,必須且只能重寫onLayout。 
    需要在設定子view的位置:調用subview.layout(); 觸發

10. draw 繪制

ondraw

draw同樣是由ViewRoot的performTraversals方法發起,它將調用DecorView的draw方法,並把成員變量canvas傳給給draw方法。而在后面draw遍歷中,傳遞的都是同一個canvas。所以android的繪制是同一個window中的所有View都繪制在同一個畫布上。等繪制完成,將會通知WMS把canvas上的內容繪制到屏幕上。

10.1 draw的流程

  1. 繪制背景
  2. 繪制漸變效果(通常不繪制)
  3. 調用onDraw
  4. 調用dispatchDraw
  5. 調用onDrawScrollBars

繪制流程

  • Step 1, draw the background, if needed 繪制背景
  • Step 2, save the canvas’ layers
  • Step 3, draw the content 繪制內容
  • Step 4, draw the children 繪制子view
  • Step 5, draw the fade effect and restore layers
  • Step 6, draw decorations (scrollbars) 對View的滾動條進行繪制

10.2 onDraw()

繪制視圖自身,View用來繪制自身的實現方法,如果我們想要自定義View,通常需要重載該方法。TextView中在該方法中繪制文字、光標和CompoundDrawable,ImageView中相對簡單,只是繪制了圖片

10.3 dispatchDraw(canvas)

  • 先根據自身的padding剪裁畫布,所有的子View都將在畫布剪裁后的區域繪制。
  • 遍歷所有子View,調用子View的computeScroll對子View的滾動值進行計算。
  • 根據滾動值和子View在父View中的坐標進行畫布原點坐標的移動,根據子在父View中的坐標計算出子View的視圖大小,然后對畫布進行剪裁,請看下面的示意圖。
  • dispatchDraw的邏輯其實比較復雜,但是幸運的是對子View流程都采用該方式,而ViewGroup已經處理好了,我們不必要重載該方法對子View進行繪制事件的派遣分發。

用來繪制子View的,遍歷子View然后drawChild(),drawChild()方法實際調用的是子View.draw()方法,ViewGroup類已經為我們實現繪制子View的默認過程,這個實現基本能滿足大部分需求,所以ViewGroup類的子類(LinearLayout,FrameLayout)也基本沒有去重寫dispatchDraw方法

無論是View還是ViewGroup對它們倆的調用順序都是onDraw()->dispatchDraw()

但在ViewGroup中,當它有背景的時候就會調用onDraw()方法,否則就會跳過onDraw()直接調用dispatchDraw();所以如果要在ViewGroup中繪圖時,往往是重寫dispatchDraw()方法。dispatchDraw()方法內部遍歷子view,調用子view的繪制方法來完成繪制工作

在View中,onDraw()和dispatchDraw()都會被調用的,所以我們無論把繪圖代碼放在onDraw()或者dispatchDraw()中都是可以得到效果的,但是由於dispatchDraw()的含義是繪制子控件,所以原則來上講,在繪制View控件時,我們是重新onDraw()函數

在繪制View控件時,需要重寫onDraw()函數,在繪制ViewGroup時,需要重寫dispatchDraw()函數。

在自定義控件public class CircleProgressView extends LinearLayout的時候,如果不設置背景的話setBackground()的話,是不會走onDraw()方法的 
dispatchDraw()繪制具體的內容(圖片和文本)

View中:

public void draw(Canvas canvas) { /* 1. Draw the background 繪制背景 2. If necessary, save the canvas' layers to prepare for fading 如有必要,顏色漸變淡之前保存畫布層(即鎖定原有的畫布內容) 3. Draw view's content 繪制view的內容 4. Draw children 繪制子view 5. If necessary, draw the fading edges and restore layers 如有必要,繪制顏色漸變淡的邊框,並恢復畫布(即畫布改變的內容附加到原有內容上) 6. Draw decorations (scrollbars for instance) 繪制裝飾,比如滾動條 */ ... if (!dirtyOpaque) { drawBackground(canvas); //背景繪制 } // skip step 2 & 5 if possible (common case) 通常情況跳過第2和第5步 ... if (!dirtyOpaque) onDraw(canvas); //調用onDraw dispatchDraw(canvas); //繪制子view onDrawScrollBars(canvas); //繪制滾動條 ... } protected void dispatchDraw(Canvas canvas) { //空實現 } protected void onDraw(Canvas canvas) { //空實現 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

ViewGroup中:

protected void dispatchDraw(Canvas canvas) { ... drawChild(...); //繪制子view ... } protected boolean drawChild(Canvas canvas, View child, long drawingTime) { return child.draw(canvas, this, drawingTime); }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

說明:

  • 自定義一個view時,重寫onDraw。 
    調用view.invalidate(),會觸發onDraw和computeScroll()。前提是該view被附加在當前窗口上view.postInvalidate(); //是在非UI線程上調用的

  • 自定義一個ViewGroup,重寫onDraw。 
    onDraw可能不會被調用,原因是需要先設置一個背景(顏色或圖)。表示這個group有東西需要繪制了,才會觸發draw,之后是onDraw。因此,一般直接重寫dispatchDraw來繪制viewGroup

  • 自定義一個ViewGroup,dispatchDraw會調用drawChild。

11. 畫布的移動和剪裁

11.1 畫布的移動和剪裁1

下面是一個ViewGroup視圖,綠色原點代表其原點,根據padding剪裁畫布后,黃色區域代表其剪裁后的畫布區域,畫布的原點將會移到黃色原點處

view

11.2 畫布的移動和剪裁2

在ViewGroup中,我們放一個TextView,Viewgroup完全滿足TextView的測量大小,給了它合適的顯示區域,也就是layout中設置的位置和它的大小一致。畫布的原點會移動到粉色原點處。此時畫布剪裁為粉色區域這么大。

view

11.3 畫布的移動和剪裁3

如果TextView的內容足夠多,onMeasure的時候我們不理會父View給的參數,直接根據自身的內容來設置大小,但是父View在onLayout的時候分配的位置還是它期望的大小,也就是黑色的邊框,這個時候粉色區域是TextView的大小,但是畫布仍舊是黑色邊框,畫布原點仍舊是粉色原點

view

11.4 畫布的移動和剪裁4

我們為了看到其他的區域文字,對TextView進行了scroll的滾動,這個時候畫布的剪裁大小任然是黑色邊框,但是原點由透明原點根據TextView的滾動值進行移動到了TextView的原點,繪制會從textView的原點進行繪制,但是因為他們超出了畫布的剪裁區域,將不會把數據繪制到畫布上。

view

12. 動畫的繪制

  • 動畫就是讓畫面“動”起來,其原理就是不斷的繪制,但是每次繪制都有區別。
  • 在ViewGroup的drawChild方法中會判斷child是否包含動畫,如果包含,則根據動畫類計算出動畫執行的區域矩形,判斷動畫是否啟動了,啟動了就獲取動畫當前的值,例如位移值等等。然后根據值對畫布進行剪裁調整,執行子View的draw進行繪制。
  • 判斷動畫是否結束,如果沒有,則調用invalidate再次請求繪制。

12.1 動畫繪制1

在ViewGroup中有一個紅色的子View,將執行一個位移動畫,位移動畫將執行到A的位置,那么將會先根據動畫參數計算出A位置的矩形大小。

animation

12.2 動畫繪制2

剪裁畫布區域A的位置,把該畫布交給子View,讓其執行draw方法,那么View的內容都將會被繪制到A區域,而V所處的位置並沒有發生變化。View繼續移動到B位置。

animation

12.3 動畫繪制3

此時剪裁畫布區域B的位置,把該畫布交給子View,讓其執行draw方法,那么View的內容都將會被繪制到B區域,但是B區域有一部分已經超過了ViewGroup畫布區域。超出的地方雖然被繪制了,但是不會添加到畫布上,也就不會顯示出來

animation

參考鏈接


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM