Android自定義控件知識探索——View的測量模式

KandyReiman 8年前發布 | 14K 次閱讀 安卓開發 Android開發 移動開發

一個Android開發者總會遇到自定義控件的問題。要學會自定義控件的開發,最好的方法是將要用到的知識點一個個掌握。當掌握這些分散的知識點就意味著寫一個自定義控件會變得容易。本篇文章是對View的測量的探究。

概念

View的測量主要掌握三種測量模式:

貼上源碼:

/**
         * Measure specification mode: The parent has not imposed any constraint
         * on the child. It can be whatever size it wants.
         */
        public static final int UNSPECIFIED = 0 << MODE_SHIFT;

        /**
         * Measure specification mode: The parent has determined an exact size
         * for the child. The child is going to be given those bounds regardless
         * of how big it wants to be.
         */
        public static final int EXACTLY     = 1 << MODE_SHIFT;

        /**
         * Measure specification mode: The child can be as large as it wants up
         * to the specified size.
         */
        public static final int AT_MOST     = 2 << MODE_SHIFT;

這里的測量是對View的width和height進行測量。

UNSPECIFIED:未指定測量模式。View大小不確定,想要多大有多大。

EXACTLY: 精確值模式。當控件的width和height設置為具體值或者match_parent時就是這個模式。

AT_MOST:最大值模式。父布局決定子布局大小(例如:父布局width或者height設置一個默認的精確值,子布局設置為wrap_content。此時子布局的最大width或者height就是父布局的width或者height)。使用這種測量模式的View,設置的一定是wrap_content。

測試

接下來通過具體的代碼來測試三種測量模式使用的場景:

準備工作:新建一個View。在onMesure()中寫測量的代碼。

public class TestMesureView extends View {

    public TestMesureView(Context context) {
        this(context, null);
    }

    public TestMesureView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public TestMesureView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int width = 0;
        int height = 0;

        int widthMode = getMode(widthMeasureSpec);
        int heightMode = getMode(heightMeasureSpec);

        /** 測量width **/
        width = getReallySize(widthMode,widthMeasureSpec);
        /** 測量height **/
        height = getReallySize(heightMode,heightMeasureSpec);

        Log.i("really width mode",logMode(widthMode));
        Log.i("really width",String.valueOf(width));

        Log.i("really split","---------------------------");

        Log.i("really height mode",logMode(heightMode));
        Log.i("really height",String.valueOf(height));
        setMeasuredDimension(width, height);
    }

    /**
     * 獲取測量模式
     * @param sizeMeasureSpec
     * @return
     */
    private int getMode(int sizeMeasureSpec){
        return MeasureSpec.getMode(sizeMeasureSpec);
    }

    /**
     * 通過測量模式獲取真正的Size
     * @param mode
     * @param sizeMeasureSpec
     * @return
     */
    private int getReallySize(int mode,int sizeMeasureSpec){
        int specSize = 0;
        switch (mode){
            case MeasureSpec.AT_MOST:
            case MeasureSpec.EXACTLY:
                specSize = MeasureSpec.getSize(sizeMeasureSpec);
                break;
            case MeasureSpec.UNSPECIFIED:
                specSize = sizeMeasureSpec;
                break;
        }
        return specSize;
    }

    private String logMode(int mode){
        switch (mode){
            case MeasureSpec.AT_MOST:
                return "AT_MOST";
            case MeasureSpec.EXACTLY:
                return "EXACTLY";
            case MeasureSpec.UNSPECIFIED:
                return "UNSPECIFIED";
        }
        return "";
    }

}

如上代碼:

getMode() : 獲取測量模式的方法,核心方法為 MeasureSpec.getMode(sizeMeasureSpec); 將onMeasure(int widthMeasureSpec, int heightMeasureSpec)。中兩個參數分別傳入就可分別得到width的測量模式和height的測量模式。

getReallySize(): 獲取測量到的值的方法。核心方法為 MeasureSpec.getSize(sizeMeasureSpec);將onMeasure(int widthMeasureSpec, int heightMeasureSpec)。中兩個參數分別傳入就可分別得到width的真實大小和height的真實大小。

1、EXACTLY

a、將layout_width,layout_height 都設為 match_parent。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <com.mg.axe.androiddevelop.view.TestMesureView
        android:background="#33ee33"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</LinearLayout>
10-10 22:25:21.271 8207-8207/com.mg.axe.androiddevelop I/really width mode: EXACTLY
10-10 22:25:21.271 8207-8207/com.mg.axe.androiddevelop I/really width: 1080
10-10 22:25:21.271 8207-8207/com.mg.axe.androiddevelop I/really split: ---------------------------
10-10 22:25:21.271 8207-8207/com.mg.axe.androiddevelop I/really height mode: EXACTLY
10-10 22:25:21.271 8207-8207/com.mg.axe.androiddevelop I/really height: 1860

運行結果

分析Log和截圖:

通過運行結果可以看到view充滿整個屏幕。

分析Log可以知道,兩者的測量模式都是 EXACTLY

手機的分辨率為1920*1080 , width為1080 , height為1860(因為有狀態欄所以不是1920)

b、指定精確大小,將layout_width,layout_height 都設為 100dp。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <com.mg.axe.androiddevelop.view.TestMesureView
        android:background="#33ee33"
        android:layout_width="100dp"
        android:layout_height="100dp" />

</LinearLayout>
10-11 00:13:23.511 763-763/com.mg.axe.androiddevelop I/really width mode: EXACTLY
10-11 00:13:23.511 763-763/com.mg.axe.androiddevelop I/really width: 300
10-11 00:13:23.511 763-763/com.mg.axe.androiddevelop I/really split: ---------------------------
10-11 00:13:23.511 763-763/com.mg.axe.androiddevelop I/really height mode: EXACTLY
10-11 00:13:23.511 763-763/com.mg.axe.androiddevelop I/really height: 300

運行結果

分析:

分析Log可以知道,兩者的測量模式都是 EXACTLY

獲取到的width和height都為 300. (系統測量會將單位轉為px)

2、AT_MOST

a、父布局將layout_width,layout_height 都設為 match_parent

將子布局的layout_width,layout_height 都設為 wrap_content

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <com.mg.axe.androiddevelop.view.TestMesureView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#33ee33"/>
</LinearLayout>
10-11 01:27:32.656 29513-29513/com.mg.axe.androiddevelop I/really width mode: AT_MOST
10-11 01:27:32.656 29513-29513/com.mg.axe.androiddevelop I/really width: 1080
10-11 01:27:32.656 29513-29513/com.mg.axe.androiddevelop I/really split: ---------------------------
10-11 01:27:32.656 29513-29513/com.mg.axe.androiddevelop I/really height mode: AT_MOST
10-11 01:27:32.656 29513-29513/com.mg.axe.androiddevelop I/really height: 1860

分析:

子布局的寬高測量模式都為: AT_MOST

父布局的layout_width和layout_height都為match_parent,父布局的寬高約為屏幕的寬高。

子布局的layout_width和layout_height都為wrap_content,子布局大小不固定,但是最大值受父布局大小影響。這種情況的測量模式就是 AT_MOST

b、將父布局設置為指定大小,需要測量的布局將layout_width,layout_height 都設為 wrap_content

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="100dp"
    android:layout_height="100dp"
    android:orientation="vertical">
    <com.mg.axe.androiddevelop.view.TestMesureView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#33ee33" />
</LinearLayout>
10-11 01:30:41.126 2423-2423/com.mg.axe.androiddevelop I/really width mode: AT_MOST
10-11 01:30:41.126 2423-2423/com.mg.axe.androiddevelop I/really width: 300
10-11 01:30:41.126 2423-2423/com.mg.axe.androiddevelop I/really split: ---------------------------
10-11 01:30:41.126 2423-2423/com.mg.axe.androiddevelop I/really height mode: AT_MOST
10-11 01:30:41.126 2423-2423/com.mg.axe.androiddevelop I/really height: 300

分析:

這種情況和上面a測試的結論一樣。子布局大小不固定,但是最大值受父布局大小影響。這種情況的測量模式就是 EXACTLY

c、測試出一種特殊的情況

當父布局是RelativeLayout,子布局的layout_width,layout_height 都設為 wrap_content時,子布局的width測量模式為EXACTLY

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="100dp"
    android:layout_height="100dp"
    android:orientation="vertical">


    <com.mg.axe.androiddevelop.view.TestMesureView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#33ee33" />
</RelativeLayout>
10-11 09:05:26.970 14275-14275/com.mg.axe.androiddevelop I/really width mode: EXACTLY
10-11 09:05:26.970 14275-14275/com.mg.axe.androiddevelop I/really width: 300
10-11 09:05:26.970 14275-14275/com.mg.axe.androiddevelop I/really split: ---------------------------
10-11 09:05:26.970 14275-14275/com.mg.axe.androiddevelop I/really height mode: AT_MOST
10-11 09:05:26.970 14275-14275/com.mg.axe.androiddevelop I/really height: 300

分析:

我暫時也不知道子View的寬的測量模式是EXACTLY。這應該是一種特殊情況。

這里再次做提醒:如果這個View的測量模式為AT_MOST,這個View一定設置了wrap_content

3、UNSPECIFIED

a、添加父布局scrollview,將測試的view放在里面

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent">

    <com.mg.axe.androiddevelop.view.TestMesureView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#33ee33"/>
    </ScrollView>
</LinearLayout>
10-11 01:18:23.566 15113-15113/com.mg.axe.androiddevelop I/really width mode: AT_MOST
10-11 01:18:23.566 15113-15113/com.mg.axe.androiddevelop I/really width: 1080
10-11 01:18:23.566 15113-15113/com.mg.axe.androiddevelop I/really split: ---------------------------
10-11 01:18:23.566 15113-15113/com.mg.axe.androiddevelop I/really height mode: UNSPECIFIED
10-11 01:18:23.566 15113-15113/com.mg.axe.androiddevelop I/really height: 0

分析:

這里我們只要分析height就行了,這種情況下 父布局ScrollView的子view的高度是不固定的,想要多大就可多大。所以這里height的測量模式為 UNSPECIFIED

實際應用

1、先測量再繪制

在寫自定義控件時,涉及到測量繪制的。一般是先測量再繪制。

2、測量方法

這個是上面寫的方法。是參照源碼寫的。

private int getReallySize(int mode,int sizeMeasureSpec){
        int specSize = 0;
        switch (mode){
            case MeasureSpec.AT_MOST:
            case MeasureSpec.EXACTLY:
                specSize = MeasureSpec.getSize(sizeMeasureSpec);
                break;
            case MeasureSpec.UNSPECIFIED:
                specSize = sizeMeasureSpec;
                break;
        }
        return specSize;
    }

在View的源碼中有一個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;
    }
}

3、測量完畢之后一定要調用setMeasuredDimension(width, height);

要調用setMeasuredDimension或者super.onMeasure來設置自身的mMeasuredWidth和mMeasuredHeight,否則,就會拋出異常.

 

來自:http://www.jianshu.com/p/85548a440cd2

 

 本文由用戶 KandyReiman 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!