目標:實現Android中的自定義View,為理清楚Android中的View繪制流程“鋪路”。
想法很簡單:從一個簡單例子着手開始編寫自定義View,對ViewGroup、View類中與繪制View相關的方法解析,並最終弄清楚View的繪制流程。
View類代表用戶界面組件的基本構建塊;View在屏幕上占據一個矩形區域,並負責繪制和事件處理;View是用於創建交互式用戶界面組件(按鈕、文本等)的基礎類。
ViewGroup是View的子類,是所有布局的父類,是一個可以包含其他View或者ViewGroup並定義它們的布局屬性一個看不見的容器。
實現一個自定義View,通常會覆寫一些Framework層上在所有View上調用的標准方法。
View在Activity中顯示出來,要經歷測量、布局和繪制三個步驟,分別對應三個動作:measure、layout和draw。
測量:onMeasure()決定View的大小;
布局:onLayout()決定View在ViewGroup中的位置;
繪制:onDraw()決定繪制這個View。
自定義View的步驟:
1. 自定義View的屬性;
2. 在View的構造方法中獲得自定義的屬性;
3. 重寫onMeasure(); --> 並不是必須的,大部分的時候還需要覆寫
4. 重寫onDraw();
自定義屬性:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- 定義字體、字體顏色、字體大小3個屬性,format指該屬性的取值類型 -->
<attr name="titleText" format="string" />
<attr name="titleTextColor" format="color" />
<attr name="titleTextSize" format="dimension" />
<declare-styleable name="CustomTitleView">
<attr name="titleText" />
<attr name="titleTextColor" />
<attr name="titleTextSize" />
</declare-styleable>
</resources>
使用自定義屬性:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:custom="http://schemas.android.com/apk/res/com.spt.designview"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.spt.designview.DesignViewActivity" >
<!-- 需要引入命名空間:xmlns:custom="http://schemas.android.com/apk/res/com.spt.designview" -->
<com.spt.designview.view.CustomTitleView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerInParent="true"
android:padding="100dp"
custom:titleText="3712"
custom:titleTextColor="#ff0000"
custom:titleTextSize="40sp" />
</RelativeLayout>
上述使用的custom:titleText,取值上文的命名空間。
View有四種形式的構造方法,其中4個參數的構造方法出現在API 21之后;我們一般只需要覆寫其他的3個構造方法即可。參數不同對應不同的創建方式;比如1個參數的構造方法通常是通過代碼初始化控件時使用的;2個參數的構造方法通常對應.xml布局文件中控件被映射成對象時調用(解析屬性);通常讓上述2種構造方式調用3個參數的構造方法,然后在該方法中進行初始化操作。
public CustomTitleView(Context context) {
this(context, null);
}
/**
* <默認構造函數> 布局文件調用的是兩個參數的構造方法
*/
public CustomTitleView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
代碼中獲取自定義屬性:
/**
* <默認構造函數> 獲得自定義屬性
*/
public CustomTitleView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
// R.styleable.CustomTitleView來自attrs.xml文件
TypedArray typedArray = context.getTheme().obtainStyledAttributes(
attrs, R.styleable.CustomTitleView, defStyleAttr, 0);
int n = typedArray.getIndexCount();
for (int i = 0; i < n; i++) {
int attr = typedArray.getIndex(i);
switch (attr) {
case R.styleable.CustomTitleView_titleText:
mTitleText = typedArray.getString(attr);
break;
case R.styleable.CustomTitleView_titleTextColor:
// 默認設置為黑色
mTitleTextColor = typedArray.getColor(attr, Color.BLACK);
break;
case R.styleable.CustomTitleView_titleTextSize:
// 默認設置為16sp,TypeValue將sp轉為px
mTitleTextSize = typedArray.getDimensionPixelSize(attr,
(int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_SP, 16,
getResources().getDisplayMetrics()));
default:
break;
}
}
typedArray.recycle();
代碼中引用的R.styleable.CustomTitleView就是attrs.xml中定義的名稱:http://blog.csdn.net/dalancon/article/details/9701855
繪制時鍾的Demo:http://blog.csdn.net/To_be_Designer/article/details/48500801
一般會在自定義View中引入自定義的屬性。
什么時候調用onMeasure方法?
當控件的父元素正要放置該控件時調用View的onMeasure()。ViewGroup會問子控件View一個問題:“你想要用多大地方啊?”,然后傳入兩個參數——widthMeasureSpec和heightMeasureSpec;這兩個參數指明控件可獲得的空間以及關於這個空間描述的元數據。更好的方法是傳遞子控件View的高度和寬度到setMeasuredDimension()里,直接告訴父控件需要多大地方放置子控件。在onMeasure()的最后都會調用setMeasuredDimension();如果不調用,將會由measure()拋出一個IllegalStateException()。
自定義View的onMeasure(): --> 測量View的大小
系統幫我們測量的高度和寬度都是MATCH_PARENT;當我們設置明確的寬度和高度時,系統測量的結果就是我們設置的結果。
當設置為WRAP_CONTENT,或者是MATCH_PARENT時,系統測量的結果就是MATCH_PARENT的長度。
當設置為WRAP_CONTENT時,而有需要進行自我測量時,就需要覆寫onMeasure()。
重寫之前先了解MeasureSpec的specMode,一共三種類型:
EXACTLY:一般是設置為明確的值或者是精確的值,Parent為子View決定了一個絕對尺寸,子View會被賦予這個邊界限制,不管子View自己想要多大;
AT_MOST:表示子布局限制在一個最大值內,代表最大可獲取的空間;代表子View可以是任意的大小,但是有一個絕對尺寸上限;
UNSPECIFIED:表示子布局想要多大就多大,很少使用;代表Parent沒有對子View強加任何限制,子View可以是它想要的任何尺寸;
public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
/**
* Measure specification mode: 父控件對子View的尺寸無任何要求
*/
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
/**
* Measure specification mode: 父控件對子View有精確的尺寸要求
*/
public static final int EXACTLY = 1 << MODE_SHIFT;
/**
* Measure specification mode: 父控件對子View有最大尺寸要求
*/
public static final int AT_MOST = 2 << MODE_SHIFT;
/**
* Creates a measure specification based on the supplied size and mode.
*/
public static int makeMeasureSpec(int size, int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
/**
* Extracts the mode from the supplied measure specification.
*/
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
/**
* Extracts the size from the supplied measure specification.
*/
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
static int adjust(int measureSpec, int delta) {
final int mode = getMode(measureSpec);
if (mode == UNSPECIFIED) {
// No need to adjust size for UNSPECIFIED mode.
return makeMeasureSpec(0, UNSPECIFIED);
}
int size = getSize(measureSpec) + delta;
if (size < 0) {
Log.e(VIEW_LOG_TAG, "MeasureSpec.adjust: new size would be negative! (" + size +
") spec: " + toString(measureSpec) + " delta: " + delta);
size = 0;
}
return makeMeasureSpec(size, mode);
}
/**
* Returns a String representation of the specified measure
* specification.
*/
public static String toString(int measureSpec) {
int mode = getMode(measureSpec);
int size = getSize(measureSpec);
StringBuilder sb = new StringBuilder("MeasureSpec: ");
if (mode == UNSPECIFIED)
sb.append("UNSPECIFIED ");
else if (mode == EXACTLY)
sb.append("EXACTLY ");
else if (mode == AT_MOST)
sb.append("AT_MOST ");
else
sb.append(mode).append(" ");
sb.append(size);
return sb.toString();
}
}
下面針對onMeasure()進行測量:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getMode(widthMeasureSpec);
int width = 0;
onMeasure()中傳入的兩個參數值,表示的是指明控件可獲得的空間以及關於這個空間描述的元數據,也就是父容器對該子View的一種期望值或者一種要求。
上述的三種類型和我們.xml文件中的布局設置有什么關系?明確地說,和fill_parent、match_parent或者wrap_content有什么關系?
當設置為wrap_content時,傳給onMeasure()的是AT_MOST, 表示子view的大小最多是多少,這樣子View會根據這個上限來設置自己的尺寸。
當設置為fill_parent或者match_parent時,傳給子View的onMeasure()的是EXACTLY,因為子view會占據剩余容器的空間,所以它大小是確定的。
當子View的大小設置為精確值時,傳給子View的onMeasure()的是EXACTLY,而MeasureSpec的UNSPECIFIED模式目前還沒有發現在什么情況下使用。
D/CustomTitleView(13652): onMeasure::MeasureSpec.AT_MOST D/CustomTitleView(13652): onMeasure::MeasureSpec.EXACTLY D/CustomTitleView(13652): onMeasure::MeasureSpec.AT_MOST D/CustomTitleView(13652): onMeasure::MeasureSpec.EXACTLY D/CustomTitleView(13652): onDraw::getMeasuredWidth()=30; getMeasuredHeight()=74 D/CustomTitleView(13652): onDraw::getWidth()=161; getHeight()=74 D/CustomTitleView(13652): onMeasure::MeasureSpec.AT_MOST D/CustomTitleView(13652): onMeasure::MeasureSpec.EXACTLY D/CustomTitleView(13652): onDraw::getMeasuredWidth()=30; getMeasuredHeight()=74 D/CustomTitleView(13652): onDraw::getWidth()=161; getHeight()=74
為什么會多次調用onMeasure()?
測試結果如下:

默認情況下,match_parent和wrap_content給出的size值時一樣的,都是填充剩余空間。
此處有一個問題:為什么.xml文件中設置為wrap_content時,內容布局會全覆蓋整個界面?
解決辦法如下:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width;
int height;
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
Log.d(TAG, "onMeasure::widthMode=" + widthMode + "; widthSize="
+ widthSize);
Log.d(TAG, "onMeasure::heightMode=" + heightMode + "; heightSize="
+ heightSize);
if (widthMode == MeasureSpec.EXACTLY) {
width = widthSize;
} else {
width = (int) (getPaddingLeft() + mBound.width() + getPaddingRight());
}
if (heightMode == MeasureSpec.EXACTLY) {
height = heightSize;
} else {
height = (int) (getPaddingTop() + mBound.height() + getPaddingBottom());
}
setMeasuredDimension(width, height);
}
如果.xml文件中寫入的是wrap_content,則計算顯示全部文本內容所需要的空間大小,實現展示全部內容。
總結如下:

當View對象的measure()返回時,它的getMeasureWidth()和getMeasuredHeight()值被設置好了,並且它的子孫的值也被設置好了。
注意:一個Parent可能會不止一次地對子View調用measure()。比如,第一遍的時候,一個Parent可能測量它的每一個孩子,並沒有指定尺寸,parent只是為了發現它們想要多大;如果第一遍之后得知,所有孩子的無限制的尺寸總和太大或者太小,Parent會再次對它的孩子調用measure(),這個時候Parent會設定規則,介入這個過程,使用實際值(讓孩子自由發展不成,於是家長介入)。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Log.d(TAG, "onDraw::getMeasuredWidth()=" + getMeasuredWidth()
+ "; getMeasuredHeight()=" + getMeasuredHeight());
Log.d(TAG, "onDraw::getWidth()=" + getWidth() + "; getHeight()="
+ getHeight());
mPaint.setColor(Color.YELLOW);
// 繪制背景(一個矩形框),長度為getMeasuredWidth(),高度為:getMeasuredHeight()
canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), mPaint);
mPaint.setColor(mTitleTextColor);
// 繪制文字
canvas.drawText(mTitleText, getWidth() / 2 - mBound.width() / 2,
getHeight() / 2 + mBound.height() / 2, mPaint);
/**
* getMeasuredWidth()和getWidth()有什么區別?上述輸出結構相同,都是300(200dp)和150(100dp)
* 什么時候上述兩種方法返回不同結果?
*/
}
onDraw()繪制View,讓UI界面顯示出來。
View的measure()用final關鍵詞修飾,無法實現覆寫;在measure()中調用了onMeasure(),子類可以覆寫onMeasure()來提供更加准確和有效的測量。

