Android开发Android开发经验谈

Android View的测量模式

2023-12-04  本文已影响0人  奔跑吧李博

onMeasure讲解

View绘制出来需要知道自己的宽高是多少,所以要先进行测量尺寸。
从门缝里面看世界,那就从View的内部类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;

    
        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);
            }
        }

        public static int makeSafeMeasureSpec(int size, int mode) {
            if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {
                return 0;
            }
            return makeMeasureSpec(size, mode);
        }

        @MeasureSpecMode
        public static int getMode(int measureSpec) {
            //noinspection ResourceType
            return (measureSpec & MODE_MASK);
        }

        public static int getSize(int measureSpec) {
            return (measureSpec & ~MODE_MASK);
        }
    }

测量模式

UNSPECIFIED
EXACTLY
AT_MOST

为了认准测量模式的对应方式,我写了一个简单测试类:

public class CustomView extends View{

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

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

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

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

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

        switch (widthMode){
            case MeasureSpec.UNSPECIFIED:
                Log.e("TAG","widthMode " + "UNSPECIFIED");
                break;
            case MeasureSpec.AT_MOST:
                Log.e("TAG","widthMode " + "AT_MOST");
                break;
            case MeasureSpec.EXACTLY:
                Log.e("TAG","widthMode " + "EXACTLY");
                break;
        }
        Log.e("TAG","widthSize " + MeasureSpec.getSize(widthMeasureSpec));

        switch (heightMode){
            case MeasureSpec.UNSPECIFIED:
                Log.e("TAG","heightMode " + "UNSPECIFIED");
                break;
            case MeasureSpec.AT_MOST:
                Log.e("TAG","heightMode " + "AT_MOST");
                break;
            case MeasureSpec.EXACTLY:
                Log.e("TAG","heightMode " + "EXACTLY");
                break;
        }
        Log.e("TAG","heightSize " + MeasureSpec.getSize(heightMeasureSpec));
    }

}
测试结果:

布局中宽高均为match_parent: 测量模式为EXACTLY

image.png

布局中宽高均为wrap_content: 测量模式为AT_MOST

image.png

布局中宽高均为200dp(固定数值): 测量模式为EXACTLY

image.png
测量完成:

测量完成回调onMeasure(int widthMeasureSpec, int heightMeasureSpec)方法。
那么这两个名字长长的变量是什么呢?就是测量出的宽和高的信息。

回到MeasureSpec类分析,一个Int有32位,用前2位表示SpecMode ,2位数有四种表示方法了,00,01,11分别表示上面的模式顺序。后30位表示SpecSize。那我们是不是获取测量模式和尺寸都要自己使用位移计算呢?不用的,MeasureSpec类已经有了,自带了拆分和打包方法。

image.png
public static int makeMeasureSpec(int size, int mode) {
            if (sUseBrokenMakeMeasureSpec) {
                return size + mode;
            } else {
                return (size & ~MODE_MASK) | (mode & MODE_MASK);
            }
        }

public static int getMode(int measureSpec) {
            return (measureSpec & MODE_MASK);
        }

public static int getSize(int measureSpec) {
            return (measureSpec & ~MODE_MASK);
        }

获取测量模式和测量大小:

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

        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
}

现在,我们要写一个正方形ImageView,使用setMeasuredDimension()自己重设测量值,让高度值也等于宽度值:

public class SquareImageView extends AppCompatImageView{

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

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

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(widthMeasureSpec,widthMeasureSpec);
    }
}
getChildMeasureSpec()代码里的生成规则:

1.当子View的宽高设置的是具体数值时,显然我们可以直接拿到子View的宽高,则子View宽高就确定了,不用再去考虑父容器的SpecMode了,此时子View的SpecMode为EXACTLY,SpecSize就是设置的宽高。

2.当子View的宽高设置的是match_parent, 则不管父容器的SpecMode是什么模式,子View的SpecSize就等于父容器的宽高,而子View的SpecMode随父容器的SpecMode。

3.当子View的宽高设置的是wrap_content,因为这种情况父容器实在不知道子View应该多宽多高,所以子View的SpecSize给的是父容器的宽高,也就是说只是给子View限制了一个最大宽高,而子View的SpecMode是AT_MOST模式。

如果你设置的是wrap_content,则默认显示出来是其父容器的大小,如果你想要它正常的显示为wrap_content,则你就要自己重写onMeasure()来自己计算它的宽高度并设置。

示例:

public class MyView extends View {
    public MyView(Context context) {
        super(context);
    }

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

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

父view的大小是200dp,子view的宽高是wrap_content,父view的背景是紫色,子view背景是黑色。

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <RelativeLayout
        android:layout_width="200dp"
        android:layout_height="200dp"
        android:background="@color/purple_200">

        <com.example.performanceopt.MyView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="@color/black"/>
    </RelativeLayout>
</RelativeLayout>

结果是子view的显示出来是其父容器的200dp大小。

上一篇下一篇

猜你喜欢

热点阅读