View 的顯示過(guò)程
一個(gè) View 經(jīng)過(guò)三步重點(diǎn)流程巷燥,最終才能顯示到屏幕上。分別是:測(cè)量号枕,布局缰揪,繪制。
其實(shí)很容易理解葱淳,一個(gè)圖形要想顯示在界面上钝腺,首先要進(jìn)行測(cè)量決定大小。然后要進(jìn)行布局赞厕,決定擺放的位置艳狐。最后就是進(jìn)行繪制,用線條和圖形描述出來(lái)皿桑。
如果要進(jìn)行自定義 View 的學(xué)習(xí)毫目,那么了解這些流程是必須的。
在 Android 中诲侮,View 同樣要經(jīng)過(guò)以上三個(gè)步驟镀虐,只是其中的布局通常是由父布局,一個(gè) ViewGroup 來(lái)決定沟绪,我們主要先來(lái)了解一下測(cè)量以及繪制的過(guò)程刮便。
測(cè)量
MeasureSpec
View 的測(cè)量是在 onMeasure() 方法中進(jìn)行。其中 Android 設(shè)計(jì)了一個(gè)類用來(lái)進(jìn)行測(cè)量 ---- MeasureSpec 類绽慈,這個(gè)類的值是一個(gè) 32 位的 int 值恨旱,高 2 位表示測(cè)量的模式,低 30 位表示測(cè)量的大小坝疼。
MeasureSpec 的三種模式
其中測(cè)量的模式分為以下三種:
EXACTLY
精確值模式窖杀,在手動(dòng)指定了控件的 layout_width 或 layout_heigth 屬性為具體數(shù)值的時(shí)候,或者指定為 match_parent 時(shí)裙士,系統(tǒng)使用的是 EXACTLY 模式。AT_MOST
最大值模式管毙,在手動(dòng)指定了控件的 layout_width 或 layout_heigth 屬性為 wrap_content 的時(shí)候腿椎,控件大小一般隨著控件的子控件或內(nèi)容變化而變化,只要不超過(guò)父控件允許的最大尺寸即可夭咬。UNSPECIFIED
表示開(kāi)發(fā)人員可以將視圖按照自己的意愿設(shè)置成任意的大小啃炸,沒(méi)有任何限制。這種情況比較少見(jiàn)卓舵,不太會(huì)用到南用。
View 類模式的 onMeasure() 方法只支持 EXACTLY 模式,所以如果自定義控件的時(shí)候不重寫(xiě) onMeasure() 方法的話,就只能使用 EXACTLY 模式裹虫≈壮埃控件可以響應(yīng)你指定的具體寬高值或者是 match_parent 屬性。但是如果需要 View 支持 wrap_content 屬性筑公,就必須重寫(xiě) onMeasure() 方法來(lái)指定 wrap_content 模式時(shí)的大小雳窟。
MeasureSpec 是怎么來(lái)的
關(guān)于這個(gè) MeasureSpec 是由父布局傳遞給子布局的布局要求,我通過(guò)代碼調(diào)試得到一些信息匣屡,我們來(lái)看一下封救。
在一個(gè) 1080 x 1920 的手機(jī)上,最外層使用一個(gè) LinearLayout 捣作, width 和 height 都使用 match_parent誉结,然后包裹了一個(gè)自定義的 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>
此時(shí)在 MyView 的 onMeasure() 中我獲取了 widthMeasureSpec 和 heightMeasureSpec 券躁,提取的結(jié)果為:
widthMeasureSpec 的 size 為:1080
heightMeasureSpec 的 size 為:1557
這里的 1080 就是頂層 LinearLayout 充滿屏幕的寬度惩坑,而 1557 就是頂層的 LinearLayout 除去狀態(tài)欄高度之后充滿屏幕的高度,由此可以得到 MeasureSpec 是傳過(guò)來(lái)的是父布局的大小嘱朽。
但是旭贬!如果你對(duì) View 進(jìn)行了自定義的大小,傳過(guò)來(lái)的就是你定義的大小搪泳。比如上面的 MyView 的 layout_width 更改為 100dp 稀轨,那么獲取到的結(jié)果就是:
widthMeasureSpec 的 size 為:300
heightMeasureSpec 的 size 為:1557
至于為什么 100dp 變成 300 這是 dp 轉(zhuǎn) px 的一個(gè)過(guò)程導(dǎo)致的,詳細(xì)的可以看我另一篇文章: Android開(kāi)發(fā)中dip岸军,dpi奋刽,density,px等詳解
分析 onMeasure
通過(guò) MeasureSpec 我們可以獲得測(cè)量模式以及大小艰赞,我們來(lái)看看部分源碼是如何進(jìn)行測(cè)量的佣谐。
我們先看 View 中的 onMeasure() 方法:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
在這里通過(guò) getDefaultSize() 來(lái)從 MeasureSpec 中獲取相應(yīng)的大小以及模式,最后轉(zhuǎn)換為一個(gè) int 類型給 setMeasuredDimension() 作為參數(shù)進(jìn)行最后測(cè)量的結(jié)果方妖,我們看看這個(gè) 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;
}
可以看到,首先通過(guò) MeasureSpec.getMode() 和 MeasureSpec.getSize() 取出模式和大小党觅,然后判斷模式雌澄。
可以看到在 AT_MOST 或 EXACTLY 模式下都是同樣的處理方式,這也說(shuō)明了上面所說(shuō)的杯瞻,View 在默認(rèn)情況下只支持 EXACTLY 模式镐牺,但是如果需要 View 支持 wrap_content 屬性,也就是 AT_MOST魁莉,就必須重寫(xiě) onMeasure() 方法來(lái)指定 AT_MOST 模式時(shí)的大小睬涧。
重寫(xiě) onMeasure
下面我們自定義一個(gè) View 來(lái)試試吧募胃,我們先新建一個(gè)空的自定義 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);
}
}
我們并沒(méi)有修改任何東西畦浓。接下來(lái)看看 XML 文件痹束,我們添加了一個(gè) myView 并設(shè)置了寬高為 wrap_content 背景是綠色便于觀察控件大小,那么在這個(gè)情況下的顯示效果會(huì)是怎么樣宅粥?
<?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>
如圖参袱,雖然我們?cè)O(shè)置了 wrap_content 屬性,當(dāng)時(shí)控件依然充滿了父控件秽梅,這就是我們上面說(shuō)的抹蚀,View 在默認(rèn)的情況下,是不支持 wrap_content 的企垦,而且在不設(shè)置指定的寬高的情況下會(huì)把父控件的寬高傳過(guò)來(lái)环壤,所以必須要重寫(xiě) onMeasure() 方法。
接下來(lái)看看應(yīng)該如何重寫(xiě) onMeasure() 方法 钞诡。根據(jù)源碼的方案郑现,是由 getDefaultSize() 方法來(lái)進(jìn)行測(cè)量,然后將結(jié)果給 setMeasuredDimension() 所以我們主要就是自定義一個(gè) “getDefaultSize()” 方法荧降。我們看下具體的代碼接箫。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getSize(widthMeasureSpec),getSize(heightMeasureSpec));
}
我們重寫(xiě)了 onMeasure() 方法,并自定義了一個(gè)測(cè)量方法 getSize() 朵诫,接下來(lái)就是看看 getSize() 中是如何寫(xiě)的辛友。
private int getSize(int MeasureSpec) {
//初始化一個(gè)返回值變量
int result;
//獲得測(cè)量模式
int specMode = View.MeasureSpec.getMode(MeasureSpec);
//獲得測(cè)量大小
int specSize = View.MeasureSpec.getSize(MeasureSpec);
//判斷模式是否是 EXACTLY
if (specMode == View.MeasureSpec.EXACTLY) {
//如果模式是 EXACTLY 則直接使用specSize的測(cè)量大小
result = specSize;
}else{
//如果是其他兩個(gè)模式,先設(shè)置一個(gè)默認(rèn)大小值 200
result = 200;
//如果是 AT_MOST 也就是 wrap_content 的話剪返,就取默認(rèn)值 200 和 specSize 中小的一個(gè)為準(zhǔn)废累。
if (specMode == View.MeasureSpec.AT_MOST) {
result = Math.min(result, specSize);
}
}
return result;
}
在上面的代碼中,我們對(duì) AT_MOST 模式時(shí)的測(cè)量方式進(jìn)行了處理脱盲,接下來(lái)看看效果如何邑滨。還是同樣的 XML 文件。
可以看到钱反,現(xiàn)在 AT_MOST 模式已經(jīng)生效了掖看,當(dāng)我們?cè)谠O(shè)置 wrap_content 的時(shí)候不會(huì)再填充父布局,而是根據(jù)我們自定義的測(cè)量代碼進(jìn)行測(cè)量面哥,用了 200 這個(gè)默認(rèn)值乙各。
至此,對(duì) View 的測(cè)量算是簡(jiǎn)單的講解完成了幢竹,其實(shí)總結(jié)一句話,如果的你自定義 View 不需要使用 wrap_content恩静,就不用管 onMeasure() 方法焕毫。不然的話蹲坷,就需要重寫(xiě)。
繪制
View 的繪制過(guò)程是在 onDraw() 方法中邑飒,如果你去看源代碼循签,會(huì)發(fā)現(xiàn)這個(gè)方法是空的,但是子類可以繼承疙咸。想來(lái)也正常县匠,每個(gè)控件都有自己的表現(xiàn)方式,繪制方法撒轮,自然要自己來(lái)寫(xiě)這部分繪制的代碼乞旦。接下來(lái)我們繼續(xù)使用上面的 MyView 自定義繪制部分的代碼。
在 onDraw() 方法中题山,傳進(jìn)來(lái)了一個(gè) Canvas 對(duì)象兰粉,這個(gè)對(duì)象等于一塊畫(huà)布,我們可以在上面作畫(huà)顶瞳,那現(xiàn)在有了畫(huà)布玖姑,我們還需要一支筆,那就是 Paint 對(duì)象慨菱。
public class MyView extends View {
//創(chuàng)建一個(gè)畫(huà)筆對(duì)象
private Paint mPaint;
public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
//初始化畫(huà)筆對(duì)象
mPaint = new Paint();
}
@Override
protected void onDraw(Canvas canvas) {
//設(shè)置畫(huà)筆的顏色為藍(lán)色
mPaint.setColor(Color.BLUE);
//使用畫(huà)筆畫(huà)一個(gè)矩形
canvas.drawRect(0,0,50,50,mPaint);
//設(shè)置畫(huà)筆的顏色的黃色
mPaint.setColor(Color.YELLOW);
//設(shè)置畫(huà)筆的字體大小為40
mPaint.setTextSize(40);
//使用畫(huà)筆寫(xiě)出一行字
canvas.drawText("我可是用筆寫(xiě)的", 0, 80, mPaint);
}
}
最后的效果
這只是簡(jiǎn)單的一個(gè)例子焰络,一般來(lái)說(shuō)一個(gè)控件的繪制過(guò)程是相當(dāng)復(fù)雜的,這個(gè)就要根據(jù)自己的情況來(lái)自定義了符喝。