轉自:http://blog.csdn.net/aigestudio/article/details/42989325
要在數量上統計中國菜的品種,在地域上毫無爭議地劃分菜系,在今天,是一件幾乎不可能完成的事……Cut…………抱歉……忘吃藥了,再來一遍。如果非要對自定義控件的流程進行一個簡單的劃分,我會嘗試將其分爲三大部分:控件的繪製、控件的測量和控件的交互行爲。前面我們用了六節的篇幅和一個翻頁的例子來對控件的繪製有了一個全新的認識但是我們所做出的所有例子都是不完美的,爲什麼這麼說呢,還是先來看個sample:
- /**
- *
- * @author AigeStudio {@link http://blog.csdn.net/aigestudio}
- * @since 2015/1/12
- *
- */
- public class ImgView extends View {
- private Bitmap mBitmap;// 位圖對象
- public ImgView(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
- @Override
- protected void onDraw(Canvas canvas) {
- // 繪製位圖
- canvas.drawBitmap(mBitmap, 0, 0, null);
- }
- /**
- * 設置位圖
- *
- * @param bitmap
- * 位圖對象
- */
- public void setBitmap(Bitmap bitmap) {
- this.mBitmap = bitmap;
- }
- }
- /**
- * 主界面
- *
- * @author Aige {@link http://blog.csdn.net/aigestudio}
- * @since 2014/11/17
- */
- public class MainActivity extends Activity {
- private ImgView mImgView;
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
- mImgView = (ImgView) findViewById(R.id.main_pv);
- Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.lovestory);
- mImgView.setBitmap(bitmap);
- }
- }
很簡單對吧,可是上面的代碼其實是有個問題的,至於什麼問題?我們待會再說,就看你通過前面我們的學習能不能發現了。這一節我們重點是控件的測量,大家不知道注意沒有,這個系列文章的命名我用了“控件”而非“View”其實目的就是說明我們的控件不僅包括View也應該包含ViewGroup,當然你也可以以官方的方式將其分爲控件和佈局,不過我更喜歡View和ViewGroup,好了廢話不說,我們先來看看View的測量方式,上面的代碼中MainActivity對應的佈局文件如下:
- <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:background="#FFFFFFFF"
- android:orientation="vertical" >
- <com.aigestudio.customviewdemo.views.ImgView
- android:id="@+id/main_pv"
- android:layout_width="match_parent"
- android:layout_height="match_parent" />
- </LinearLayout>
- <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:background="#FFFFFFFF"
- android:orientation="vertical" >
- <com.aigestudio.customviewdemo.views.ImgView
- android:id="@+id/main_pv"
- android:layout_width="match_parent"
- android:layout_height="match_parent" />
- <Button
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="AigeStudio" />
- <TextView
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="AigeStudio" />
- </LinearLayout>
- <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:background="#FFFFFFFF"
- android:orientation="vertical" >
- <com.aigestudio.customviewdemo.views.ImgView
- android:id="@+id/main_pv"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content" />
- <!-- ……省略一些代碼…… -->
- </LinearLayout>
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- }
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
- }
- public void setContentView(int layoutResID) {
- getWindow().setContentView(layoutResID);
- initActionBar();
- }
- public Window getWindow() {
- return mWindow;
- }
- final void attach(Context context, ActivityThread aThread,
- // 此處省去一些代碼……
- mWindow = PolicyManager.makeNewWindow(this);
- mWindow.setCallback(this);
- mWindow.getLayoutInflater().setPrivateFactory(this);
- if (info.softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED) {
- mWindow.setSoftInputMode(info.softInputMode);
- }
- if (info.uiOptions != 0) {
- mWindow.setUiOptions(info.uiOptions);
- }
- // 此處省去巨量代碼……
- }
- public final class PolicyManager {
- private static final String POLICY_IMPL_CLASS_NAME =
- "com.android.internal.policy.impl.Policy";
- private static final IPolicy sPolicy;
- static {
- try {
- Class policyClass = Class.forName(POLICY_IMPL_CLASS_NAME);
- sPolicy = (IPolicy)policyClass.newInstance();
- } catch (ClassNotFoundException ex) {
- throw new RuntimeException(
- POLICY_IMPL_CLASS_NAME + " could not be loaded", ex);
- } catch (InstantiationException ex) {
- throw new RuntimeException(
- POLICY_IMPL_CLASS_NAME + " could not be instantiated", ex);
- } catch (IllegalAccessException ex) {
- throw new RuntimeException(
- POLICY_IMPL_CLASS_NAME + " could not be instantiated", ex);
- }
- }
- // 省去構造方法……
- public static Window makeNewWindow(Context context) {
- return sPolicy.makeNewWindow(context);
- }
- // 省去無關代碼……
- }
- public Window makeNewWindow(Context context) {
- return new PhoneWindow(context);
- }
- public abstract class Window {
- // 省去不可估量的代碼……
- public abstract void setContentView(int layoutResID);
- public abstract void setContentView(View view);
- public abstract void setContentView(View view, ViewGroup.LayoutParams params);
- // 省去數以億計的代碼……
- }
- public class PhoneWindow extends Window implements MenuBuilder.Callback {
- // 省去草泥馬個代碼……
- @Override
- public void setContentView(int layoutResID) {
- if (mContentParent == null) {
- installDecor();
- } else {
- mContentParent.removeAllViews();
- }
- mLayoutInflater.inflate(layoutResID, mContentParent);
- final Callback cb = getCallback();
- if (cb != null && !isDestroyed()) {
- cb.onContentChanged();
- }
- }
- @Override
- public void setContentView(View view) {
- setContentView(view, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
- }
- @Override
- public void setContentView(View view, ViewGroup.LayoutParams params) {
- if (mContentParent == null) {
- installDecor();
- } else {
- mContentParent.removeAllViews();
- }
- mContentParent.addView(view, params);
- final Callback cb = getCallback();
- if (cb != null && !isDestroyed()) {
- cb.onContentChanged();
- }
- }
- // 省去法克魷個代碼……
- }
- private void installDecor() {
- if (mDecor == null) {
- mDecor = generateDecor();
- // 省省省……
- }
- if (mContentParent == null) {
- mContentParent = generateLayout(mDecor);
- // 省省省……
- }
- // 省省省……
- }
- protected ViewGroup generateLayout(DecorView decor) {
- // 省去巨量代碼……
- ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
- // 省去一些代碼……
- }
說了大半天才理清這個小關係,但是我們還沒說到重點…………………………就是widthMeasureSpec和heightMeasureSpec究竟是從哪來的……………………如果我們不做上面的一個分析,很多童鞋壓根無從下手,有了上面一個分析,我們知道我們界面的真正根視圖應該是DecorView,那麼我們的widthMeasureSpec和heightMeasureSpec應該從這裏或者更上一層PhoneWindow傳遞進來對吧,但是DecorView是FrameLayout的一個實例,在FrameLayout的onMeasure中我們確實有對子元素的測量,但是問題是FrameLayout:onMeasure方法中的widthMeasureSpec和heightMeasureSpec又是從何而來呢?追溯上去我們又回到了View…………………………………………………………不瞭解Android
GUI框架的童鞋邁出的第一步就被無情地煽了回去。其實在Android中我們可以在很多方面看到類似MVC架構的影子,比如最最常見的就是我們的xml界面佈局——Activity等組件——model數據之間的關係,而在整個GUI的框架中,我們也可以對其做出類似的規劃,View在設計過程中就註定了其只會對顯示數據進行處理比如我們的測量佈局和繪製還有動畫等等,而承擔Controller控制器重任的是誰呢?在Android中這一功能由ViewRootImpl承擔,我們在前面提到過這個類,其負責的東西很多,比如我們窗口的顯示、用戶的輸入輸出當然還有關於處理我們繪製流程的方法:
- private void performTraversals() {
- // ………………啦啦啦啦………………
- }
- private void performTraversals() {
- // ………省略宇宙塵埃數量那麼多的代碼………
- if (!mStopped) {
- // ……省略一些代碼
- int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
- int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
- // ……省省省
- performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
- }
- // ………省略人體細胞數量那麼多的代碼………
- }
- private static int getRootMeasureSpec(int windowSize, int rootDimension) {
- int measureSpec;
- switch (rootDimension) {
- case ViewGroup.LayoutParams.MATCH_PARENT:
- // Window不能調整其大小,強制使根視圖大小與Window一致
- measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
- break;
- case ViewGroup.LayoutParams.WRAP_CONTENT:
- // Window可以調整其大小,爲根視圖設置一個最大值
- measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
- break;
- default:
- // Window想要一個確定的尺寸,強制將根視圖的尺寸作爲其尺寸
- measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
- break;
- }
- return measureSpec;
- }
至此,我們算是真正接觸到根視圖的測量規格,爾後這個規格會被由上至下傳遞下去,並由當前view與其父容器共同作用決定最終的測量大小,在View與ViewGroup遞歸調用實現測量的過程中有幾個重要的方法,對於View而言則是measure方法:
- public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
- // 省略部分代碼……
- /*
- * 判斷當前mPrivateFlags是否帶有PFLAG_FORCE_LAYOUT強制佈局標記
- * 判斷當前widthMeasureSpec和heightMeasureSpec是否發生了改變
- */
- if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||
- widthMeasureSpec != mOldWidthMeasureSpec ||
- heightMeasureSpec != mOldHeightMeasureSpec) {
- // 如果發生了改變表示需要重新進行測量此時清除掉mPrivateFlags中已測量的標識位PFLAG_MEASURED_DIMENSION_SET
- mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
- resolveRtlPropertiesIfNeeded();
- int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 :
- mMeasureCache.indexOfKey(key);
- if (cacheIndex < 0 || sIgnoreMeasureCache) {
- // 測量View的尺寸
- onMeasure(widthMeasureSpec, heightMeasureSpec);
- mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
- } else {
- long value = mMeasureCache.valueAt(cacheIndex);
- setMeasuredDimension((int) (value >> 32), (int) value);
- mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
- }
- /*
- * 如果mPrivateFlags裏沒有表示已測量的標識位PFLAG_MEASURED_DIMENSION_SET則會拋出異常
- */
- if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
- throw new IllegalStateException("onMeasure() did not set the"
- + " measured dimension by calling"
- + " setMeasuredDimension()");
- }
- // 如果已測量View那麼就可以往mPrivateFlags添加標識位PFLAG_LAYOUT_REQUIRED表示可以進行佈局了
- mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
- }
- // 最後存儲測量完成的測量規格
- mOldWidthMeasureSpec = widthMeasureSpec;
- mOldHeightMeasureSpec = heightMeasureSpec;
- mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
- (long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
- }
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
- getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
- }
- protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
- // 省去部分代碼……
- // 設置測量後的寬高
- mMeasuredWidth = measuredWidth;
- mMeasuredHeight = measuredHeight;
- // 重新將已測量標識位存入mPrivateFlags標識測量的完成
- mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
- }
- protected int getSuggestedMinimumWidth() {
- return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
- }
- public static int getDefaultSize(int size, int measureSpec) {
- // 將我們獲得的最小值賦給result
- int result = size;
- // 從measureSpec中解算出測量規格的模式和尺寸
- 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;
- }
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- // 設置測量尺寸
- setMeasuredDimension(250, 250);
- }
當然這樣不好,用Android官方的話來說就是太過“專政”,因爲它完全摒棄了父容器的意願,完全由自己決定了大小,如果大家逛blog看技術文章或者聽別人討論常常會聽到別人這麼說view的最終測量尺寸是由view本身何其父容器共同決定的,至於如何共同決定我們呆會再說,這裏我們先看看如何能在一定程度上順應爹的“意願”呢?從View默認的測量模式中我們可以看到它頻繁使用了一個叫做MeasureSpec的類,而在ViewRootImpl中呢也有大量用到該類,該類的具體說明大家可以圍觀我早期的一篇文章:http://blog.csdn.net/aigestudio/article/details/38636531,裏面有對MeasureSpec類的詳細說明,這裏我就簡單概述下MeasureSpec類中的三個Mode常量值的意義,其中UNSPECIFIED表示未指定,爹不會對兒子作任何的束縛,兒子想要多大都可以;EXACTLY表示完全的,意爲兒子多大爹心裏有數,爹早已算好了;AT_MOST表示至多,爹已經爲兒子設置好了一個最大限制,兒子你不能比這個值大,不能再多了!父容器所謂的“意圖”其實就由上述三個常量值表現,既然如此我們就該對這三個Mode常量做一個判斷纔行,不然怎麼知道爹的意圖呢:
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- // 聲明一個臨時變量來存儲計算出的測量值
- int resultWidth = 0;
- // 獲取寬度測量規格中的mode
- int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
- // 獲取寬度測量規格中的size
- int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
- /*
- * 如果爹心裏有數
- */
- if (modeWidth == MeasureSpec.EXACTLY) {
- // 那麼兒子也不要讓爹難做就取爹給的大小吧
- resultWidth = sizeWidth;
- }
- /*
- * 如果爹心裏沒數
- */
- else {
- // 那麼兒子可要自己看看自己需要多大了
- resultWidth = mBitmap.getWidth();
- /*
- * 如果爹給兒子的是一個限制值
- */
- if (modeWidth == MeasureSpec.AT_MOST) {
- // 那麼兒子自己的需求就要跟爹的限制比比看誰小要誰
- resultWidth = Math.min(resultWidth, sizeWidth);
- }
- }
- int resultHeight = 0;
- int modeHeight = MeasureSpec.getMode(heightMeasureSpec);
- int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);
- if (modeHeight == MeasureSpec.EXACTLY) {
- resultHeight = sizeHeight;
- } else {
- resultHeight = mBitmap.getHeight();
- if (modeHeight == MeasureSpec.AT_MOST) {
- resultHeight = Math.min(resultHeight, sizeHeight);
- }
- }
- // 設置測量尺寸
- setMeasuredDimension(resultWidth, resultHeight);
- }
如我所說,控件的實際大小需要根據我們的實際需求去計算,這裏我更改一下xml爲我們的ImgView加一個內邊距值:
- <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:background="#FFFFFFFF"
- android:orientation="vertical" >
- <com.aigestudio.customviewdemo.views.ImgView
- android:id="@+id/main_pv"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:padding="20dp" />
- <Button
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="AigeStudio" />
- <TextView
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="AigeStudio" />
- </LinearLayout>
- resultWidth = mBitmap.getWidth() + getPaddingLeft() + getPaddingRight();
- resultHeight = mBitmap.getHeight() + getPaddingTop() + getPaddingBottom();
誒、等等,好像不對啊,上邊距和左邊距爲什麼沒有了?原因很簡單,因爲我們在繪製時並沒有考慮到Padding的影響,下面我們更改一下繪製邏輯:
- @Override
- protected void onDraw(Canvas canvas) {
- // 繪製位圖
- canvas.drawBitmap(mBitmap, getPaddingLeft(), getPaddingTop(), null);
- }
很多朋友問那Margin外邊距呢??淡定,外邊距輪不到view來算,Andorid將其封裝在LayoutParams內交由父容器統一處理。很多時候我們的控件往往不只是一張簡單的圖片那麼乏味,比如類似圖標的效果:
一個圖標常常除了一張圖片外底部還有一個title,這時我們的測量邏輯就應該做出相應的改變了,這裏我用一個新的IconView去做:
- /**
- *
- * @author AigeStudio {@link http://blog.csdn.net/aigestudio}
- * @since 2015/1/13
- *
- */
- public class IconView extends View {
- private Bitmap mBitmap;// 位圖
- private TextPaint mPaint;// 繪製文本的畫筆
- private String mStr;// 繪製的文本
- private float mTextSize;// 畫筆的文本尺寸
- /**
- * 寬高枚舉類
- *
- * @author AigeStudio {@link http://blog.csdn.net/aigestudio}
- *
- */
- private enum Ratio {
- WIDTH, HEIGHT
- }
- public IconView(Context context, AttributeSet attrs) {
- super(context, attrs);
- // 計算參數
- calArgs(context);
- // 初始化
- init();
- }
- /**
- * 參數計算
- *
- * @param context
- * 上下文環境引用
- */
- private void calArgs(Context context) {
- // 獲取屏幕寬
- int sreenW = MeasureUtil.getScreenSize((Activity) context)[0];
- // 計算文本尺寸
- mTextSize = sreenW * 1 / 10F;
- }
- /**
- * 初始化
- */
- private void init() {
- /*
- * 獲取Bitmap
- */
- if (null == mBitmap) {
- mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.logo);
- }
- /*
- * 爲mStr賦值
- */
- if (null == mStr || mStr.trim().length() == 0) {
- mStr = "AigeStudio";
- }
- /*
- * 初始化畫筆並設置參數
- */
- mPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG | Paint.LINEAR_TEXT_FLAG);
- mPaint.setColor(Color.LTGRAY);
- mPaint.setTextSize(mTextSize);
- mPaint.setTextAlign(Paint.Align.CENTER);
- mPaint.setTypeface(Typeface.DEFAULT_BOLD);
- }
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- // 設置測量後的尺寸
- setMeasuredDimension(getMeasureSize(widthMeasureSpec, Ratio.WIDTH), getMeasureSize(heightMeasureSpec, Ratio.HEIGHT));
- }
- /**
- * 獲取測量後的尺寸
- *
- * @param measureSpec
- * 測量規格
- * @param ratio
- * 寬高標識
- * @return 寬或高的測量值
- */
- private int getMeasureSize(int measureSpec, Ratio ratio) {
- // 聲明臨時變量保存測量值
- int result = 0;
- /*
- * 獲取mode和size
- */
- int mode = MeasureSpec.getMode(measureSpec);
- int size = MeasureSpec.getSize(measureSpec);
- /*
- * 判斷mode的具體值
- */
- switch (mode) {
- case MeasureSpec.EXACTLY:// EXACTLY時直接賦值
- result = size;
- break;
- default:// 默認情況下將UNSPECIFIED和AT_MOST一併處理
- if (ratio == Ratio.WIDTH) {
- float textWidth = mPaint.measureText(mStr);
- result = ((int) (textWidth >= mBitmap.getWidth() ? textWidth : mBitmap.getWidth())) + getPaddingLeft() + getPaddingRight();
- } else if (ratio == Ratio.HEIGHT) {
- result = ((int) ((mPaint.descent() - mPaint.ascent()) * 2 + mBitmap.getHeight())) + getPaddingTop() + getPaddingBottom();
- }
- /*
- * AT_MOST時判斷size和result的大小取小值
- */
- if (mode == MeasureSpec.AT_MOST) {
- result = Math.min(result, size);
- }
- break;
- }
- return result;
- }
- @Override
- protected void onDraw(Canvas canvas) {
- /*
- * 繪製
- * 參數就不做單獨處理了因爲只會Draw一次不會頻繁調用
- */
- canvas.drawBitmap(mBitmap, getWidth() / 2 - mBitmap.getWidth() / 2, getHeight() / 2 - mBitmap.getHeight() / 2, null);
- canvas.drawText(mStr, getWidth() / 2, mBitmap.getHeight() + getHeight() / 2 - mBitmap.getHeight() / 2 - mPaint.ascent(), mPaint);
- }
- }
- <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:background="#FFFFFFFF"
- android:orientation="vertical" >
- <com.aigestudio.customviewdemo.views.IconView
- android:id="@+id/main_pv"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:padding="50dp" />
- <Button
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="AigeStudio" />
- <TextView
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="AigeStudio" />
- </LinearLayout>
好了就先這樣吧,上面我們曾說過View的測量大小是由View和其父容器共同決定的,但是上述源碼的分析中我們其實並沒有體現,因爲它們都在ViewGroup中,這裏我們就要涉及ViewGroup中與測量相關的另外幾個方法:measureChildren、measureChild和measureChildWithMargins還有getChildMeasureSpec,見名知意這幾個方法都跟ViewGroup測量子元素有關,其中measureChildWithMargins和measureChildren類似只是加入了對Margins外邊距的處理,ViewGroup提供對子元素測量的方法從measureChildren開始:
- protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
- final int size = mChildrenCount;
- final View[] children = mChildren;
- for (int i = 0; i < size; ++i) {
- final View child = children[i];
- if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
- measureChild(child, widthMeasureSpec, heightMeasureSpec);
- }
- }
- }
- protected void measureChild(View child, int parentWidthMeasureSpec,
- int parentHeightMeasureSpec) {
- // 獲取子元素的佈局參數
- final LayoutParams lp = child.getLayoutParams();
- /*
- * 將父容器的測量規格已經上下和左右的邊距還有子元素本身的佈局參數傳入getChildMeasureSpec方法計算最終測量規格
- */
- final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
- mPaddingLeft + mPaddingRight, lp.width);
- final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
- mPaddingTop + mPaddingBottom, lp.height);
- // 調用子元素的measure傳入計算好的測量規格
- child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
- }
- public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
- // 獲取父容器的測量模式和尺寸大小
- int specMode = MeasureSpec.getMode(spec);
- int specSize = MeasureSpec.getSize(spec);
- // 這個尺寸應該減去內邊距的值
- int size = Math.max(0, specSize - padding);
- // 聲明臨時變量存值
- int resultSize = 0;
- int resultMode = 0;
- /*
- * 根據模式判斷
- */
- switch (specMode) {
- case MeasureSpec.EXACTLY: // 父容器尺寸大小是一個確定的值
- /*
- * 根據子元素的佈局參數判斷
- */
- if (childDimension >= 0) { //如果childDimension是一個具體的值
- // 那麼就將該值作爲結果
- resultSize = childDimension;
- // 而這個值也是被確定的
- resultMode = MeasureSpec.EXACTLY;
- } else if (childDimension == LayoutParams.MATCH_PARENT) { //如果子元素的佈局參數爲MATCH_PARENT
- // 那麼就將父容器的大小作爲結果
- resultSize = size;
- // 因爲父容器的大小是被確定的所以子元素大小也是可以被確定的
- resultMode = MeasureSpec.EXACTLY;
- } else if (childDimension == LayoutParams.WRAP_CONTENT) { //如果子元素的佈局參數爲WRAP_CONTENT
- // 那麼就將父容器的大小作爲結果
- resultSize = size;
- // 但是子元素的大小包裹了其內容後不能超過父容器
- resultMode = MeasureSpec.AT_MOST;
- }
- break;
- case MeasureSpec.AT_MOST: // 父容器尺寸大小擁有一個限制值
- /*
- * 根據子元素的佈局參數判斷
- */
- if (childDimension >= 0) { //如果childDimension是一個具體的值
- // 那麼就將該值作爲結果
- resultSize = childDimension;
- // 而這個值也是被確定的
- resultMode = MeasureSpec.EXACTLY;
- } else if (childDimension == LayoutParams.MATCH_PARENT) { //如果子元素的佈局參數爲MATCH_PARENT
- // 那麼就將父容器的大小作爲結果
- resultSize = size;
- // 因爲父容器的大小是受到限制值的限制所以子元素的大小也應該受到父容器的限制
- resultMode = MeasureSpec.AT_MOST;
- } else if (childDimension == LayoutParams.WRAP_CONTENT) { //如果子元素的佈局參數爲WRAP_CONTENT
- // 那麼就將父容器的大小作爲結果
- resultSize = size;
- // 但是子元素的大小包裹了其內容後不能超過父容器
- resultMode = MeasureSpec.AT_MOST;
- }
- break;
- case MeasureSpec.UNSPECIFIED: // 父容器尺寸大小未受限制
- /*
- * 根據子元素的佈局參數判斷
- */
- if (childDimension >= 0) { //如果childDimension是一個具體的值
- // 那麼就將該值作爲結果
- resultSize = childDimension;
- // 而這個值也是被確定的
- resultMode = MeasureSpec.EXACTLY;
- } else if (childDimension == LayoutParams.MATCH_PARENT) { //如果子元素的佈局參數爲MATCH_PARENT
- // 因爲父容器的大小不受限制而對子元素來說也可以是任意大小所以不指定也不限制子元素的大小
- resultSize = 0;
- resultMode = MeasureSpec.UNSPECIFIED;
- } else if (childDimension == LayoutParams.WRAP_CONTENT) { //如果子元素的佈局參數爲WRAP_CONTENT
- // 因爲父容器的大小不受限制而對子元素來說也可以是任意大小所以不指定也不限制子元素的大小
- resultSize = 0;
- resultMode = MeasureSpec.UNSPECIFIED;
- }
- break;
- }
- // 返回封裝後的測量規格
- return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
- }
- /**
- *
- * @author AigeStudio {@link http://blog.csdn.net/aigestudio}
- * @since 2015/1/15
- *
- */
- public class CustomLayout extends ViewGroup {
- public CustomLayout(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
- @Override
- protected void onLayout(boolean changed, int l, int t, int r, int b) {
- }
- }
- public class View implements Drawable.Callback, KeyEvent.Callback,
- AccessibilityEventSource {
- // 省去無數代碼………………
- /**
- * Called from layout when this view should
- * assign a size and position to each of its children.
- *
- * Derived classes with children should override
- * this method and call layout on each of
- * their children.
- * @param changed This is a new size or position for this view
- * @param left Left position, relative to parent
- * @param top Top position, relative to parent
- * @param right Right position, relative to parent
- * @param bottom Bottom position, relative to parent
- */
- protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
- }
- // 省去無數代碼………………
- }
- /**
- *
- * @author AigeStudio {@link http://blog.csdn.net/aigestudio}
- * @since 2015/1/15
- *
- */
- public class CustomLayout extends ViewGroup {
- public CustomLayout(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- /*
- * 如果有子元素
- */
- if (getChildCount() > 0) {
- // 那麼對子元素進行測量
- measureChildren(widthMeasureSpec, heightMeasureSpec);
- }
- }
- @Override
- protected void onLayout(boolean changed, int l, int t, int r, int b) {
- }
- }
- <com.aigestudio.customviewdemo.views.CustomLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:background="#FFFFFFFF"
- android:orientation="vertical" >
- <com.aigestudio.customviewdemo.views.IconView
- android:id="@+id/main_pv"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:padding="50dp" />
- <Button
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="AigeStudio" />
- <TextView
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="AigeStudio" />
- </com.aigestudio.customviewdemo.views.CustomLayout>
- @Override
- protected void onLayout(boolean changed, int l, int t, int r, int b) {
- /*
- * 如果有子元素
- */
- if (getChildCount() > 0) {
- // 那麼遍歷子元素並對其進行定位佈局
- for (int i = 0; i < getChildCount(); i++) {
- View child = getChildAt(i);
- child.layout(0, 0, getMeasuredWidth(), getMeasuredHeight());
- }
- }
- }
看到屏幕上的巨大Button我不禁吸了一口屁!這樣的佈局太蛋疼,全被Button一個玩完了還搞毛,可不可以像LinearLayout那樣挨個顯示呢?答案是肯定的!我們來修改下onLayout的邏輯:
- @Override
- protected void onLayout(boolean changed, int l, int t, int r, int b) {
- /*
- * 如果有子元素
- */
- if (getChildCount() > 0) {
- // 聲明一個臨時變量存儲高度倍增值
- int mutilHeight = 0;
- // 那麼遍歷子元素並對其進行定位佈局
- for (int i = 0; i < getChildCount(); i++) {
- // 獲取一個子元素
- View child = getChildAt(i);
- // 通知子元素進行佈局
- child.layout(0, mutilHeight, child.getMeasuredWidth(), child.getMeasuredHeight() + mutilHeight);
- // 改變高度倍增值
- mutilHeight += child.getMeasuredHeight();
- }
- }
- }
是不是和上面LinearLayout效果有點一樣了?當然LinearLayout的佈局邏輯遠比我們的複雜得多,我們呢也只是對其進行一個簡單的模擬而已。大家注意到ViewGroup的onLayout方法的簽名列表中有五個參數,其中boolean changed表示是否與上一次位置不同,其具體值在View的layout方法中通過setFrame等方法確定:
- public void layout(int l, int t, int r, int b) {
- // 省略一些代碼……
- boolean changed = isLayoutModeOptical(mParent) ?
- setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
- // 省略大量代碼……
- }
好了,說到這裏想必大家對ViewGroup的測量也有一定的瞭解了,但是這必定不是測量過程全部,如我上面所說,測量的具體過程因控件而異,上面我們曾因爲給我們的自定義View加了內邊距後修改了繪製的邏輯,因爲我們需要在繪製時考慮內邊距的影響,而我們的自定義ViewGroup呢?是不是也一樣呢?這裏我給其加入60dp的內邊距:
- <com.aigestudio.customviewdemo.views.CustomLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:padding="60dp"
- android:background="#FFFFFFFF"
- android:orientation="vertical" >
- <com.aigestudio.customviewdemo.views.IconView
- android:id="@+id/main_pv"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:padding="50dp" />
- <Button
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="AigeStudio" />
- <TextView
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="AigeStudio" />
- </com.aigestudio.customviewdemo.views.CustomLayout>
內邊距把我們的子元素給“喫”掉了,那麼也就是說我們在對子元素進行定位時應該進一步考慮到父容器內邊距的影響對吧,OK,我們重理onLayout的邏輯:
- @Override
- protected void onLayout(boolean changed, int l, int t, int r, int b) {
- // 獲取父容器內邊距
- int parentPaddingLeft = getPaddingLeft();
- int parentPaddingTop = getPaddingTop();
- /*
- * 如果有子元素
- */
- if (getChildCount() > 0) {
- // 聲明一個臨時變量存儲高度倍增值
- int mutilHeight = 0;
- // 那麼遍歷子元素並對其進行定位佈局
- for (int i = 0; i < getChildCount(); i++) {
- // 獲取一個子元素
- View child = getChildAt(i);
- // 通知子元素進行佈局
- // 此時考慮父容器內邊距的影響
- child.layout(parentPaddingLeft, mutilHeight + parentPaddingTop, child.getMeasuredWidth() + parentPaddingLeft, child.getMeasuredHeight() + mutilHeight + parentPaddingTop);
- // 改變高度倍增值
- mutilHeight += child.getMeasuredHeight();
- }
- }
- }
既然內邊距如此,那麼Margins外邊距呢?我們來看看,在xml佈局文件中爲我們的CustomLayout加一個margins:
- <com.aigestudio.customviewdemo.views.CustomLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:layout_margin="30dp"
- android:padding="20dp"
- android:background="#FF597210"
- android:orientation="vertical" >
- <com.aigestudio.customviewdemo.views.IconView
- android:id="@+id/main_pv"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content" />
- <Button
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="AigeStudio" />
- <TextView
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="AigeStudio" />
- </com.aigestudio.customviewdemo.views.CustomLayout>
OK,目測沒什麼問題,可是當我們爲子元素設置外邊距時,問題就來了……不管你怎麼設都不會有任何效果,原因很簡單,我們上面也說了,Margins是由父容器來處理,而我們的CustomLayout中並沒有對其做任何的處理,那麼我們應該怎麼做呢?首先要知道Margins封裝在LayoutParams中,如果我們想實現自己對其的處理那麼我們必然也有必要實現自己佈局的LayoutParams:
- /**
- *
- * @author AigeStudio {@link http://blog.csdn.net/aigestudio}
- * @since 2015/1/15
- *
- */
- public class CustomLayout extends ViewGroup {
- // 省略部分代碼…………
- /**
- *
- * @author AigeStudio {@link http://blog.csdn.net/aigestudio}
- *
- */
- public static class CustomLayoutParams extends MarginLayoutParams {
- public CustomLayoutParams(MarginLayoutParams source) {
- super(source);
- }
- public CustomLayoutParams(android.view.ViewGroup.LayoutParams source) {
- super(source);
- }
- public CustomLayoutParams(Context c, AttributeSet attrs) {
- super(c, attrs);
- }
- public CustomLayoutParams(int width, int height) {
- super(width, height);
- }
- }
- }
- /**
- *
- * @author AigeStudio {@link http://blog.csdn.net/aigestudio}
- * @since 2015/1/15
- *
- */
- public class CustomLayout extends ViewGroup {
- // 省略部分代碼…………
- /**
- * 生成默認的佈局參數
- */
- @Override
- protected CustomLayoutParams generateDefaultLayoutParams() {
- return new CustomLayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
- }
- /**
- * 生成佈局參數
- * 將佈局參數包裝成我們的
- */
- @Override
- protected android.view.ViewGroup.LayoutParams generateLayoutParams(android.view.ViewGroup.LayoutParams p) {
- return new CustomLayoutParams(p);
- }
- /**
- * 生成佈局參數
- * 從屬性配置中生成我們的佈局參數
- */
- @Override
- public android.view.ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
- return new CustomLayoutParams(getContext(), attrs);
- }
- /**
- * 檢查當前佈局參數是否是我們定義的類型這在code聲明佈局參數時常常用到
- */
- @Override
- protected boolean checkLayoutParams(android.view.ViewGroup.LayoutParams p) {
- return p instanceof CustomLayoutParams;
- }
- // 省略部分代碼…………
- }
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- // 聲明臨時變量存儲父容器的期望值
- int parentDesireWidth = 0;
- int parentDesireHeight = 0;
- /*
- * 如果有子元素
- */
- if (getChildCount() > 0) {
- // 那麼遍歷子元素並對其進行測量
- for (int i = 0; i < getChildCount(); i++) {
- // 獲取子元素
- View child = getChildAt(i);
- // 獲取子元素的佈局參數
- CustomLayoutParams clp = (CustomLayoutParams) child.getLayoutParams();
- // 測量子元素並考慮外邊距
- measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
- // 計算父容器的期望值
- parentDesireWidth += child.getMeasuredWidth() + clp.leftMargin + clp.rightMargin;
- parentDesireHeight += child.getMeasuredHeight() + clp.topMargin + clp.bottomMargin;
- }
- // 考慮父容器的內邊距
- parentDesireWidth += getPaddingLeft() + getPaddingRight();
- parentDesireHeight += getPaddingTop() + getPaddingBottom();
- // 嘗試比較建議最小值和期望值的大小並取大值
- parentDesireWidth = Math.max(parentDesireWidth, getSuggestedMinimumWidth());
- parentDesireHeight = Math.max(parentDesireHeight, getSuggestedMinimumHeight());
- }
- // 設置最終測量值O
- setMeasuredDimension(resolveSize(parentDesireWidth, widthMeasureSpec), resolveSize(parentDesireHeight, heightMeasureSpec));
- }
- @Override
- protected void onLayout(boolean changed, int l, int t, int r, int b) {
- // 獲取父容器內邊距
- int parentPaddingLeft = getPaddingLeft();
- int parentPaddingTop = getPaddingTop();
- /*
- * 如果有子元素
- */
- if (getChildCount() > 0) {
- // 聲明一個臨時變量存儲高度倍增值
- int mutilHeight = 0;
- // 那麼遍歷子元素並對其進行定位佈局
- for (int i = 0; i < getChildCount(); i++) {
- // 獲取一個子元素
- View child = getChildAt(i);
- CustomLayoutParams clp = (CustomLayoutParams) child.getLayoutParams();
- // 通知子元素進行佈局
- // 此時考慮父容器內邊距和子元素外邊距的影響
- child.layout(parentPaddingLeft + clp.leftMargin, mutilHeight + parentPaddingTop + clp.topMargin, child.getMeasuredWidth() + parentPaddingLeft + clp.leftMargin, child.getMeasuredHeight() + mutilHeight + parentPaddingTop + clp.topMargin);
- // 改變高度倍增值
- mutilHeight += child.getMeasuredHeight() + clp.topMargin + clp.bottomMargin;
- }
- }
- }
- <com.aigestudio.customviewdemo.views.CustomLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:background="#FF597210"
- android:orientation="vertical" >
- <com.aigestudio.customviewdemo.views.IconView
- android:id="@+id/main_pv"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginBottom="10dp"
- android:layout_marginLeft="20dp"
- android:layout_marginRight="30dp"
- android:layout_marginTop="5dp" />
- <Button
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginBottom="16dp"
- android:layout_marginLeft="2dp"
- android:layout_marginRight="8dp"
- android:layout_marginTop="4dp"
- android:text="AigeStudio" />
- <TextView
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginBottom="28dp"
- android:layout_marginLeft="7dp"
- android:layout_marginRight="19dp"
- android:layout_marginTop="14dp"
- android:background="#FF166792"
- android:text="AigeStudio" />
- </com.aigestudio.customviewdemo.views.CustomLayout>
~~~~~~~~好了好了、不講了,View的基本測量過程大致就是這樣,如我所說測量並不是定式的過程,總會因控件而已,我們在自定義控件時要準確地測量,一定要準確,測量的結果會直接影響後面的佈局定位、繪製甚至交互,所以馬虎不得,你也可以看到Android給我們提供的LinearLayout、FrameLayout等佈局都有極其嚴謹的測量邏輯,爲的就是確保測量結果的準確。
本篇幅雖長,但是我們其實就講了三點:
- 一個界面窗口的元素構成
- framework對View測量的控制處理
- View和ViewGroup的簡單測量
好了、不說了、實在說不動了………………到此爲止&¥……#¥……%#¥%#¥%#%¥哦!對了,文章開頭我給各位設了一個問題,不知道大家發現沒有,本來說這節順帶講了,看着篇幅太長下節再說吧……
源碼下載:傳送門