自定義View機制詳解
Activity、Window、PhoneWindow、Decorview、Rootview關係
應用的介面怎麼顯示出來的?
Activity包含一個window,通過getwindow()可以得到抽象的Window類,代表一個視窗。window持有一個DecorView,是檢視的根佈局。Decorview繼承自Framelayout,內部有垂直方向的Linearlayout。上面是標題欄ActionBar,下面是內容欄contentview。
圖解層級關係:

視窗層級關係圖
Activity內部屬性,這裡可見有mWindow,mDecor,有mActionBar:

image.png
setContentView()過程:
1.建立一個DecorView的物件mDecor,該mDecor物件將作為整個應用視窗的根檢視。
2.依據Feature等style theme建立不同的視窗修飾佈局檔案,並且通過findViewById獲取Activity佈局檔案該存放的地方(視窗修飾佈局檔案中id為content的FrameLayout)。
3.將Activity的佈局檔案新增至id為content的FrameLayout內。
public void setContentView(int layoutResID) { getWindow().setContentView(layoutResID); initWindowDecorActionBar(); } public void setContentView(View view) { getWindow().setContentView(view); initWindowDecorActionBar(); } public void setContentView(View view, ViewGroup.LayoutParams params) { getWindow().setContentView(view, params); initWindowDecorActionBar(); }
View渲染機制
自定義view建構函式呼叫:
public class CustomView extends View{ /** * 建構函式1 * @param context */ public CustomView(Context context) { super(context); } /** * 建構函式2 * @param context */ public CustomView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); } /** * 建構函式3 * @param context */ public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } }
1.直接new一個CustomView時,呼叫建構函式1。
2.在佈局中引入CustomView時,呼叫建構函式2;並且此時可以通過context.obtainStyledAttributes()獲取自定義屬性值。
3.建構函式3中,defStyleAttr用來給View提供一個基本的style,需要建構函式1和2主動呼叫才會呼叫。
獲取style屬性:
public TypedArray obtainStyledAttributes (AttributeSet set, int[] attrs, int defStyleAttr, int defStyleRes)
onMeasure講解
View繪製出來需要知道自己的寬高是多少,所以要先進行測量尺寸。
從門縫裡面看世界,那就從View的內部類MeasureSpec測量類去學:
public static class MeasureSpec { private static final int MODE_SHIFT = 30; private static final int MODE_MASK= 0x3 << MODE_SHIFT; /** @hide */ @IntDef({UNSPECIFIED, EXACTLY, AT_MOST}) @Retention(RetentionPolicy.SOURCE) public @interface MeasureSpecMode {} /** * Measure specification mode: The parent has not imposed any constraint * on the child. It can be whatever size it wants. */ public static final int UNSPECIFIED = 0 << MODE_SHIFT; /** * Measure specification mode: The parent has determined an exact size * for the child. The child is going to be given those bounds regardless * of how big it wants to be. */ public static final int EXACTLY= 1 << MODE_SHIFT; /** * Measure specification mode: The child can be as large as it wants up * to the specified size. */ public static final int AT_MOST= 2 << MODE_SHIFT; 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 makeSafeMeasureSpec(int size, int mode) { if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) { return 0; } return makeMeasureSpec(size, mode); } @MeasureSpecMode public static int getMode(int measureSpec) { //noinspection ResourceType return (measureSpec & MODE_MASK); } public static int getSize(int measureSpec) { return (measureSpec & ~MODE_MASK); } }
測量模式:
UNSPECIFIED EXACTLY AT_MOST
為了認準測量模式的對應方式,我寫了一個簡單測試類:
public class CustomView extends View{ public CustomView(Context context) { super(context); } public CustomView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); } public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int widthMode = MeasureSpec.getMode(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); switch (widthMode){ case MeasureSpec.UNSPECIFIED: Log.e("TAG","widthMode " + "UNSPECIFIED"); break; case MeasureSpec.AT_MOST: Log.e("TAG","widthMode " + "AT_MOST"); break; case MeasureSpec.EXACTLY: Log.e("TAG","widthMode " + "EXACTLY"); break; } Log.e("TAG","widthSize " + MeasureSpec.getSize(widthMeasureSpec)); switch (heightMode){ case MeasureSpec.UNSPECIFIED: Log.e("TAG","heightMode " + "UNSPECIFIED"); break; case MeasureSpec.AT_MOST: Log.e("TAG","heightMode " + "AT_MOST"); break; case MeasureSpec.EXACTLY: Log.e("TAG","heightMode " + "EXACTLY"); break; } Log.e("TAG","heightSize " + MeasureSpec.getSize(heightMeasureSpec)); } }
測試結果:
佈局中寬高均為 match_parent : 測量模式為 EXACTLY

image.png
佈局中寬高均為 wrap_content : 測量模式為 AT_MOST

image.png
佈局中寬高均為 200dp (固定數值): 測量模式為 EXACTLY

image.png
- UNSPECIFIED 父容器沒有對當前View有任何限制,可以隨便用空間,老爸的卡隨便刷的富二代
- EXACTLY 父容器測量的值是多少,那麼這個view的大小就是這個specSize,毫不討價還價
- AT_MOST 父容器給定一個子view的最大尺寸,大小在這個值範圍以內,具體是多少看子view的表現
測量完成:
測量完成回撥onMeasure(int widthMeasureSpec, int heightMeasureSpec)方法。
那麼這兩個名字長長的變數是什麼呢?就是測量出的寬和高的資訊。
回到MeasureSpec類分析,一個Int有32位,用前2位表示SpecMode ,2位數有四種表示方法了,00,01,11分別表示上面的模式順序。後30位表示SpecSize。那我們是不是獲取測量模式和尺寸都要自己使用位移計算呢?不用的,MeasureSpec類已經有了,自帶了拆分和打包方法。

image.png
public static int makeMeasureSpec(int size, int mode) { if (sUseBrokenMakeMeasureSpec) { return size + mode; } else { return (size & ~MODE_MASK) | (mode & MODE_MASK); } } public static int getMode(int measureSpec) { return (measureSpec & MODE_MASK); } public static int getSize(int measureSpec) { return (measureSpec & ~MODE_MASK); }
獲取測量模式和測量大小:
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int widthMode = MeasureSpec.getMode(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); }
現在,我們要寫一個正方形ImageView,使用setMeasuredDimension()自己重設測量值,讓高度值也等於寬度值:
public class SquareImageView extends AppCompatImageView{ public SquareImageView(Context context) { super(context); } public SquareImageView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(widthMeasureSpec,widthMeasureSpec); } }
效果:

image.png
onDraw機制:
將view繪製到螢幕上有以下幾步:
- 繪製背景 background.draw(sanvas)
- 繪製自己 onDraw
- 繪製children dispatchDraw (ViewGroup才有,View沒有)
- 繪製滾動條 onDrawScrollBars
View繪製機制:
Android應用程式呼叫 SurfaceFlinger 服務把經過測量、佈局和繪製後的Surface渲染到顯示螢幕上。
Android目前有兩種繪製模型: 基於軟體的繪製模型和硬體加速的繪製模型 。
在基於軟體的繪製模型下,CPU主導繪圖,檢視按照兩個步驟繪製:
-
讓View層次結構失效
-
繪製View層次結構
當應用程式需要更新它的部分UI時,都會呼叫內容發生改變的View物件的invalidate()方法。無效(invalidation)訊息請求會在View物件層次結構中傳遞,以便計算出需要重繪的螢幕區域(髒區)。然後,Android系統會在View層次結構中繪製所有的跟髒區相交的區域。不幸的是,這種方法有兩個缺點:
-
繪製了不需要重繪的檢視(與髒區域相交的區域)
-
掩蓋了一些應用的bug(由於會重繪與髒區域相交的區域)
注意:在View物件的屬性發生變化時,如背景色或TextView物件中的文字等,Android系統會自動的呼叫該View物件的invalidate()方法。
在基於硬體加速的繪製模式下,GPU主導繪圖,繪製按照三個步驟繪製:
-
讓View層次結構失效
-
記錄、更新顯示列表
-
繪製顯示列表
這種模式下,Android系統依然會使用invalidate()方法和draw()方法來請求螢幕更新和展現View物件。但Android系統並不是立即執行繪製命令,而是首先把這些View的繪製函式作為繪製指令記錄一個顯示列表中,然後再讀取顯示列表中的繪製指令呼叫OpenGL相關函式完成實際繪製。另一個優化是,Android系統只需要針對由invalidate()方法呼叫所標記的View物件的髒區進行記錄和更新顯示列表。沒有失效的View物件則能重放先前顯示列表記錄的繪製指令來進行簡單的重繪工作。
使用顯示列表的目的是,把檢視的各種繪製函式翻譯成繪製指令儲存起來,對於沒有發生改變的檢視把原先儲存的操作指令重新讀取出來重放一次就可以了,提高了檢視的顯示速度。而對於需要重繪的View,則更新顯示列表,以便下次重用,然後再呼叫OpenGL完成繪製。
硬體加速提高了Android系統顯示和重新整理的速度,但它也不是萬能的,它有三個缺陷:
- 相容性(部分繪製函式不支援或不完全硬體加速)
- 記憶體消耗(OpenGL API呼叫就會佔用8MB,而實際上會佔用更多記憶體)
- 電量消耗(GPU耗電)
自定義View自定義屬性:
在style中declare-styleable中宣告屬性,可自定屬性的種類有

image.png
boolean:設定布林值 color:顏色 dimension:設定尺寸 enum:列舉值 flag:位或運算 float:浮點值 fraction:百分數 integer:整形 reference:指定Theme中資源ID string:字串
舉個栗子來了,現在我們要畫一個圓形,寫一個CustomCicleView,可以在引用到佈局的時候自定義半徑,可選擇紅黃藍顏色之一,圓心文字編號。
按照需求定義屬性:
<declare-styleable name="CustomCicleView"> <attr name="radius" format="integer"/> <attr name="text" format="string"/> <attr name="colorType"> <enum name="yellow" value="0"/> <enum name="green" value="1"/> <enum name="blue" value="2"/> </attr> </declare-styleable>
CustomCicleView類:TypedArray是儲存資源陣列的容器,他可以通過obtaiStyledAttributes()方法創建出來。如果不在使用了,需用recycle()方法把它釋放。通過array.getXX獲取各個對應屬性值。
import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Paint; import android.support.annotation.Nullable; import android.util.AttributeSet; import android.view.View; public class CustomCicleView extends View{ private int defaultSize = 100; private int colorType; private int circleColor; private int radius = defaultSize; private String text = "0"; private int textColor = R.color.colorPrimary; private Paint paint; public CustomCicleView(Context context) { super(context); init(); } public CustomCicleView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); TypedArray array = context.obtainStyledAttributes(attrs,R.styleable.CustomCicleView); colorType = array.getInt(R.styleable.CustomCicleView_colorType,0); radius = array.getInteger(R.styleable.CustomCicleView_radius,defaultSize); text = array.getString(R.styleable.CustomCicleView_text); array.recycle(); init(); } private void init(){ paint = new Paint(); paint.setAntiAlias(true); if(colorType == 0){ circleColor = R.color.orange; }else if(colorType == 1){ circleColor = R.color.Skyblue; }else{ circleColor = R.color.Grassgreen; } paint.setColor(getResources().getColor(circleColor)); paint.setTextSize(60); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.drawCircle(getWidth()/2,getHeight()/2,radius,paint); paint.setColor(getResources().getColor(textColor)); canvas.drawText(text,getMeasuredWidth()/2,getMeasuredHeight()/2,paint); } }
那麼,現在就可以使用該自定義view並設定不同的屬性值了:
<com.example.customview.CustomCicleView android:layout_width="150dp" android:layout_height="150dp" app:radius="120" app:text="1" app:colorType="yellow"/> <com.example.customview.CustomCicleView android:layout_width="150dp" android:layout_height="150dp" app:radius="150" app:text="2" android:layout_margin="20dp" app:colorType="blue"/> <com.example.customview.CustomCicleView android:layout_width="150dp" android:layout_height="150dp" app:radius="100" android:layout_margin="20dp" app:text="3" app:colorType="green"/>
效果圖:

image.png
好了,自定義View的機制就總結完了,該篇文章是自定義View的基礎,磨刀不誤砍柴工,相信看完這篇文章的你對自定義View的知識體系有了全面的認識了吧。這篇文章會繼續完善,繼續更新。想繼續深造的看我的該系列其他自定義View控制元件。祝你早日寫出各種牛逼轟轟的自定義View。
年底了,Android和IOS工作更不好找了,我也回想起去年和前年在寒風凜冽中找工作的心酸,這種環境和壓力下一定要堅持,風雨過後才會更加懂得珍惜。但是市場飽和的是大量的初級開發人員,只要肯花時間學,梳理出自己良好的知識體系,一步一步走向高階,從被公司踢來踢去的小白變成強者!

image.png