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


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



免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM