我的Android笔记--自定义View的measure

来源:互联网 发布:linux 发布tomcat项目 编辑:程序博客网 时间:2024/06/03 18:00

在工作过程中,很多时候系统提供的控件并不能完成需求。这时候我们就需要自己定义绘制视图。
一般的自定义控件继承View直接绘制,绘制时我们需要清楚了解控件中主要方法和成员的含义和作用,才能以更高的效率去完成我们的工作。
定义一个视图,首先需要绘制视图在屏幕窗口处的边界尺寸,下面几点讲的就是关于MeasureSpec和onMeasure()的内容。

一、MeasureSpec

MeasureSpec是Measure Specification的缩写,字面翻译是测量规范。
MeasureSpec封装通过从父容器到子布局的参数。每个MeasureSpec表示宽度和高度的参数要求。一个MeasureSpec包括尺寸和模式信息。
它有三种模式。分别是。
UNSPECIFIED:父容器没有强加任何约束给子视图。子视图可以是任何它要的尺寸。
EXACTLY:父容器决定了子视图的确切大小。子视图被限定在给定界面里面,忽略本身大小。
AT_MOST:子视图至多达到指定大小的值。
这个类里,有几个主要的方法makeMeasureSpec(int size,int mode),makeSafeMeasureSpec(int size,int mode),getMode(int measureSpec),getSize(int measureSpec),adjust(int measureSpec,int delta).

a、makeMeasureSpec(int size,int mode)

/*** 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>* 在API17及以下,参数的命令或者值的溢出都不会对makeMeasureSpec的实现结果造成很大的影响。这个bug对RelativeLayout造成了不小影响。* 在高于17的API版本中修复了这些问题,使得其更加健壮。* @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(int size, int mode) {    if (sUseBrokenMakeMeasureSpec) {        return size + mode;    } else {        return (size & ~MODE_MASK) | (mode & MODE_MASK);    }}对于方法中sUseBrokenMakeMeasureSpec参数的管理,在View的构造函数中有具体处理// Older apps may need this compatibility hack for measurement.// 旧的应用可能需要这种兼容性进行测量。sUseBrokenMakeMeasureSpec = targetSdkVersion <= JELLY_BEAN_MR1;

b、makeSafeMeasureSpec(int size,int mode)

这是内部使用的方法。

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

c、getMode(int measureSpec)

/*** 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}*/public static int getMode(int measureSpec) {    return (measureSpec & MODE_MASK);}

d、getSize(int measureSpec)

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

e、adjust(int measureSpec,int delta)

这个方法没有注释,不知道神马意思。

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

二、onMeasure

在自定义视图的时候,我们通常要手动计算并指定视图的尺寸大小,但有一点需要清楚onMeasure()方法可以指定视图的大小,被指定的大小只是视图的边界大小,不是视图的内容大小。实际上,视图的内容大小可以是无限大,也就是画布是无限大的。我们通常在有限的空间里面展示,但内容超出边界范围了,我们可能就需要嵌套滚动容器或者自己添加手势处理来控制视图内容滚动。
查看android.view.View中onMeasure()的源码

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));}

在这个方法里面,主要有2个方法,分别是setMeasureDimension()和getDefaultSize(),先看看setMeasureDimension()的源码

/*** <p>This method must be called by {@link #onMeasure(int, int)} to store the* measured width and measured height. Failing to do so will trigger an* exception at measurement time.</p>* 这个方法必须在onMeasure()中调用,它用以存储控件的高度和宽度。如果不在onMeasure()方法中调用它会引起异常。* @param measuredWidth The measured width of this view.  May be a complex* bit mask as defined by {@link #MEASURED_SIZE_MASK} and* {@link #MEASURED_STATE_TOO_SMALL}.* @param measuredHeight The measured height of this view.  May be a complex* bit mask as defined by {@link #MEASURED_SIZE_MASK} and* {@link #MEASURED_STATE_TOO_SMALL}.*/protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {    //该方法用以判别View是否属于是一个使用视觉边界布局的ViewGroup类型的类。    boolean optical = isLayoutModeOptical(this);    //optical和该View的父容器optical不相等,那么获取一个视觉插入物(Insets)    if (optical != isLayoutModeOptical(mParent)) {        Insets insets = getOpticalInsets();        int opticalWidth  = insets.left + insets.right;        int opticalHeight = insets.top  + insets.bottom;        //根据optical来判断是实际值加上还是减去视觉尺寸。        measuredWidth  += optical ? opticalWidth  : -opticalWidth;        measuredHeight += optical ? opticalHeight : -opticalHeight;    }    //调用setMeasureDimensionRaw()方法设置View 的大小。    setMeasuredDimensionRaw(measuredWidth, measuredHeight);}

由于我查看的源码是API23中的,涉及到了opticalMode这种模式的内容,在这也顺便记录下来。
贴出原文描述,顺便翻译一下,下面是原文:
For views that contain nine-patch background images, you can now specify that they should be aligned with neighboring views based on the “optical” bounds of the background image rather than the “clip” bounds of the view.
For example, figures 1 and 2 each show the same layout, but the version in figure 1 is using clip bounds (the default behavior), while figure 2 is using optical bounds. Because the nine-patch images used for the button and the photo frame include padding around the edges, they don’t appear to align with each other or the text when using clip bounds.
Note: The screenshot in figures 1 and 2 have the “Show layout bounds” developer setting enabled. For each view, red lines indicate the optical bounds, blue lines indicate the clip bounds, and pink indicates margins.
Mouse over to hide the layout bounds.
使用默认布局模式使用默认布局模式
使用opticalMode使用opticalMode
默认布局打开布局边界默认布局打开布局边界
使用opticalMode打开布局边界使用opticalMode打开布局边界
To align the views based on their optical bounds, set the android:layoutMode attribute to “opticalBounds” in one of the parent layouts. For example:

<LinearLayout android:layoutMode="opticalBounds" ... >

For this to work, the nine-patch images applied to the background of your views must specify the optical bounds using red lines along the bottom and right-side of the nine-patch file (as shown in figure 3). The red lines indicate the region that should be subtracted from the clip bounds, leaving the optical bounds of the image.
这里写图片描述
Figure 3. Zoomed view of the Holo button nine-patch with optical bounds.
When you enable optical bounds for a ViewGroup in your layout, all descendant views inherit the optical bounds layout mode unless you override it for a group by setting android:layoutMode to “clipBounds”. All layout elements also honor the optical bounds of their child views, adapting their own bounds based on the optical bounds of the views within them. However, layout elements (subclasses of ViewGroup) currently do not support optical bounds for nine-patch images applied to their own background.
If you create a custom view by subclassing View, ViewGroup, or any subclasses thereof, your view will inherit these optical bound behaviors.
Note: All widgets supported by the Holo theme have been updated with optical bounds, including Button, Spinner, EditText, and others. So you can immediately benefit by setting the android:layoutMode attribute to “opticalBounds” if your app applies a Holo theme (Theme.Holo,Theme.Holo.Light, etc.).
To specify optical bounds for your own nine-patch images with the Draw 9-patch tool, hold CTRL when clicking on the border pixels.
对于使用.9图片作为背景的视图,你现在可以指定使用基于”optical”边界的背景图片来对齐附近的视图而不是使用默认的”clip”来对齐。
例如,图左和图右展示同一个布局,图左使用默认布局,图右显示视觉布局。因为.9图作为按钮和照片帧饱含有填充边缘,使用默认边界不会对齐图形和文字。
注:下面的图片是在开发者选项里面开启布局边界选项以后的截图,上边是普通截图。红线表明视觉边界,蓝线表明剪辑边界,粉红线表明边缘。
为了让View基于他们的视觉边界布局,只要在他们父容器xml文件中设置”android:layoutMode=”opticalBounds”即可。
使用点九图作为你视图的背景必须指定红线沿着点九文件的底部和右边。红线表明应该从默认布局中减去的区域中留下的图像视图边界。
当你在你的布局中对一个ViewGroup启用视觉布局选项,全部子视图继承视觉边界布局模式,除非你在其它组中单独指定布局模式为clipBounds。所有布局元素的子视图都会遵守视觉布局,他们基于视图的视觉边界来适应他们自己的边界。
然而,布局元素(ViewGroup的子类)目前不支持视觉边界应用点九图作为他们的背景。
如果你通过集成View,ViewGroup添加一个自定义视图或者其他他们的子类,他们都会集成弗雷的视觉边界设定。
注意的是:所有Holo主题支持的小部件都已经升级为视觉边界,包括Button,Spinner,EditText等等。如果你的应用使用Holo主题,那你就可以很容易的设置他们的layoutMode属性为”opticalBounds”。
我们可以通过按住CTRL 点击图像边缘使用Draw 9-patch工具来指定你的点九图的视觉边界。

三、使用场景

正常来说,MeasureSpec这个类一般在View的onMeasure()中使用,我们可以生成两个MeasureSpec来控制视图的边界,一个是控制宽度的一个是控制高度的。

四、makeMeasureSpec(int size,int mode)

这个方法生成一个整型数值,在自定义View的onMeasure()中将其传递给其父类的onMeasure方法就可以对视图的大小作出规范。其中size对应着具体的数值,而mode直接决定自订View的边界。
size这个参数就不用解释了,对于mode,很多人一开始接触的时候并不知道mode到底可以决定什么。可以将mode和xml的layout_w/h这个参数来类别一些。
编写一个视图的xml文件,参数如下。在代码中打印控件的MeasureSpec信息。下面是xml代码

<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:app="http://schemas.android.com/apk/res-auto"    android:layout_width="match_parent"    android:layout_height="match_parent"    android:orientation="vertical">    <com.example.administrator.testone.customized.CustomizedTV        android:id="@+id/tv1"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:gravity="center"        android:text="TV1"        app:name="第一个控件" />    <com.example.administrator.testone.customized.CustomizedTV        android:id="@+id/tv2"        android:layout_width="100dp"        android:layout_height="100dp"        android:gravity="center"        android:text="TV2"        app:name="第二个控件" />    <com.example.administrator.testone.customized.CustomizedTV        android:id="@+id/tv3"        android:layout_width="match_parent"        android:layout_height="match_parent"        android:gravity="center"        android:text="TV3"        app:name="第三个控件" /></LinearLayout>

这是自定View 的自定义属性

<declare-styleable name="CustomizedTV">    <attr name="name" format="string" /></declare-styleable>

这是在CustomizedTV中onMeasure()的打印函数

 int count = 1;        do {            int spec;            if (count == 1) {                spec = widthMeasureSpec;            } else {                spec = heightMeasureSpec;            }            switch (MeasureSpec.getMode(spec)) {                case MeasureSpec.AT_MOST:                    Log.i(TAG, name + "   MeasureSpec.AT_MOST");                    break;                case MeasureSpec.UNSPECIFIED:                    Log.i(TAG, name + "   MeasureSpec.UNSPECIFIED");                    break;                case MeasureSpec.EXACTLY:                    Log.i(TAG, name + "   MeasureSpec.EXACTLY");                    break;                default:                    break;            }            count--;        } while (count > 0);

打印结果如下:

08-06 16:23:20.691 20330-20330/com.example.administrator.testone I/CustomizedTV: 第一个控件   MeasureSpec.AT_MOST08-06 16:23:20.696 20330-20330/com.example.administrator.testone I/CustomizedTV: 第二个控件   MeasureSpec.EXACTLY08-06 16:23:20.696 20330-20330/com.example.administrator.testone I/CustomizedTV: 第三个控件   MeasureSpec.EXACTLY08-06 16:23:20.801 20330-20330/com.example.administrator.testone I/CustomizedTV: 第一个控件   MeasureSpec.AT_MOST08-06 16:23:20.801 20330-20330/com.example.administrator.testone I/CustomizedTV: 第二个控件   MeasureSpec.EXACTLY08-06 16:23:20.801 20330-20330/com.example.administrator.testone I/CustomizedTV: 第三个控件   MeasureSpec.EXACTLY

这个打印结果不是按顺序输出,所以使用自定义属性来作为标签比较容易分析。
第一个CustomizedView的layout_w/h是wrap_content对应的是MeasureSpec.AT_MOST。
第二个CustomizedView的layout_w/h是固定值对应的是MeasureSpec.EXACTLY 。
第三个CustomizedView的layout_w/h是match_parent对应的是MeasureSpec.EXACTLY 。
从这可以得出结论是:在线性布局中,宽高设置固定值或者match_parent,它对应的是 MeasureSpec.EXACTLY 。也就是在父容器的边界下,它本身大小被忽略。而wrap_content则是对应MeasureSpec.AT_MOST。这是最简单的映射关系。
但是在复杂的布局,如RelativeLayout中,由于存在着其它复杂的规则限定,这种映射关系就被打乱,需要在具体情况中具体分析。

五、其它方法

对于getMode(int spec)和getSize(int spec)方法,可以获取自定义视图的模式和尺寸值

六、简单应用

有时候我们需要在页面展示一排有规律的按钮,并且这个页面的嵌套在ScrollView中的,我们可以偷懒使用复用控件如GridView来完成这个需求。自定义一个View,重写GridView的onMeasure方法,指定GridView的高度值和指定mode为MeasureSpec.AT_MOST即可。
下面是demo,主要重写继承的GridView的onMeasure()方法。

@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {    int spec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE>>1,            MeasureSpec.AT_MOST);    super.onMeasure(widthMeasureSpec, spec);}

如果不知道MeasureSpec给出的这个值是如何工作的,我们可能无法理解为什么设置了两个整数值就可以完成GridView高度的设置。查看GridView中onMeasure()的代码。

@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {    // Sets up mListPadding    super.onMeasure(widthMeasureSpec, heightMeasureSpec);    int widthMode = MeasureSpec.getMode(widthMeasureSpec);    int heightMode = MeasureSpec.getMode(heightMeasureSpec);    int widthSize = MeasureSpec.getSize(widthMeasureSpec);    int heightSize = MeasureSpec.getSize(heightMeasureSpec);    if (widthMode == MeasureSpec.UNSPECIFIED) {        if (mColumnWidth > 0) {            widthSize = mColumnWidth + mListPadding.left + mListPadding.right;        } else {            widthSize = mListPadding.left + mListPadding.right;        }        widthSize += getVerticalScrollbarWidth();    }    int childWidth = widthSize - mListPadding.left - mListPadding.right;    boolean didNotInitiallyFit = determineColumns(childWidth);    int childHeight = 0;    int childState = 0;    mItemCount = mAdapter == null ? 0 : mAdapter.getCount();    final int count = mItemCount;    if (count > 0) {        final View child = obtainView(0, mIsScrap);        AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams();        if (p == null) {            p = (AbsListView.LayoutParams) generateDefaultLayoutParams();            child.setLayoutParams(p);        }        p.viewType = mAdapter.getItemViewType(0);        p.forceAdd = true;        int childHeightSpec = getChildMeasureSpec(                MeasureSpec.makeSafeMeasureSpec(MeasureSpec.getSize(heightMeasureSpec),                        MeasureSpec.UNSPECIFIED), 0, p.height);        int childWidthSpec = getChildMeasureSpec(                MeasureSpec.makeMeasureSpec(mColumnWidth, MeasureSpec.EXACTLY), 0, p.width);        child.measure(childWidthSpec, childHeightSpec);        childHeight = child.getMeasuredHeight();        childState = combineMeasuredStates(childState, child.getMeasuredState());        if (mRecycler.shouldRecycleViewType(p.viewType)) {            mRecycler.addScrapView(child, -1);        }    }    if (heightMode == MeasureSpec.UNSPECIFIED) {        heightSize = mListPadding.top + mListPadding.bottom + childHeight +                getVerticalFadingEdgeLength() * 2;    }    if (heightMode == MeasureSpec.AT_MOST) {        int ourSize =  mListPadding.top + mListPadding.bottom;        final int numColumns = mNumColumns;        for (int i = 0; i < count; i += numColumns) {            ourSize += childHeight;            if (i + numColumns < count) {                ourSize += mVerticalSpacing;            }            if (ourSize >= heightSize) {                ourSize = heightSize;                break;            }        }        heightSize = ourSize;    }    if (widthMode == MeasureSpec.AT_MOST && mRequestedNumColumns != AUTO_FIT) {        int ourSize = (mRequestedNumColumns*mColumnWidth)                + ((mRequestedNumColumns-1)*mHorizontalSpacing)                + mListPadding.left + mListPadding.right;        if (ourSize > widthSize || didNotInitiallyFit) {            widthSize |= MEASURED_STATE_TOO_SMALL;        }    }    setMeasuredDimension(widthSize, heightSize);    mWidthMeasureSpec = widthMeasureSpec;}

在GridView的onMeasure()方法中,对mode的三种模式的值分别作了处理。在mode==MeasureSpec处,当mode==MeasureSpec.AT_MOST时,分别对hieghtSize和widthSize进行了设置。代码中提取了顶部和底部的padding以后,再对各行的高度进行叠加,注意到在循环中,如果ourSize大于heighSize,那么高度就设定为heighSize。我们对于GridView的高度设置为MeasureSpec.makeMeasureSpec(size,mode)的最大值即可,GridView会自动设置它的实际高度。
另外需要注意的是,继承不同的View,使用super.onMeasure()传入的specMode的值得出的结果一般是不同,在android.view.View中,使用MeasureSpec.UNSPICED这个模式,如果不给视图设置minWidth和minHeight,那么视图的可视化区域也仅仅是0。

1 0
原创粉丝点击