View 的顯示過程
一個 View 經(jīng)過三步重點流程,最終才能顯示到屏幕上。分別是:測量,布局,繪制。
其實很容易理解,一個圖形要想顯示在界面上,首先要進行測量決定大小。然后要進行布局,決定擺放的位置。最后就是進行繪制,用線條和圖形描述出來。
如果要進行自定義 View 的學(xué)習(xí),那么了解這些流程是必須的。
在 Android 中,View 同樣要經(jīng)過以上三個步驟,只是其中的布局通常是由父布局,一個 ViewGroup 來決定,我們主要先來了解一下測量以及繪制的過程。
測量
MeasureSpec
View 的測量是在 onMeasure() 方法中進行。其中 Android 設(shè)計了一個類用來進行測量 ---- MeasureSpec 類,這個類的值是一個 32 位的 int 值,高 2 位表示測量的模式,低 30 位表示測量的大小。
MeasureSpec 的三種模式
其中測量的模式分為以下三種:
EXACTLY
精確值模式,在手動指定了控件的 layout_width 或 layout_heigth 屬性為具體數(shù)值的時候,或者指定為 match_parent 時,系統(tǒng)使用的是 EXACTLY 模式。AT_MOST
最大值模式,在手動指定了控件的 layout_width 或 layout_heigth 屬性為 wrap_content 的時候,控件大小一般隨著控件的子控件或內(nèi)容變化而變化,只要不超過父控件允許的最大尺寸即可。UNSPECIFIED
表示開發(fā)人員可以將視圖按照自己的意愿設(shè)置成任意的大小,沒有任何限制。這種情況比較少見,不太會用到。
View 類模式的 onMeasure() 方法只支持 EXACTLY 模式,所以如果自定義控件的時候不重寫 onMeasure() 方法的話,就只能使用 EXACTLY 模式??丶梢皂憫?yīng)你指定的具體寬高值或者是 match_parent 屬性。但是如果需要 View 支持 wrap_content 屬性,就必須重寫 onMeasure() 方法來指定 wrap_content 模式時的大小。
MeasureSpec 是怎么來的
關(guān)于這個 MeasureSpec 是由父布局傳遞給子布局的布局要求,我通過代碼調(diào)試得到一些信息,我們來看一下。
在一個 1080 x 1920 的手機上,最外層使用一個 LinearLayout , width 和 height 都使用 match_parent,然后包裹了一個自定義的 View 。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.shire.myapplication.MyView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#f2f2" />
</LinearLayout>
此時在 MyView 的 onMeasure() 中我獲取了 widthMeasureSpec 和 heightMeasureSpec ,提取的結(jié)果為:
widthMeasureSpec 的 size 為:1080
heightMeasureSpec 的 size 為:1557
這里的 1080 就是頂層 LinearLayout 充滿屏幕的寬度,而 1557 就是頂層的 LinearLayout 除去狀態(tài)欄高度之后充滿屏幕的高度,由此可以得到 MeasureSpec 是傳過來的是父布局的大小。
但是!如果你對 View 進行了自定義的大小,傳過來的就是你定義的大小。比如上面的 MyView 的 layout_width 更改為 100dp ,那么獲取到的結(jié)果就是:
widthMeasureSpec 的 size 為:300
heightMeasureSpec 的 size 為:1557
至于為什么 100dp 變成 300 這是 dp 轉(zhuǎn) px 的一個過程導(dǎo)致的,詳細的可以看我另一篇文章: Android開發(fā)中dip,dpi,density,px等詳解
分析 onMeasure
通過 MeasureSpec 我們可以獲得測量模式以及大小,我們來看看部分源碼是如何進行測量的。
我們先看 View 中的 onMeasure() 方法:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
在這里通過 getDefaultSize() 來從 MeasureSpec 中獲取相應(yīng)的大小以及模式,最后轉(zhuǎn)換為一個 int 類型給 setMeasuredDimension() 作為參數(shù)進行最后測量的結(jié)果,我們看看這個 getDefaultSize()。
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
可以看到,首先通過 MeasureSpec.getMode() 和 MeasureSpec.getSize() 取出模式和大小,然后判斷模式。
可以看到在 AT_MOST 或 EXACTLY 模式下都是同樣的處理方式,這也說明了上面所說的,View 在默認情況下只支持 EXACTLY 模式,但是如果需要 View 支持 wrap_content 屬性,也就是 AT_MOST,就必須重寫 onMeasure() 方法來指定 AT_MOST 模式時的大小。
重寫 onMeasure
下面我們自定義一個 View 來試試吧,我們先新建一個空的自定義 View 。
package com.shire.myapplication;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
public class MyView extends View {
public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
}
}
我們并沒有修改任何東西。接下來看看 XML 文件,我們添加了一個 myView 并設(shè)置了寬高為 wrap_content 背景是綠色便于觀察控件大小,那么在這個情況下的顯示效果會是怎么樣?
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.shire.myapplication.MyView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#f2f2" />
</LinearLayout>

如圖,雖然我們設(shè)置了 wrap_content 屬性,當時控件依然充滿了父控件,這就是我們上面說的,View 在默認的情況下,是不支持 wrap_content 的,而且在不設(shè)置指定的寬高的情況下會把父控件的寬高傳過來,所以必須要重寫 onMeasure() 方法。
接下來看看應(yīng)該如何重寫 onMeasure() 方法 。根據(jù)源碼的方案,是由 getDefaultSize() 方法來進行測量,然后將結(jié)果給 setMeasuredDimension() 所以我們主要就是自定義一個 “getDefaultSize()” 方法。我們看下具體的代碼。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getSize(widthMeasureSpec),getSize(heightMeasureSpec));
}
我們重寫了 onMeasure() 方法,并自定義了一個測量方法 getSize() ,接下來就是看看 getSize() 中是如何寫的。
private int getSize(int MeasureSpec) {
//初始化一個返回值變量
int result;
//獲得測量模式
int specMode = View.MeasureSpec.getMode(MeasureSpec);
//獲得測量大小
int specSize = View.MeasureSpec.getSize(MeasureSpec);
//判斷模式是否是 EXACTLY
if (specMode == View.MeasureSpec.EXACTLY) {
//如果模式是 EXACTLY 則直接使用specSize的測量大小
result = specSize;
}else{
//如果是其他兩個模式,先設(shè)置一個默認大小值 200
result = 200;
//如果是 AT_MOST 也就是 wrap_content 的話,就取默認值 200 和 specSize 中小的一個為準。
if (specMode == View.MeasureSpec.AT_MOST) {
result = Math.min(result, specSize);
}
}
return result;
}
在上面的代碼中,我們對 AT_MOST 模式時的測量方式進行了處理,接下來看看效果如何。還是同樣的 XML 文件。

可以看到,現(xiàn)在 AT_MOST 模式已經(jīng)生效了,當我們在設(shè)置 wrap_content 的時候不會再填充父布局,而是根據(jù)我們自定義的測量代碼進行測量,用了 200 這個默認值。
至此,對 View 的測量算是簡單的講解完成了,其實總結(jié)一句話,如果的你自定義 View 不需要使用 wrap_content,就不用管 onMeasure() 方法。不然的話,就需要重寫。
繪制
View 的繪制過程是在 onDraw() 方法中,如果你去看源代碼,會發(fā)現(xiàn)這個方法是空的,但是子類可以繼承。想來也正常,每個控件都有自己的表現(xiàn)方式,繪制方法,自然要自己來寫這部分繪制的代碼。接下來我們繼續(xù)使用上面的 MyView 自定義繪制部分的代碼。
在 onDraw() 方法中,傳進來了一個 Canvas 對象,這個對象等于一塊畫布,我們可以在上面作畫,那現(xiàn)在有了畫布,我們還需要一支筆,那就是 Paint 對象。
public class MyView extends View {
//創(chuàng)建一個畫筆對象
private Paint mPaint;
public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
//初始化畫筆對象
mPaint = new Paint();
}
@Override
protected void onDraw(Canvas canvas) {
//設(shè)置畫筆的顏色為藍色
mPaint.setColor(Color.BLUE);
//使用畫筆畫一個矩形
canvas.drawRect(0,0,50,50,mPaint);
//設(shè)置畫筆的顏色的黃色
mPaint.setColor(Color.YELLOW);
//設(shè)置畫筆的字體大小為40
mPaint.setTextSize(40);
//使用畫筆寫出一行字
canvas.drawText("我可是用筆寫的", 0, 80, mPaint);
}
}
最后的效果

這只是簡單的一個例子,一般來說一個控件的繪制過程是相當復(fù)雜的,這個就要根據(jù)自己的情況來自定義了。