自定義控件那些事兒 ----- 三【量測】

自定義控件實現中,主要包含:屬性的獲取,初始化,量測,佈局和繪製。

這一章節,主要用於實現理解量測過程。


一、模擬ImageView實現量測過程


1,自定義基礎控件實現


public class MImageView extends View {

    /**
     * 圖片文件
     */
    private Bitmap bitmap;

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

    public MImageView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public MImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //繪製內容
        canvas.drawBitmap(bitmap, 0, 0, null);
    }

    /**
     * 設置展示圖片
     *
     * @param bitmap
     */
    public void setBitmap(Bitmap bitmap) {
        this.bitmap = bitmap;
    }


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
       super.onMeasure(widthMeasureSpec, heightMeasureSpec);

    }
}


2,自定義控件的使用

(1)佈局文件中引入


<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.future.measuredemo.MainActivity">

    <com.future.measuredemo.view.MImageView
        android:id="@+id/content_miv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        />

    <ImageView
        android:id="@+id/normal_iv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/content_miv"
        android:src="@mipmap/eddie1" />

</RelativeLayout>


爲了比對顯示效果,添加ImageView顯示內容。


(2)設置所需要資源文件


public class MainActivity extends AppCompatActivity {
    /**
     * 控件
     */
    private MImageView imageView;
    /**
     * 展示內容
     */
    private Bitmap bitmap;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);


        initView();
        initData();
    }


    /**
     * 初始化佈局
     */
    private void initView() {
        imageView = findViewById(R.id.content_miv);

    }

    /**
     * 初始化數據
     */
    private void initData() {
        bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.eddie2);
        imageView.setBitmap(bitmap);
    }
}


3,運行

此時使用默認量測效果。想修改量測,最簡單的方法,就是設置固定值。


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
       //super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        // 設置測量尺寸【固定值】
        setMeasuredDimension(500, 500);
    }

展示效果如圖所示:上面的是設置定值顯示,下面的使用ImageView控件展示內容。


因爲設置定值,圖片超出定值的部分並沒有顯示。


4,修改量測方式


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        /**
         *  計算寬度
         */
        int width = 0;
        int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
        int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);

        if (modeWidth == MeasureSpec.EXACTLY) {
            width = sizeWidth;
        } else {
            width = bitmap.getWidth();
        }


        /**
         * 計算高度
         */
        int height = 0;
        //獲取數據拆解
        int modeHeight = MeasureSpec.getMode(heightMeasureSpec);
        int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);

        if (modeHeight == MeasureSpec.EXACTLY) {
            height = sizeHeight;
        } else {
            height = bitmap.getHeight() ;
        }

        setMeasuredDimension(width, height);
    }

此時,對控件設置padding值並不起作用。繼續優化顯示方案量測顯示方案:


   @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        /**
         *  計算寬度
         */
        int width = 0;
        int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
        int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);

        if (modeWidth == MeasureSpec.EXACTLY) {
            width = sizeWidth;
        } else {
//            width = bitmap.getWidth();
            width = bitmap.getWidth() + getPaddingLeft() + getPaddingRight();
            if (modeWidth == MeasureSpec.AT_MOST) {
                width = Math.min(width, sizeWidth);
            }
        }


        /**
         * 計算高度
         */
        int height = 0;
        //獲取數據拆解
        int modeHeight = MeasureSpec.getMode(heightMeasureSpec);
        int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);

        if (modeHeight == MeasureSpec.EXACTLY) {
            height = sizeHeight;
        } else {
//            height = bitmap.getHeight() ;
            height = bitmap.getHeight() + getPaddingTop() + getPaddingBottom();
            if (modeHeight == MeasureSpec.AT_MOST) {
                height = Math.min(height, sizeHeight);
            }
        }

        setMeasuredDimension(width, height);
    }


   @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //繪製內容
//        canvas.drawBitmap(bitmap, 0, 0, null);
        canvas.drawBitmap(bitmap, getPaddingLeft(), getPaddingTop(), null);
    }
展示效果:



當繪製函數不優化時,周邊的padding值有加入運算,實際展示效果卻不對。自定義控件的三個主要方法,其實是相互關聯的。

有了padding值,就會想到margin值。margin值主要交於父控件計算。此時添加margin值,也是可以正確顯示的,因爲margin值交於自定義控件父控件RelativeLayout處理。


二、模擬圖片加文字合成控件實現量測


爲增強理解整個量測過程,將TextView和ImageView合成一個控件。


public class ImageTextView extends View {
    /**
     * 圖片內容
     */
    private Bitmap bitmap;
    /**
     * 繪製文本筆
     */
    private TextPaint textPaint;
    /**
     * 文本內容
     */
    private String textStr;
    /**
     * 文字大小
     */
    private float textSize;


    /**
     * 枚舉標識
     */
    private enum Direction {
        WIDTH, HEIGHT
    }

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

    public ImageTextView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        calculationParameters();
        init();
    }

    public ImageTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    /**
     * 計算參數
     */
    private void calculationParameters() {
        int screenWidth = MainApplication.getApplication().getWidth();
        textSize = screenWidth / 10f;
    }

    /**
     * 初始化
     */
    private void init() {
        if (null == bitmap) {
            bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.eddie4);
        }

        if (null == textStr || textStr.trim().length() == 0) {
            textStr = "陽光正能量偶像";
        }

        //設置筆形參數
        textPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG
                | Paint.DITHER_FLAG | Paint.LINEAR_TEXT_FLAG);
        textPaint.setColor(Color.CYAN);
        textPaint.setTextSize(textSize);
        textPaint.setTextAlign(Paint.Align.CENTER);
        textPaint.setTypeface(Typeface.DEFAULT_BOLD);
    }


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        setMeasuredDimension(getMeasureSize(widthMeasureSpec, Direction.WIDTH)
                , getMeasureSize(heightMeasureSpec, Direction.HEIGHT));
    }

    /***
     * 量測佈局
     * @param measureSpec
     * @param direction
     * @return
     */
    private int getMeasureSize(int measureSpec, Direction direction) {
        int result = 0;

        int mode = MeasureSpec.getMode(measureSpec);
        int size = MeasureSpec.getSize(measureSpec);

        switch (mode) {
            case MeasureSpec.EXACTLY:
                result = size;
                break;
            default:
                if (direction == Direction.WIDTH) {
                    float textWidth = textPaint.measureText(textStr);
                    result = (textWidth >= bitmap.getWidth()) ? (int) textWidth : bitmap.getWidth();
                } else if (direction == Direction.HEIGHT) {
                    result = (int) (textPaint.descent() - textPaint.ascent() * 2 + bitmap.getHeight());
                }

                if (mode == MeasureSpec.AT_MOST) {
                    result = Math.min(result, size);
                }
                break;
        }
        return result;
    }


    @Override
    protected void onDraw(Canvas canvas) {
//        super.onDraw(canvas);
        canvas.drawBitmap(bitmap, getWidth() / 2 - bitmap.getWidth() / 2,
                getHeight() / 2 - bitmap.getHeight() / 2, null);
        canvas.drawText(textStr, getWidth() / 2, bitmap.getHeight()
                + getHeight() / 2 - bitmap.getHeight() / 2 - textPaint.ascent(), textPaint);
    }
}

展示效果:


其實還是比較懶的啦,就是在上面Demo稍微修改就展示了偷笑偷笑偷笑。View的量測到此就基本結束了。


三、ViewGroup量測實現過程


僅僅只是完成View的量測是不夠的,畢竟自定義控件內部可能嵌套很多種的形式。ViewGroup的量測也是非常重要的。不廢話,直接來一個小栗子看看。


public class ViewGroupLayout extends ViewGroup {
    public ViewGroupLayout(Context context) {
        super(context);
    }

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

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

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        if (getChildCount() > 0) {
            //遍歷量測子佈局
            measureChildren(widthMeasureSpec, heightMeasureSpec);
        }
    }

    @Override
    protected void onLayout(boolean b, int i, int i1, int i2, int i3) {
        // 獲取父容器內邊距
        int parentPaddingLeft = getPaddingLeft();
        int parentPaddingTop = getPaddingTop();


        if (getChildCount() > 0) {
            // 那麼遍歷子元素並對其進行定位佈局
/*            for (int j = 0; j < getChildCount(); j++) {
                View child = getChildAt(j);
                child.layout(0, 0, getMeasuredWidth(), getMeasuredHeight());
            }*/

            /**
             * 所有佈局均從[0,0]開始,不符合佈局方式
             * 在開始點處有初始數據積累
             */

            int tempHeight = 0;
            for (int j = 0; j < getChildCount(); j++) {
                View child = getChildAt(j);
//                child.layout(0, tempHeight, child.getMeasuredWidth(), child.getMeasuredHeight() + tempHeight);
                child.layout(parentPaddingLeft, parentPaddingTop + tempHeight,
                        child.getMeasuredWidth() + parentPaddingLeft,
                        child.getMeasuredHeight() + tempHeight + parentPaddingTop);
                tempHeight += child.getMeasuredHeight();
            }
        }

    }
}

注意小細節:

在最開始時,Layout直接都從[0,0]開始,所有的子View都疊在了左上角。修改佈局方式,整體展示基本正常。



但是這裏,與量測有個毛關係啊!!

當然不是了,當前佈局中,使用了系統自帶量測方法。

整個量測其實是從頂部往下開始計算的,最開始的佈局是填充父窗體,也就是整個屏幕。

ViewGroup的量測主要使用了measureChildren()方法。說曹操曹操到:


 protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {  
    final int size = mChildrenCount;  
     final View[] children = mChildren;  
      for (int i = 0; i < size; ++i) {  
          final View child = children[i];  
           if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {  
              measureChild(child, widthMeasureSpec, heightMeasureSpec);  
          }  
     }  
  }  


實際就是遞歸量測每一個子類:


    protected void measureChild(View child, int parentWidthMeasureSpec,
                                int parentHeightMeasureSpec) {
        // 獲取子元素的佈局參數  
        final LayoutParams lp = child.getLayoutParams();
        /* 
        * 將父容器的測量規格已經上下和左右的邊距還有子元素本身的佈局參數傳入getChildMeasureSpec方法計算最終測量規格 
        */
        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom, lp.height);

        // 調用子元素的measure傳入計算好的測量規格  
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

要看看,那就儘量看清楚些吧。


 public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        // 獲取父容器的測量模式和尺寸大小  
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);

        // 這個尺寸應該減去內邊距的值  
        int size = Math.max(0, specSize - padding);

        // 聲明臨時變量存值  
        int resultSize = 0;
        int resultMode = 0;
        /* 
       * 根據模式判斷 
       */
        switch (specMode) {
            case MeasureSpec.EXACTLY: // 父容器尺寸大小是一個確定的值  
                    /* 
                     * 根據子元素的佈局參數判斷 
                     */
                if (childDimension >= 0) { //如果childDimension是一個具體的值  
                    // 那麼就將該值作爲結果  
                    resultSize = childDimension;

                    // 而這個值也是被確定的  
                    resultMode = MeasureSpec.EXACTLY;
                } else if (childDimension == LayoutParams.MATCH_PARENT) { //如果子元素的佈局參數爲MATCH_PARENT  
                    // 那麼就將父容器的大小作爲結果  
                    resultSize = size;

                    // 因爲父容器的大小是被確定的所以子元素大小也是可以被確定的  
                    resultMode = MeasureSpec.EXACTLY;
                } else if (childDimension == LayoutParams.WRAP_CONTENT) { //如果子元素的佈局參數爲WRAP_CONTENT  
                    // 那麼就將父容器的大小作爲結果  
                    resultSize = size;

                    // 但是子元素的大小包裹了其內容後不能超過父容器  
                    resultMode = MeasureSpec.AT_MOST;
                }
                break;

            case MeasureSpec.AT_MOST: // 父容器尺寸大小擁有一個限制值  
                     /* 
                     * 根據子元素的佈局參數判斷 
                    */
                if (childDimension >= 0) { //如果childDimension是一個具體的值  
                    // 那麼就將該值作爲結果  
                    resultSize = childDimension;

                    // 而這個值也是被確定的  
                    resultMode = MeasureSpec.EXACTLY;
                } else if (childDimension == LayoutParams.MATCH_PARENT) { //如果子元素的佈局參數爲MATCH_PARENT  
                    // 那麼就將父容器的大小作爲結果  
                    resultSize = size;

                    // 因爲父容器的大小是受到限制值的限制所以子元素的大小也應該受到父容器的限制  
                    resultMode = MeasureSpec.AT_MOST;
                } else if (childDimension == LayoutParams.WRAP_CONTENT) { //如果子元素的佈局參數爲WRAP_CONTENT  
                    // 那麼就將父容器的大小作爲結果  
                    resultSize = size;

                    // 但是子元素的大小包裹了其內容後不能超過父容器  
                    resultMode = MeasureSpec.AT_MOST;
                }
                break;

            case MeasureSpec.UNSPECIFIED: // 父容器尺寸大小未受限制  
                       /* 
                        * 根據子元素的佈局參數判斷 
                        */
                if (childDimension >= 0) { //如果childDimension是一個具體的值  
                    // 那麼就將該值作爲結果  
                    resultSize = childDimension;

                    // 而這個值也是被確定的  
                    resultMode = MeasureSpec.EXACTLY;
                } else if (childDimension == LayoutParams.MATCH_PARENT) { //如果子元素的佈局參數爲MATCH_PARENT  
                    // 因爲父容器的大小不受限制而對子元素來說也可以是任意大小所以不指定也不限制子元素的大小  
                    resultSize = 0;
                    resultMode = MeasureSpec.UNSPECIFIED;
                } else if (childDimension == LayoutParams.WRAP_CONTENT) { //如果子元素的佈局參數爲WRAP_CONTENT  
                    // 因爲父容器的大小不受限制而對子元素來說也可以是任意大小所以不指定也不限制子元素的大小  
                    resultSize = 0;
                    resultMode = MeasureSpec.UNSPECIFIED;
                }
                break;
        }

        // 返回封裝後的測量規格  
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

似乎就能大功告成了,最後發現添加margin值,不頂用了?!哎喲,XXX,想罵人,但是沒用啊。

之前說過,margin值由父類控制計算。這一次,逃不掉了唄~~~

自定義參數類型,爲御用做準備:


    public static class ViewGroupLayoutParams extends MarginLayoutParams {
        public ViewGroupLayoutParams(MarginLayoutParams source) {
            super(source);
        }

        public ViewGroupLayoutParams(android.view.ViewGroup.LayoutParams source) {
            super(source);
        }

        public ViewGroupLayoutParams(Context c, AttributeSet attrs) {
            super(c, attrs);
        }

        public ViewGroupLayoutParams(int width, int height) {
            super(width, height);
        }
    }

    @Override
    protected ViewGroupLayoutParams generateDefaultLayoutParams() {
        return new ViewGroupLayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
    }

    @Override
    protected android.view.ViewGroup.LayoutParams generateLayoutParams(android.view.ViewGroup.LayoutParams p) {
        return new ViewGroupLayoutParams(p);
    }


    @Override
    public android.view.ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new ViewGroupLayoutParams(getContext(), attrs);
    }

    @Override
    protected boolean checkLayoutParams(android.view.ViewGroup.LayoutParams p) {
        return p instanceof ViewGroupLayoutParams;
    }

重寫量測方法:


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int parentDesireWidth = 0;
        int parentDesireHeight = 0;

        if (getChildCount() > 0) {
            if (getChildCount() > 0) {
                // 那麼遍歷子元素並對其進行測量
                for (int i = 0; i < getChildCount(); i++) {

                    // 獲取子元素
                    View child = getChildAt(i);

                    // 獲取子元素的佈局參數
                    ViewGroupLayoutParams clp = (ViewGroupLayoutParams) child.getLayoutParams();

                    // 測量子元素並考慮外邊距
                    measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);

                    // 計算父容器的期望值
                    parentDesireWidth += child.getMeasuredWidth() + clp.leftMargin + clp.rightMargin;
                    parentDesireHeight += child.getMeasuredHeight() + clp.topMargin + clp.bottomMargin;
                }

                // 考慮父容器的內邊距
                parentDesireWidth += getPaddingLeft() + getPaddingRight();
                parentDesireHeight += getPaddingTop() + getPaddingBottom();

                // 嘗試比較建議最小值和期望值的大小並取大值
                parentDesireWidth = Math.max(parentDesireWidth, getSuggestedMinimumWidth());
                parentDesireHeight = Math.max(parentDesireHeight, getSuggestedMinimumHeight());
            }

            // 設置最終測量值
            setMeasuredDimension(resolveSize(parentDesireWidth, widthMeasureSpec), resolveSize(parentDesireHeight, heightMeasureSpec));
        }
    }

他的老兄弟就不幹了,你都更新刷新裝備了,我得升級啊!onLayout()也就在這邊嗷嗷叫了。。。。。


    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int parentPaddingLeft = getPaddingLeft();
        int parentPaddingTop = getPaddingTop();

        if (getChildCount() > 0) {
            // 聲明一個臨時變量存儲高度倍增值
            int mutilHeight = 0;

            // 那麼遍歷子元素並對其進行定位佈局
            for (int i = 0; i < getChildCount(); i++) {
                // 獲取一個子元素
                View child = getChildAt(i);

                ViewGroupLayoutParams clp = (ViewGroupLayoutParams) child.getLayoutParams();

                // 通知子元素進行佈局
                // 此時考慮父容器內邊距和子元素外邊距的影響
                child.layout(parentPaddingLeft + clp.leftMargin, mutilHeight + parentPaddingTop + clp.topMargin,
                        child.getMeasuredWidth() + parentPaddingLeft + clp.leftMargin,
                        child.getMeasuredHeight() + mutilHeight + parentPaddingTop + clp.topMargin);

                // 改變高度倍增值
                mutilHeight += child.getMeasuredHeight() + clp.topMargin + clp.bottomMargin;
            }
        }
    }

說句實在話,這最後的一些,只是藉助網絡的力量實現了,在消化上還需要功夫。

此處,Layout的使用也進入頂配了。後續的文章會細化Layout基礎的理解部分。

展示效果:



磨嘰磨嘰的傳送門

【一如既往的源碼地址】






誰不是一邊不想活了,一邊努力活着!

公衆號看見的一段話語:

努力活着纔有希望。

才能看見美麗的風景,

才能完成夢想,

才能等到期盼已久的自由。

愛你的人還在趕來的路上,

你要好好的活下去。


請你繼續熱愛生活,

一邊哭泣,一邊咬牙繼續。

那些磨礪的沙,

總有一天會讓你變成珍珠。

沒有一種痛苦是專門爲你準備的。

夜黑透了,接下來就是黎明。



發佈了121 篇原創文章 · 獲贊 32 · 訪問量 16萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章