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>