秒懂OnMeasure

自定義控件系列:
秒懂OnMeasure
秒懂OnLayout
讓自定義ViewGroup裏的子控件支持Margin
讓自定義ViewGroup支持Padding
自定義ViewGroup的一個綜合實踐 FlowLayout
onDraw
最簡單的自定義View:SwitchView

我感覺之所以寫不好自定義view,是因爲我們瞭解的自定義View的基礎知識知道的太少,但是在瞭解自定義view的基礎知識的過程中,又很容易被源碼帶跑偏,找不到重點,結果是看了很多源碼,雲裏霧裏等於沒看。

很多時候,源碼是很重要,但是不懂適可而止的看源碼,你就陷入了汪洋大海。
例如:初中幾何里老師講了“兩點之間、直線最短”這個公理後,我們就可以做很多幾何題目了,做的過程中還很爽,但是老師沒講“兩點之間、直線最短”這個公理的源碼是什麼,爲什麼“兩點之間、直線最短”,你要想證明這個公理,對於初中生甚至大學生都是不可能解答的,但是這絲毫不影響一個初中生做幾何題目(當然,我記得老師說過,公理不需要證明)

所以這個系列博客採用知識點+應用的模式,有重點,有舉例

當這些結論性的知識點積累到足夠多,很多自定義view,不過就是多個結論的綜合應用+小小邏輯算法,我們怕的不是小小邏輯算法,再繞的算法,多試驗就出來了,但是不懂基本的結論性知識點,就很茫然了

知識點

關於MeasureSpec是什麼,不懂的朋友請先搜索一下,這裏對這個不做解釋。

  1. 如果你的自定義view的寬高只支持MeasureSpec.EXACTLY(即:match_parent和具體的數值),那麼onMeasure方法不需要重寫,因爲View這個基類已經默認實現了

  2. 如果你想支持MeasureSpec.AT_MOST(即:wrap_content),必須重寫onMeasure方法,不然你寫wrap_content和match_parent效果是一樣的(即系統默認返回一個父容器所能給予你的最大尺寸)。

    想想爲什麼,View這個基類,不幫我們實現MeasureSpec.AT_MOST模式呢?
    因爲不同的view,對於自己的MeasureSpec.AT_MOST(包裹內容)有自己特有的計算方式,例如:ImageView的MeasureSpec.AT_MOST,ImageView會根據你設置的圖片,來計算在wrap_content時候的寬高。
    TextView的MeasureSpec.AT_MOST,TextView會根據你設置的文字內容多少,(因爲內容多了可能換行)和你設置的TextSize(字體大了,自然需要更大的寬高)來計算Textview在wrap_content時候的寬高
    FrameLayout在MeasureSpec.AT_MOST模式下,寬度就是所有子view裏面最大的那個View的寬度,高度就是所有子view裏的最大的那個View的高度
    LinearLayout在MeasureSpec.AT_MOST模式下(假設是豎向佈局),寬度是所有View裏最大的那個View的寬度,高度是所有View的高度的總和
    所以View天生支持MeasureSpec.EXACTLY,但是他對於MeasureSpec.AT_MOST是無能爲力的,需要具體的View自己具體實現

  3. 最重要就是這個方法,計算出控件在各種模式下的寬高,通過這個方法設置進去,就好了setMeasuredDimension(width, height);

以下就是View的onMeasure的默認實現(源碼)

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }
    
 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://關鍵在這裏系統對AT_MOST和EXACTLY的處理是一樣的,
        //都是返回父容器的最大尺寸,不信你可以自己打印出來看看
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        return result;
    }

應用

1. 給出支持MeasureSpec.AT_MOST的模板代碼

其實這個代碼是套路代碼,結構是不變的

當寫wrap_content,意思是父佈局不傳給你確定的尺寸,需要這個view自己確定個默認的尺寸,這個尺寸是你自己根據自己的情況計算出來的。

public class CustomView extends View {


    public CustomView(Context context) {
        super(context);
    }

    public CustomView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

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


    
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        // 首先計算一下在AtMost模式下,這個自定義view的寬高,
        // 這裏把計算出來的寬高封裝在了一個Point裏,x爲寬,y爲高
        // 這是模板代碼,你只需要按照自己的實現caculateAtMostSize()的具體邏輯,其餘的不用變
                Point point = caculateAtMostSize(widthMeasureSpec,heightMeasureSpec);


        // 根據 默認寬高、AtMost下的寬高、MeasureSpec測量規格,計算出最終這個view的寬高
        int width = measureSize(0, point.x, widthMeasureSpec);
        int height = measureSize(0, point.y, heightMeasureSpec);

        // 把上面計算出來的寬高作爲參數設置給setMeasuredDimension就ok了
        setMeasuredDimension(width, height);
    }


    /**
     * 通過widthMeasureSpec計算出這個View最終的寬高
     *
     * @param defalut     這個view的默認值,僅僅是爲了支持下UNSPECIFIED模式,但是這個模式其實用不到
     * @param atMostSize  AT_MOST下的尺寸
     * @param measureSpec 測量規格(包含了模式+尺寸)
     * @return
     */
    private int measureSize(int defalut, int atMostSize, int measureSpec) {


        int result = defalut;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        switch (specMode) {
            case MeasureSpec.UNSPECIFIED:
                result = defalut;
                break;
            case MeasureSpec.AT_MOST:
                //在AT_MOST模式下,系統傳來的specSize是一個父容器所能容納的最大值,你這個自定義view計算的尺寸不能大於這個值
                result = Math.min(atMostSize, specSize);
                break;
            case MeasureSpec.EXACTLY:
                result = specSize;
                break;
        }


        return result;
    }


    /**
     * 計算本View在AtMost模式下的寬高
     * 其他代碼都是不用動的,在這裏寫下你特有的邏輯就可以
     * 我這裏只是簡單的返回寬高都是 200
     *
     * @return
     */
        private Point caculateAtMostSize(int widthMeasureSpec, int heightMeasureSpec) {
        //一般情況,寫的自定義view是不需要特別計算這個值的,我會直接給一個默認值
        //但是你是自定義ViewGroup的話,這裏你必須好好寫了
        int width = 200;
        int height = 200;
        return new Point(width, height);
    }











2. 一個自定義ViewGroup支持MeasureSpec.AT_MOST的例子:

效果如圖:
在這裏插入圖片描述

對應的佈局文件時這樣的
在這裏插入圖片描述
注意:這裏僅僅寫支持AT_MOST的代碼,還沒寫onLayout,所以代碼運行是看不到效果的,可以打印log,來看下這個view的寬高是不是正確的

/**
     * 計算本View在AtMost模式下的寬高
     * 其他代碼都是不用動的,在這裏寫下你特有的邏輯就可以
     *
     * @param widthMeasureSpec
     * @param heightMeasureSpec
     * @return
     */
    private Point caculateAtMostSize(int widthMeasureSpec, int heightMeasureSpec) {

        int width = 0;
        int height = 0;

        int count = getChildCount();
        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);
            // 測量一下子控件的寬高
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
            // 獲得子控件的寬高
            int childWidth = child.getMeasuredWidth();
            int childHeight = child.getMeasuredHeight();

            // 因爲我們的自定義View模擬的是豎向的LinearLayout,所以:
            // 控件的寬度爲所有子控件裏,寬度最大的那個view的寬度,
            // 控件高度是所有子空間的高度之和
            width = Math.max(childWidth, width);
            height += childHeight;
        }


        return new Point(width, height);
    }

本例源碼github之CustomView

參考資料:
《Android羣英傳》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章