目標:實現Android中的自定義View,爲理清楚Android中的View繪製流程「鋪路」。html
想法很簡單:從一個簡單例子着手開始編寫自定義View,對ViewGroup、View類中與繪製View相關的方法解析,並最終弄清楚View的繪製流程。java
View類表明用戶界面組件的基本構建塊;View在屏幕上佔據一個矩形區域,並負責繪製和事件處理;View是用於建立交互式用戶界面組件(按鈕、文本等)的基礎類。android
ViewGroup是View的子類,是全部佈局的父類,是一個能夠包含其餘View或者ViewGroup並定義它們的佈局屬性一個看不見的容器。canvas
實現一個自定義View,一般會覆寫一些Framework層上在全部View上調用的標準方法。bash
View在Activity中顯示出來,要經歷測量、佈局和繪製三個步驟,分別對應三個動做:measure、layout和draw。app
測量:onMeasure()決定View的大小;ide
佈局: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()來提供更加準確和有效的測量。