自定义view,你真的理解onMeasure了吗?

IsabelleChr 8年前
   <h3><strong>疑惑</strong></h3>    <p>当android内置view无法实现我们的需求,此刻我们需要自定义view来实现定制需求效果。自定义view主要是通过onMeasure、onLayout、onDraw等实现的。然而很多童鞋在onMeasure中存在很多疑惑,我们已经在xml布局中指明了宽高尺寸大小,为何还要在自定义view中获取宽高并指定宽高?</p>    <h3><strong>实际</strong></h3>    <p>在我们写布局xml文件的时候,layout_width、layout_height等不需要写具体的尺寸大小,可以是wrap_content(包裹内容)和match_content(填充内容),它们并没有指定具体的尺寸大小,但是我们绘制view的时候是需要具体的尺寸大小,所以我们需要去处理并设置尺寸大小。</p>    <h3><strong>onMeasure是什么?</strong></h3>    <p>onMeasure,根据谷歌翻译字面意思是测量,继承view写这个方法的时候,我们可以发现此方法有两个参数分别为widthMeasureSpec,heightMeasureSpec。这两个参数是干嘛的呢?根据谷歌翻译字面意思分别是宽度测量规格、高度测量规格。这两个参数包含了宽度和高度的信息。具体信息是什么呢?勤快地童鞋会去百度查,主要是测量模式和尺寸大小。</p>    <p><strong>测量模式有三种</strong></p>    <ul>     <li> <p>UNSPECIFIED</p>      <ul>       <li>父容器没有对当前View有任何限制,当前View可以任意取尺寸</li>      </ul> </li>     <li> <p>EXACTLY</p>      <ul>       <li>当前的尺寸就是当前View应该取的尺寸</li>      </ul> </li>     <li> <p>AT_MOST</p>      <ul>       <li>前尺寸是当前View能取的最大尺寸</li>      </ul> </li>    </ul>    <p>那此测量模式跟我们写的xml布局文件中wrap_content、match_parent、固定的尺寸有什么对应关系呢?</p>    <ul>     <li> <p>wrap_content</p>      <ul>       <li>对应AT_MOST,如何理解?就是我们想要将大小设置为包裹我们的view内容,那么尺寸大小就是父View给我们作为参考的尺寸,只要不超过这个尺寸就可以啦,具体尺寸就根据我们的需求去设定。</li>      </ul> </li>     <li> <p>match_parent</p>      <ul>       <li>对应EXACTLY,如何理解?就是要利用父View给我们提供的所有剩余空间,而父View剩余空间是确定的,也就是这个测量模式的整数里面存放的尺寸。</li>      </ul> </li>     <li> <p>固定尺寸(如50dp)</p>      <ul>       <li>对应EXACTLY,如何理解?用户自己指定了尺寸大小,我们就不用再去干涉了,当然是以指定的大小为主啦。</li>      </ul> </li>    </ul>    <h3><strong>实践</strong></h3>    <p>理论终究是理论,实践才能加深我们对此理解,接下来我们就自定义一个CustomView来验证理论是否正确?</p>    <p>此处我们需要自定义一个默认尺寸大小布局属性,该怎么做呢?</p>    <p>首先我们需要在res/values/styles.xml文件里面声明一个我们自定义的属性:</p>    <pre>  <code class="language-java"><resources>      <!--name为声明的"属性集合"名,可以随便取,但是最好是设置为跟我们的View一样的名称-->      <declare-styleable name="CustomView">          <!--声明我们的属性,名称为default_size,取值类型为尺寸类型(dp,px等)-->          <attr name="default_size" format="dimension" />      </declare-styleable>  </resources></code></pre>    <p>然后在布局文件用我们的自定义的属性,如下:</p>    <pre>  <code class="language-java"><?xml version="1.0" encoding="utf-8"?>  <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"      xmlns:tools="http://schemas.android.com/tools"      xmlns:ctv = "http://schemas.android.com/apk/res-auto"      android:layout_width="match_parent"      android:layout_height="match_parent"      android:paddingBottom="@dimen/activity_vertical_margin"      android:paddingLeft="@dimen/activity_horizontal_margin"      android:paddingRight="@dimen/activity_horizontal_margin"      android:paddingTop="@dimen/activity_vertical_margin"      tools:context="cn.jianke.customview.MainActivity">      <cn.jianke.customview.CustomView              android:layout_width="wrap_content"              android:layout_height="100dp"              android:background="@color/colorAccent"              ctv:default_size="50dp"/>  </RelativeLayout></code></pre>    <p>注意:需要在根标签(RelativeLayout )里面设定命名空间,命名空间名称可以随便取,比如ctv,命名空间后面取得值是固定的:</p>    <p>最后在我们自定义View里面将我们自定义的属性值取出来,在构造函数中,有个AttributeSet属性,通过它我们可以把布局里面属性取出来,代码如下:</p>    <pre>  <code class="language-java">public CustomView(Context context, AttributeSet attrs) {          super(context, attrs);          //第二个参数就是我们在styles.xml文件中的<declare-styleable>标签          //即属性集合的标签,在R文件中名称为R.styleable+name          TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CustomView);            //第一个参数为属性集合里面的属性,R文件名称:R.styleable+属性集合名称+下划线+属性名称          //第二个参数为,如果没有设置这个属性,则设置的默认的值          defaultSize = a.getDimensionPixelSize(R.styleable.CustomView_default_size, 100);            //最后记得将TypedArray对象回收          a.recycle();      }</code></pre>    <p>接下来我们就可以在onMeasure计算该view的大小,并在特定的测量模式下设置特定的尺寸大小值。</p>    <p>onMeasure代码如下:</p>    <pre>  <code class="language-java">@Override      protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {          super.onMeasure(widthMeasureSpec, heightMeasureSpec);          // 获取合适的宽度值          int width = getProperSize(defaultSize, widthMeasureSpec);          // 获取合适的高度值          int height = getProperSize(defaultSize, heightMeasureSpec);          // 设置宽高尺寸大小值,此方法决定view最终的尺寸大小          setMeasuredDimension(width, height);      }            /**       * 获取合适的大小       * @author leibing       * @createTime 2016/09/19       * @lastModify 2016/09/19       * @param defaultSize 默认大小       * @param measureSpec 测量规格       * @return       */      private int getProperSize(int defaultSize, int measureSpec){          int properSize = defaultSize;          int mode = MeasureSpec.getMode(measureSpec);          int size = MeasureSpec.getSize(measureSpec);          switch (mode){              case MeasureSpec.UNSPECIFIED:                  // 没有指定大小,设置为默认大小                  properSize = defaultSize;                  break;              case MeasureSpec.EXACTLY:                  // 固定大小,无需更改其大小                  properSize = size;                  break;              case MeasureSpec.AT_MOST:                  // 此处该值可以取小于等于最大值的任意值,此处取最大值的1/4                  properSize = size / 4;          }            return properSize;      }</code></pre>    <p>效果图如下(宽为wrap_content(宽度最大值的1/4),高度100dp):</p>    <p><img src="https://simg.open-open.com/show/696162e70eca19d65e8ab9a0dfa69d7a.gif"></p>    <p style="text-align:center">CustomView.gif</p>    <p>大家理解onMeasure了吗?是不是很简单?大家动动手,一切会变得简单。</p>    <p> </p>    <p> </p>    <p>来自:http://www.jianshu.com/p/df387b08c76f</p>    <p> </p>