Android的自定義View及View的繪製流程

目標:實現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()來提供更加準確和有效的測量。

相關文章
相關標籤/搜索