Android 自定義view的測量

Android 自定義view的測量

  1. 首先在自定義view的時候,你需要告訴系統該畫一個多大的View。這個過程在onMeasure()方法裏進行的。

    Android系統給我們提供了一個設計短小精悍卻功能強大的類——MeasureSpec類。通過它可以幫助我們測量view。
    MeasureSpec是一個32位的int值,其中高2位爲測量的模式,低30位爲測量的大小,在計算中使用位運算的原因是爲了提高並優化效率。

如下是MeasureSpec源碼:

 public static class MeasureSpec {
        private static final int MODE_SHIFT = 30;
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

        /** @hide */
        @IntDef({UNSPECIFIED, EXACTLY, AT_MOST})
        @Retention(RetentionPolicy.SOURCE)
        public @interface MeasureSpecMode {}

        /**
         * 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;

        /**
         * Creates a measure specification based on the supplied size and mode.
         *
         * The mode must always be one of the following:
         * <ul>
         *  <li>{@link android.view.View.MeasureSpec#UNSPECIFIED}</li>
         *  <li>{@link android.view.View.MeasureSpec#EXACTLY}</li>
         *  <li>{@link android.view.View.MeasureSpec#AT_MOST}</li>
         * </ul>
         *
         * <p><strong>Note:</strong> On API level 17 and lower, makeMeasureSpec's
         * implementation was such that the order of arguments did not matter
         * and overflow in either value could impact the resulting MeasureSpec.
         * {@link android.widget.RelativeLayout} was affected by this bug.
         * Apps targeting API levels greater than 17 will get the fixed, more strict
         * behavior.</p>
         *
         * @param size the size of the measure specification
         * @param mode the mode of the measure specification
         * @return the measure specification based on size and mode
         */
        public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
                                          @MeasureSpecMode int mode) {
            if (sUseBrokenMakeMeasureSpec) {
                return size + mode;
            } else {
                return (size & ~MODE_MASK) | (mode & MODE_MASK);
            }
        }

        /**
         * Like {@link #makeMeasureSpec(int, int)}, but any spec with a mode of UNSPECIFIED
         * will automatically get a size of 0. Older apps expect this.
         *
         * @hide internal use only for compatibility with system widgets and older apps
         */
        public static int makeSafeMeasureSpec(int size, int mode) {
            if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {
                return 0;
            }
            return makeMeasureSpec(size, mode);
        }

        /**
         * Extracts the mode from the supplied measure specification.
         *
         * @param measureSpec the measure specification to extract the mode from
         * @return {@link android.view.View.MeasureSpec#UNSPECIFIED},
         *         {@link android.view.View.MeasureSpec#AT_MOST} or
         *         {@link android.view.View.MeasureSpec#EXACTLY}
         */
        @MeasureSpecMode
        public static int getMode(int measureSpec) {
            //noinspection ResourceType
            return (measureSpec & MODE_MASK);
        }

        /**
         * Extracts the size from the supplied measure specification.
         *
         * @param measureSpec the measure specification to extract the size from
         * @return the size in pixels defined in 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);
            int size = getSize(measureSpec);
            if (mode == UNSPECIFIED) {
                // No need to adjust size for UNSPECIFIED mode.
                return makeMeasureSpec(size, UNSPECIFIED);
            }
            size += 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.
         *
         * @param measureSpec the measure specification to convert to a String
         * @return a String with the following format: "MeasureSpec: MODE SIZE"
         */
        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();
        }
    }
  • 在MeasureSpec類中有三種模式:

    • EXACTLY
      即精準模式,當我們將控件的layout_width屬性或者layout_height屬性指定爲具體數值時,比如layout_width=”200dp”,或者指定爲match_parent屬性時(佔據父類View的大小),系統使用的是EXACTLY。

    • AT_MOST
      最大值模式,當控件的layout_width屬性或layout_height屬性指定爲warp_content時,控件大小一般隨着控件的子控件的內容的變化而變化,此時控件的尺寸只要不超過父控件允許的最大值即可。

    • UNSPECIFIED
      這個屬性它不指定其大小測量模式,View想多大就多大,通常情況下繪製自定義view時才使用。

下面我們在看下View中的onMeasure()方法是怎麼寫的:

   /**
     * <p>
     * Measure the view and its content to determine the measured width and the
     * measured height. This method is invoked by {@link #measure(int, int)} and
     * should be overridden by subclasses to provide accurate and efficient
     * measurement of their contents.
     * </p>
     *
     * <p>
     * <strong>CONTRACT:</strong> When overriding this method, you
     * <em>must</em> call {@link #setMeasuredDimension(int, int)} to store the
     * measured width and height of this view. Failure to do so will trigger an
     * <code>IllegalStateException</code>, thrown by
     * {@link #measure(int, int)}. Calling the superclass'
     * {@link #onMeasure(int, int)} is a valid use.
     * </p>
     *
     * <p>
     * The base class implementation of measure defaults to the background size,
     * unless a larger size is allowed by the MeasureSpec. Subclasses should
     * override {@link #onMeasure(int, int)} to provide better measurements of
     * their content.
     * </p>
     *
     * <p>
     * If this method is overridden, it is the subclass's responsibility to make
     * sure the measured height and width are at least the view's minimum height
     * and width ({@link #getSuggestedMinimumHeight()} and
     * {@link #getSuggestedMinimumWidth()}).
     * </p>
     *
     * @param widthMeasureSpec horizontal space requirements as imposed by the parent.
     *                         The requirements are encoded with
     *                         {@link android.view.View.MeasureSpec}.
     * @param heightMeasureSpec vertical space requirements as imposed by the parent.
     *                         The requirements are encoded with
     *                         {@link android.view.View.MeasureSpec}.
     *
     * @see #getMeasuredWidth()
     * @see #getMeasuredHeight()
     * @see #setMeasuredDimension(int, int)
     * @see #getSuggestedMinimumHeight()
     * @see #getSuggestedMinimumWidth()
     * @see android.view.View.MeasureSpec#getMode(int)
     * @see android.view.View.MeasureSpec#getSize(int)
     */
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

有上面的代碼看出來在View中onMeasure中最後執行的是setMeasureDimension()方法,參數是getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)

我們在來看下getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),

  /**
     * Utility to return a default size. Uses the supplied size if the
     * MeasureSpec imposed no constraints. Will get larger if allowed
     * by the MeasureSpec.
     *
     * @param size Default size for this view
     * @param measureSpec Constraints imposed by the parent
     * @return The size this view should be.
     */
    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;
    }

由上面的源代碼可以看出來只處理 MeasureSpec.UNSPECIFIED、MeasureSpec.EXACTLY這倆種測量方式。

所以我們在自定義view時,如果想讓自定義View支持warp_content屬性,那麼就必須要重寫onMeasure()方法來指定warp_content時的大小。

接下來我們可以自己自定義View 如下

  1. 首先重寫onMeasure()方法。
  @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
         setMeasuredDimension(measureWidth(widthMeasureSpec),
                measureHeight(heightMeasureSpec));
    }
  • 其中setMeasuredDimension() 是view在onMeasure()方法中最後要調用的。這裏我們用measureWidth(widthMeasureSpec)和measureHeight(heightMeasureSpec)重新定義測量規則。
  • 下面我們對measureWidth(widthMeasureSpec)舉例說明如何自定義測量值的。

  • 第一步從MeasureSpec對象中提取具體的測量模式和大小,代碼如下:

        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);
  • 第二步通過判斷測量的模式,給出不同的測量值如下:
 private int measureWidth(int widthMeasureSpec){
        int result=0;
        int specMode=MeasureSpec.getMode(widthMeasureSpec);
        int specSize=MeasureSpec.getSize(widthMeasureSpec);

        if(specMode==MeasureSpec.EXACTLY){
            result=widthMeasureSpec;
        }else{
            result=500;
            if(specMode==MeasureSpec.AT_MOST){                
                result=Math.min(result,specSize);
            }
        }

        return result;
    }

我在這裏說明一下,當specMode爲EXACTLy時,直接指定specSize即可,當specMode爲其他的模式時,需要給一個默認的大小。如果指定wrap_content屬性,即AT_MOST模式,需要取出我們指定的大小與specSize 其中最小的那個值,作爲測量的最後測量值.

ViewGroup
在最後簡單說下ViewGroup的測量,ViewGroup回去管理其子View,其中一個管理項目就是負責子View的顯示大小。當ViewGroup的大小爲wrap_content,ViewGroup就需要對子view進行遍歷,以便獲的所有子view的大小,從而決定自己的大小。而在其他模式下則會通過具體的指定值來設置自身的大小。

當子view測量完畢後,就需要將子view放到合適的位置上,這個過程就是View的Layout過程。ViewGroup在執行Layout過程時,同樣是遍歷來調用子view的layout的方法,並指定其具體顯示的位置,從而來決定其佈局位置。

自定義ViewGroup時,通常會重寫onLayout方法來控制子View顯示位置的邏輯。同樣,如果需要支持wrap_content屬性,那麼它還必須重寫onMeasure()方法,這點與View是一樣的。

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