Android自定义控件知识探索——View的测量模式

KandyReiman 8年前
   <p>一个Android开发者总会遇到自定义控件的问题。要学会自定义控件的开发,最好的方法是将要用到的知识点一个个掌握。当掌握这些分散的知识点就意味着写一个自定义控件会变得容易。本篇文章是对View的测量的探究。</p>    <h2><strong>概念</strong></h2>    <p>View的测量主要掌握三种测量模式:</p>    <p>贴上源码:</p>    <pre>  <code class="language-java">/**           * 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;</code></pre>    <p>这里的测量是对View的width和height进行测量。</p>    <p><strong>UNSPECIFIED</strong>:未指定测量模式。View大小不确定,想要多大有多大。</p>    <p><strong>EXACTLY</strong>: 精确值模式。当控件的width和height设置为具体值或者match_parent时就是这个模式。</p>    <p><strong>AT_MOST</strong>:最大值模式。父布局决定子布局大小(例如:父布局width或者height设置一个默认的精确值,子布局设置为wrap_content。此时子布局的最大width或者height就是父布局的width或者height)。使用这种测量模式的View,设置的一定是wrap_content。</p>    <h2><strong>测试</strong></h2>    <p>接下来通过具体的代码来测试三种测量模式使用的场景:</p>    <p><strong>准备工作:新建一个View。在onMesure()中写测量的代码。</strong></p>    <pre>  <code class="language-java">public class TestMesureView extends View {        public TestMesureView(Context context) {          this(context, null);      }        public TestMesureView(Context context, AttributeSet attrs) {          this(context, attrs, 0);      }        public TestMesureView(Context context, AttributeSet attrs, int defStyleAttr) {          super(context, attrs, defStyleAttr);      }        @Override      protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {          super.onMeasure(widthMeasureSpec, heightMeasureSpec);          int width = 0;          int height = 0;            int widthMode = getMode(widthMeasureSpec);          int heightMode = getMode(heightMeasureSpec);            /** 测量width **/          width = getReallySize(widthMode,widthMeasureSpec);          /** 测量height **/          height = getReallySize(heightMode,heightMeasureSpec);            Log.i("really width mode",logMode(widthMode));          Log.i("really width",String.valueOf(width));            Log.i("really split","---------------------------");            Log.i("really height mode",logMode(heightMode));          Log.i("really height",String.valueOf(height));          setMeasuredDimension(width, height);      }        /**       * 获取测量模式       * @param sizeMeasureSpec       * @return       */      private int getMode(int sizeMeasureSpec){          return MeasureSpec.getMode(sizeMeasureSpec);      }        /**       * 通过测量模式获取真正的Size       * @param mode       * @param sizeMeasureSpec       * @return       */      private int getReallySize(int mode,int sizeMeasureSpec){          int specSize = 0;          switch (mode){              case MeasureSpec.AT_MOST:              case MeasureSpec.EXACTLY:                  specSize = MeasureSpec.getSize(sizeMeasureSpec);                  break;              case MeasureSpec.UNSPECIFIED:                  specSize = sizeMeasureSpec;                  break;          }          return specSize;      }        private String logMode(int mode){          switch (mode){              case MeasureSpec.AT_MOST:                  return "AT_MOST";              case MeasureSpec.EXACTLY:                  return "EXACTLY";              case MeasureSpec.UNSPECIFIED:                  return "UNSPECIFIED";          }          return "";      }    }</code></pre>    <p>如上代码:</p>    <p>getMode() : 获取测量模式的方法,核心方法为 MeasureSpec.getMode(sizeMeasureSpec); 将onMeasure(int widthMeasureSpec, int heightMeasureSpec)。中两个参数分别传入就可分别得到width的测量模式和height的测量模式。</p>    <p>getReallySize(): 获取测量到的值的方法。核心方法为 MeasureSpec.getSize(sizeMeasureSpec);将onMeasure(int widthMeasureSpec, int heightMeasureSpec)。中两个参数分别传入就可分别得到width的真实大小和height的真实大小。</p>    <p><strong>1、EXACTLY</strong></p>    <p><strong>a、将layout_width,layout_height 都设为 match_parent。</strong></p>    <pre>  <code class="language-java"><?xml version="1.0" encoding="utf-8"?>  <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"      android:layout_width="match_parent"      android:layout_height="match_parent"      android:orientation="vertical">      <com.mg.axe.androiddevelop.view.TestMesureView          android:background="#33ee33"          android:layout_width="match_parent"          android:layout_height="match_parent" />  </LinearLayout></code></pre>    <pre>  <code class="language-java">10-10 22:25:21.271 8207-8207/com.mg.axe.androiddevelop I/really width mode: EXACTLY  10-10 22:25:21.271 8207-8207/com.mg.axe.androiddevelop I/really width: 1080  10-10 22:25:21.271 8207-8207/com.mg.axe.androiddevelop I/really split: ---------------------------  10-10 22:25:21.271 8207-8207/com.mg.axe.androiddevelop I/really height mode: EXACTLY  10-10 22:25:21.271 8207-8207/com.mg.axe.androiddevelop I/really height: 1860</code></pre>    <p style="text-align:center"><img src="https://simg.open-open.com/show/5dd556f5617e41a689e10878387eadd8.png"></p>    <p style="text-align:center">运行结果</p>    <p>分析Log和截图:</p>    <p>通过运行结果可以看到view充满整个屏幕。</p>    <p>分析Log可以知道,两者的测量模式都是 <strong>EXACTLY</strong></p>    <p>手机的分辨率为1920*1080 , width为1080 , height为1860(因为有状态栏所以不是1920)</p>    <p><strong>b、指定精确大小,将layout_width,layout_height 都设为 100dp。</strong></p>    <pre>  <code class="language-java"><?xml version="1.0" encoding="utf-8"?>  <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"      android:layout_width="match_parent"      android:layout_height="match_parent"      android:orientation="vertical">      <com.mg.axe.androiddevelop.view.TestMesureView          android:background="#33ee33"          android:layout_width="100dp"          android:layout_height="100dp" />    </LinearLayout></code></pre>    <pre>  <code class="language-java">10-11 00:13:23.511 763-763/com.mg.axe.androiddevelop I/really width mode: EXACTLY  10-11 00:13:23.511 763-763/com.mg.axe.androiddevelop I/really width: 300  10-11 00:13:23.511 763-763/com.mg.axe.androiddevelop I/really split: ---------------------------  10-11 00:13:23.511 763-763/com.mg.axe.androiddevelop I/really height mode: EXACTLY  10-11 00:13:23.511 763-763/com.mg.axe.androiddevelop I/really height: 300</code></pre>    <p style="text-align:center"><img src="https://simg.open-open.com/show/2518127acdfeffe721b65d7d660fee58.png"></p>    <p style="text-align:center">运行结果</p>    <p>分析:</p>    <p>分析Log可以知道,两者的测量模式都是 <strong>EXACTLY</strong></p>    <p>获取到的width和height都为 300. (系统测量会将单位转为px)</p>    <p><strong>2、AT_MOST</strong></p>    <p><strong>a、父布局将layout_width,layout_height 都设为 match_parent</strong></p>    <p><strong>将子布局的layout_width,layout_height 都设为 wrap_content</strong></p>    <pre>  <code class="language-java"><?xml version="1.0" encoding="utf-8"?>  <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"      android:layout_width="match_parent"      android:layout_height="match_parent"      android:orientation="vertical">      <com.mg.axe.androiddevelop.view.TestMesureView          android:layout_width="wrap_content"          android:layout_height="wrap_content"          android:background="#33ee33"/>  </LinearLayout></code></pre>    <pre>  <code class="language-java">10-11 01:27:32.656 29513-29513/com.mg.axe.androiddevelop I/really width mode: AT_MOST  10-11 01:27:32.656 29513-29513/com.mg.axe.androiddevelop I/really width: 1080  10-11 01:27:32.656 29513-29513/com.mg.axe.androiddevelop I/really split: ---------------------------  10-11 01:27:32.656 29513-29513/com.mg.axe.androiddevelop I/really height mode: AT_MOST  10-11 01:27:32.656 29513-29513/com.mg.axe.androiddevelop I/really height: 1860</code></pre>    <p>分析:</p>    <p>子布局的宽高测量模式都为: <strong>AT_MOST</strong></p>    <p>父布局的layout_width和layout_height都为match_parent,父布局的宽高约为屏幕的宽高。</p>    <p>子布局的layout_width和layout_height都为wrap_content,子布局大小不固定,但是最大值受父布局大小影响。这种情况的测量模式就是 <strong>AT_MOST</strong> 。</p>    <p><strong>b、将父布局设置为指定大小,需要测量的布局将layout_width,layout_height 都设为 wrap_content</strong></p>    <pre>  <code class="language-java"><?xml version="1.0" encoding="utf-8"?>  <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"      android:layout_width="100dp"      android:layout_height="100dp"      android:orientation="vertical">      <com.mg.axe.androiddevelop.view.TestMesureView          android:layout_width="wrap_content"          android:layout_height="wrap_content"          android:background="#33ee33" />  </LinearLayout></code></pre>    <pre>  <code class="language-java">10-11 01:30:41.126 2423-2423/com.mg.axe.androiddevelop I/really width mode: AT_MOST  10-11 01:30:41.126 2423-2423/com.mg.axe.androiddevelop I/really width: 300  10-11 01:30:41.126 2423-2423/com.mg.axe.androiddevelop I/really split: ---------------------------  10-11 01:30:41.126 2423-2423/com.mg.axe.androiddevelop I/really height mode: AT_MOST  10-11 01:30:41.126 2423-2423/com.mg.axe.androiddevelop I/really height: 300</code></pre>    <p>分析:</p>    <p>这种情况和上面a测试的结论一样。子布局大小不固定,但是最大值受父布局大小影响。这种情况的测量模式就是 <strong>EXACTLY</strong> 。</p>    <p><strong>c、测试出一种特殊的情况</strong></p>    <p><strong>当父布局是RelativeLayout,子布局的layout_width,layout_height 都设为 wrap_content时,子布局的width测量模式为EXACTLY</strong></p>    <pre>  <code class="language-java"><?xml version="1.0" encoding="utf-8"?>  <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"      android:layout_width="100dp"      android:layout_height="100dp"      android:orientation="vertical">          <com.mg.axe.androiddevelop.view.TestMesureView          android:layout_width="wrap_content"          android:layout_height="wrap_content"          android:background="#33ee33" />  </RelativeLayout></code></pre>    <pre>  <code class="language-java">10-11 09:05:26.970 14275-14275/com.mg.axe.androiddevelop I/really width mode: EXACTLY  10-11 09:05:26.970 14275-14275/com.mg.axe.androiddevelop I/really width: 300  10-11 09:05:26.970 14275-14275/com.mg.axe.androiddevelop I/really split: ---------------------------  10-11 09:05:26.970 14275-14275/com.mg.axe.androiddevelop I/really height mode: AT_MOST  10-11 09:05:26.970 14275-14275/com.mg.axe.androiddevelop I/really height: 300</code></pre>    <p>分析:</p>    <p>我暂时也不知道子View的宽的测量模式是EXACTLY。这应该是一种特殊情况。</p>    <p>这里再次做提醒:如果这个View的测量模式为AT_MOST,这个View一定设置了wrap_content</p>    <p><strong>3、UNSPECIFIED</strong></p>    <p><strong>a、添加父布局scrollview,将测试的view放在里面</strong></p>    <pre>  <code class="language-java"><?xml version="1.0" encoding="utf-8"?>  <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"      android:layout_width="match_parent"      android:layout_height="match_parent"      android:orientation="vertical">        <ScrollView          android:layout_width="match_parent"          android:layout_height="match_parent">        <com.mg.axe.androiddevelop.view.TestMesureView          android:layout_width="wrap_content"          android:layout_height="wrap_content"          android:background="#33ee33"/>      </ScrollView>  </LinearLayout></code></pre>    <pre>  <code class="language-java">10-11 01:18:23.566 15113-15113/com.mg.axe.androiddevelop I/really width mode: AT_MOST  10-11 01:18:23.566 15113-15113/com.mg.axe.androiddevelop I/really width: 1080  10-11 01:18:23.566 15113-15113/com.mg.axe.androiddevelop I/really split: ---------------------------  10-11 01:18:23.566 15113-15113/com.mg.axe.androiddevelop I/really height mode: UNSPECIFIED  10-11 01:18:23.566 15113-15113/com.mg.axe.androiddevelop I/really height: 0</code></pre>    <p>分析:</p>    <p>这里我们只要分析height就行了,这种情况下 父布局ScrollView的子view的高度是不固定的,想要多大就可多大。所以这里height的测量模式为 <strong>UNSPECIFIED</strong></p>    <h2><strong>实际应用</strong></h2>    <p><strong>1、先测量再绘制</strong></p>    <p>在写自定义控件时,涉及到测量绘制的。一般是先测量再绘制。</p>    <p><strong>2、测量方法</strong></p>    <p>这个是上面写的方法。是参照源码写的。</p>    <pre>  <code class="language-java">private int getReallySize(int mode,int sizeMeasureSpec){          int specSize = 0;          switch (mode){              case MeasureSpec.AT_MOST:              case MeasureSpec.EXACTLY:                  specSize = MeasureSpec.getSize(sizeMeasureSpec);                  break;              case MeasureSpec.UNSPECIFIED:                  specSize = sizeMeasureSpec;                  break;          }          return specSize;      }</code></pre>    <p>在View的源码中有一个getDefaultSize的方法。</p>    <pre>  <code class="language-java">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;      }  }</code></pre>    <p><strong>3、测量完毕之后一定要调用setMeasuredDimension(width, height);</strong></p>    <p>要调用setMeasuredDimension或者super.onMeasure来设置自身的mMeasuredWidth和mMeasuredHeight,否则,就会抛出异常.</p>    <p> </p>    <p>来自:http://www.jianshu.com/p/85548a440cd2</p>    <p> </p>