android 自定義view大小 - CSDN

2020-12-14 CSDN技術社區

自定義View是Android開發中最普通的需求,靈活控制View的尺寸是開發者面臨的第一個問題,比如,為什麼明明使用的是WRAP_CONTENT卻跟MATCH_PARENT表現相同。在處理View尺寸的時候,我們都知道最好在onMeasure中設定好自定義View尺寸,那麼究竟如何合理的選擇這個尺寸呢。直觀來說,可能有以下問題需要考慮:

  • 自定的View最好不要超過父控制項的大小,這樣才能保證自己能在父控制項中完整顯示
  • 自定的View(如果是ViewGroup)的子控制項最好不要超過自己的大小,這樣才能保證子控制項顯示完整
  • 如果明確為View指定了尺寸,最好按照指定的尺寸設置

以上三個問題可能是自定義ViewGroup最需要考慮的問題,首先先解決第一個問題。

父容器的限制與MeasureSpec

先假定,父容器是300dp*300dp的尺寸,如果子View的布局參數是

<!--場景1-->android:layout_width="match_parent"android:layout_height="match_parent"

那麼按照我們的期望,希望子View的尺寸要是300dp*300dp,如果子View的布局參數是

<!--場景2-->android:layout_width="100dp"android:layout_height="100dp"

按照我們的期望,希望子View的尺寸要是100dp*100dp,如果子View的布局參數是

<!--場景3-->android:layout_width="wrap_content"android:layout_height="wrap_content"

按照我們的期望,希望子View的尺寸可以按照自己需求的尺寸來確定,但是最好不要超過300dp*300dp。

那麼父容器怎麼把這些要求告訴子View呢?MeasureSpec其實就是承擔這種作用:MeasureSpec是父控制項提供給子View的一個參數,作為設定自身大小參考,只是個參考,要多大,還是View自己說了算。先看下MeasureSpec的構成,MeasureSpec由size和mode組成,mode包括三種,UNSPECIFIED、EXACTLY、AT_MOST,size就是配合mode給出的參考尺寸,具體意義如下:

  • UNSPECIFIED(未指定),父控制項對子控制項不加任何束縛,子元素可以得到任意想要的大小,這種MeasureSpec一般是由父控制項自身的特性決定的。比如ScrollView,它的子View可以隨意設置大小,無論多高,都能滾動顯示,這個時候,size一般就沒什麼意義。
  • EXACTLY(完全),父控制項為子View指定確切大小,希望子View完全按照自己給定尺寸來處理,跟上面的場景1跟2比較相似,這時的MeasureSpec一般是父控制項根據自身的MeasureSpec跟子View的布局參數來確定的。一般這種情況下size>0,有個確定值。
  • AT_MOST(至多),父控制項為子元素指定最大參考尺寸,希望子View的尺寸不要超過這個尺寸,跟上面場景3比較相似。這種模式也是父控制項根據自身的MeasureSpec跟子View的布局參數來確定的,一般是子View的布局參數採用wrap_content的時候。

先來看一下ViewGroup源碼中measureChild怎麼為子View構造MeasureSpec的:

protected void measureChild(View child, int parentWidthMeasureSpec,         int parentHeightMeasureSpec) {     final LayoutParams lp = child.getLayoutParams();     final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,             mPaddingLeft + mPaddingRight, lp.width);     final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,             mPaddingTop + mPaddingBottom, lp.height);     child.measure(childWidthMeasureSpec, childHeightMeasureSpec); }

由於任何View都是支持Padding參數的,在為子View設置參考尺寸的時候,需要先把自己的Padding給去除,這同時也是為了Layout做鋪墊。接著看如何getChildMeasureSpec獲取傳遞給子View的MeasureSpec的:

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) {    // Parent has imposed an exact size on us    case MeasureSpec.EXACTLY:        if (childDimension >= 0) {            resultSize = childDimension;            resultMode = MeasureSpec.EXACTLY;        } else if (childDimension == LayoutParams.MATCH_PARENT) {            // Child wants to be our size. So be it.            resultSize = size;            resultMode = MeasureSpec.EXACTLY;        } else if (childDimension == LayoutParams.WRAP_CONTENT) {            // Child wants to determine its own size. It can't be            // bigger than us.            resultSize = size;            resultMode = MeasureSpec.AT_MOST;        }        break;    // Parent has imposed a maximum size on us    case MeasureSpec.AT_MOST:        if (childDimension >= 0) {            // Child wants a specific size... so be it            resultSize = childDimension;            resultMode = MeasureSpec.EXACTLY;        } else if (childDimension == LayoutParams.MATCH_PARENT) {            // Child wants to be our size, but our size is not fixed.            // Constrain child to not be bigger than us.            resultSize = size;            resultMode = MeasureSpec.AT_MOST;        } else if (childDimension == LayoutParams.WRAP_CONTENT) {            // Child wants to determine its own size. It can't be            // bigger than us.            resultSize = size;            resultMode = MeasureSpec.AT_MOST;        }        break;    // Parent asked to see how big we want to be    case MeasureSpec.UNSPECIFIED:        if (childDimension >= 0) {            // Child wants a specific size... let him have it            resultSize = childDimension;            resultMode = MeasureSpec.EXACTLY;        } else if (childDimension == LayoutParams.MATCH_PARENT) {            // Child wants to be our size... find out how big it should            // be            resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;            resultMode = MeasureSpec.UNSPECIFIED;        } else if (childDimension == LayoutParams.WRAP_CONTENT) {            // Child wants to determine its own size.... find out how            // big it should be            resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;            resultMode = MeasureSpec.UNSPECIFIED;        }        break;    }    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);}

可以看到父控制項會參考自己的MeasureSpec跟子View的布局參數,為子View構建合適的MeasureSpec,盜用網上的一張圖來描述就是

當子View接收到父控制項傳遞的MeasureSpec的時候,就可以知道父控制項希望自己如何顯示,這個點對於開發者而言就是onMeasure函數,先來看下View.java中onMeasure函數的實現:

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

其中getSuggestedMinimumWidth是根據設置的背景跟最小尺寸得到一個備用的參考尺寸,接著看getDefaultSize,如下:

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

可以看到,如果自定義View沒有重寫onMeasure函數,MeasureSpec.AT_MOST跟MeasureSpec.AT_MOST的表現是一樣的,也就是對於場景2跟3的表現其實是一樣的,也就是wrap_content就跟match_parent一個效果,現在我們知道MeasureSpec的主要作用:父控制項傳遞給子View的參考,那么子View拿到後該如何用呢?

自定義View尺寸的確定

接收到父控制項傳遞的MeasureSpec後,View應該如何用來處理自己的尺寸呢?onMeasure是View測量尺寸最合理的時機,如果View不是ViewGroup相對就比較簡單,只需要參照MeasureSpec,並跟自身需求來設定尺寸即可,默認onMeasure的就是完全按照父控制項傳遞MeasureSpec設定自己的尺寸的。這裡重點講一下ViewGroup,為了獲得合理的寬高尺寸,ViewGroup在計算自己尺寸的時候,必須預先知道所有子View的尺寸,舉個例子,用一個常用的流式布局FlowLayout來講解一下如何合理的設定自己的尺寸。

先分析一下FLowLayout流式布局(從左到右)的特點:FLowLayout將所有子View從左往右依次放置,如果當前行,放不開的就換行。從流失布局的特點來看,在確定FLowLayout尺寸的時候,我們需要知道下列信息,

  • 父容器傳遞給FlowLayout的MeasureSpec推薦的大小(超出了,顯示不出來,又沒意義)
  • FlowLayout中所有子View的寬度與寬度:計算寬度跟高度的時候需要用的到。
  • 綜合MeasureSpec跟自身需求,得出合理的尺寸

首先看父容器傳遞給FlowLayout的MeasureSpec,對開發者而言,它可見於onMeasure函數,是通過onMeasure的參數傳遞進來的,它的意義上面的已經說過了,現在來看,怎麼用比較合理?其實ViewGroup.java源碼中也提供了比較簡潔的方法,有兩個比較常用的measureChildren跟resolveSize,在之前的分析中我們知道measureChildren會調用getChildMeasureSpec為子View創建MeasureSpec,並通過measureChild測量每個子View的尺寸。那麼resolveSize呢,看下面源碼,resolveSize(int size, int measureSpec)的兩個輸入參數,第一個參數:size,是View自身希望獲取的尺寸,第二參數:measureSpec,其實父控制項傳遞給View,推薦View獲取的尺寸,resolveSize就是綜合考量兩個參數,最後給一個建議的尺寸:

public static int resolveSize(int size, int measureSpec) {        return resolveSizeAndState(size, measureSpec, 0) & MEASURED_SIZE_MASK;    }public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {    final int specMode = MeasureSpec.getMode(measureSpec);    final int specSize = MeasureSpec.getSize(measureSpec);    final int result;    switch (specMode) {        case MeasureSpec.AT_MOST:            if (specSize < size) {                result = specSize | MEASURED_STATE_TOO_SMALL;            } else {                result = size;            }            break;        case MeasureSpec.EXACTLY:            result = specSize;            break;   case MeasureSpec.UNSPECIFIED:        default:            result = size;    }    return result | (childMeasuredState & MEASURED_STATE_MASK);}

可以看到:

  • 如果父控制項傳遞給的MeasureSpec的mode是MeasureSpec.UNSPECIFIED,就說明,父控制項對自己沒有任何限制,那麼尺寸就選擇自己需要的尺寸size
  • 如果父控制項傳遞給的MeasureSpec的mode是MeasureSpec.EXACTLY,就說明父控制項有明確的要求,希望自己能用measureSpec中的尺寸,這時就推薦使用MeasureSpec.getSize(measureSpec)
  • 如果父控制項傳遞給的MeasureSpec的mode是MeasureSpec.AT_MOST,就說明父控制項希望自己不要超出MeasureSpec.getSize(measureSpec),如果超出了,就選擇MeasureSpec.getSize(measureSpec),否則用自己想要的尺寸就行了

對於FlowLayout,可以假設每個子View都可以充滿FlowLayout,因此,可以直接用measureChildren測量所有的子View的尺寸:

@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {    int widthSize = MeasureSpec.getSize(widthMeasureSpec);    int paddingLeft = getPaddingLeft();    int paddingRight = getPaddingRight();    int paddingBottom = getPaddingBottom();    int paddingTop = getPaddingTop();    int count = getChildCount();    int maxWidth = 0;    int totalHeight = 0;    int lineWidth = 0;    int lineHeight = 0;    int extraWidth = widthSize - paddingLeft - paddingRight;    <!--直接用measureChildren測量所有的子View的高度-->    measureChildren(widthMeasureSpec, heightMeasureSpec);    <!--現在可以獲得所有子View的尺寸-->    for (int i = 0; i < count; i++) {        View view = getChildAt(i);        if (view != null && view.getVisibility() != GONE) {            if (lineWidth + view.getMeasuredWidth() > extraWidth) {                totalHeight += lineHeight ;                lineWidth = view.getMeasuredWidth();                lineHeight = view.getMeasuredHeight();                maxWidth = widthSize;            } else {                lineWidth += view.getMeasuredWidth();            }            <!--獲取每行的最高View尺寸-->            lineHeight = Math.max(lineHeight, view.getMeasuredHeight());        }    }    totalHeight = Math.max(totalHeight + lineHeight, lineHeight);    maxWidth = Math.max(lineWidth, maxWidth);    <!--totalHeight 跟 maxWidth都是FlowLayout渴望得到的尺寸-->    <!--至於合不合適,通過resolveSize再來判斷一遍,當然,如果你非要按照自己的尺寸來,也可以設定,但是不太合理-->    totalHeight = resolveSize(totalHeight + paddingBottom + paddingTop, heightMeasureSpec);    lineWidth = resolveSize(maxWidth + paddingLeft + paddingRight, widthMeasureSpec);    setMeasuredDimension(lineWidth, totalHeight);}

可以看到,設定自定義ViewGroup的尺寸其實只需要三部:

  • 測量所有子View,獲取所有子View的尺寸
  • 根據自身特點計算所需要的尺寸
  • 綜合考量需要的尺寸跟父控制項傳遞的MeasureSpec,得出一個合理的尺寸

頂層View的MeasureSpec是誰指定

傳遞給子View的MeasureSpec是父容器根據自己的MeasureSpec及子View的布局參數所確定的,那麼根MeasureSpec是誰創建的呢?我們用最常用的兩種Window來解釋一下,Activity與Dialog,DecorView是Activity的根布局,傳遞給DecorView的MeasureSpec是系統根據Activity或者Dialog的Theme來確定的,也就是說,最初的MeasureSpec是直接根據Window的屬性構建的,一般對於Activity來說,根MeasureSpec是EXACTLY+屏幕尺寸,對於Dialog來說,如果不做特殊設定會採用AT_MOST+屏幕尺寸。這裡牽扯到WindowManagerService跟ActivityManagerService,感興趣的可以跟蹤一下WindowManager.LayoutParams ,後面也會專門分析一下,比如,實現最簡單試的全屏的Dialog就跟這些知識相關。

作者:看書的小蝸牛
連結:https://www.jianshu.com/p/d16ec64181f2

相關焦點

  • 如何利用 Android 自定義控制項實現炫酷的動畫?|CSDN 博文精選
    自定義LayoutManager基礎知識有關自定義LayoutManager基礎知識,請查閱以下文章,寫的非常棒:1、陳小緣的自定義LayoutManager第十一式之飛龍在天(小緣大佬自定義文章邏輯清晰明了,堪稱教科書,非常經典)https://blog.csdn.net/u011387817/article/details/81875021
  • android開發 自我優勢 - CSDN
    6、熟悉android手機屏幕適配及屏幕適配的原則,提高應用的兼容性(解決不同尺寸手機顯示圖片大小問題)7、熟悉Android的數據存儲方式(File,SharedPrefrence,Sqlite,ContentProvider,Net)8、掌握APP應用開發框架結構的基本搭建,抽取activity,fragment,adapter,holder等公用代碼,
  • android啟動頁設計專題及常見問題 - CSDN
    轉載請註明出處:http://blog.csdn.net/wangjihuanghun/article/details/63255144啟動頁幾乎成為了每個app的標配,有些商家在啟動頁中增加了開屏廣告以此帶來更多的收入。
  • android 布局 覆蓋 - CSDN
    項目中listview中嵌套checkbox,將父控制項設置為android:descendantFocusability="blocksDescendants",這樣設置為的是:會覆蓋子類控制項而直接獲得焦點,即點擊listview的item區域即可選中checkbox。
  • android 監聽屏幕鎖屏專題及常見問題 - CSDN
    > 鎖屏聽音樂(音頻),沒有鎖屏看視頻Android系統亮屏、鎖屏、屏幕解鎖事件(解決部分手機亮屏後未解鎖即進入resume狀態)- http://blog.csdn.net/oracleot/article/details/20378453Android 實現鎖屏的較完美方案- https://segmentfault.com/a/1190000003075989
  • Android - android xml 層級專題及常見問題 - CSDN
    TextView 對象上使用資源 ID 來設置文本,具體如下:TextView msgTextView = (TextView) findViewById(R.id.msg);msgTextView.setText(R.string.hello);實例考慮如下定義的布局 res/layout/activity_main.xmlandroid
  • android app被殺原因專題及常見問題 - CSDN
    分析長按HOME鍵清理App最終會執行到ActivityManagerService.cleanUpRemovedTaskLocked方法中,ActivityManagerService類在文件"frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java"中,
  • android 啟動頁慢專題及常見問題 - CSDN
    對應用進行打點獲取相關信息: @Override public void onCreate(final Bundle icicle) { setTheme(R.style.BrowserTheme); Intent intent = getIntent(); NLog.i(LOGTAG,"onCreate"); super.onCreate(icicle); //開始記錄,且該方法可以設置文件大小和路徑
  • Android Spinner下拉框的基本使用
    ;java </item><item>php</item><item>xml</item><item>html</item></string-array>4、在布局文件xml的Spinner下添加:android
  • Android ConstraintLayout約束布局可視化工具使用~
    :constraint-layout:1.0.2'<android.support.constraint.ConstraintLayoutandroid:layout_width="match_parent"
  • android 虛擬機版本專題及常見問題 - CSDN
    我們使用Java開發android,在編譯打包APK文件時,會經過以下流程Java編譯器將應用中所有Java文件編譯為class文件 dx工具將應用編譯輸出的類文件轉換為Dalvik字節碼,即dex文件之後經過籤名、對齊等操作變為APK文件。
  • 對抗學習專題及常見問題 - CSDN
    深度學習筆記 (bp,卷積層,池化層,全連接層,激活函數層,softmax層的前向反向實現)(看完文字)【https://blog.csdn.net物理世界中的對抗樣本,有列印重照、亮度對比度等調整)【https://blog.csdn.net/u010710787/article/details/78916762】
  • 英語in view of 和 in the view of 的區別
    英語中,in view of 和 in the view of 是兩個意思十分相近的詞組,但是它們的意思並不相同,今天我們一起來學習一下。1.In view of:這個詞組的意思是「鑑於,考慮到」。例句1:In view of the weather, the event will be held indoors.鑑於天氣的緣故,這項賽事將在室內進行。
  • FWUL專為Android調試和改裝而設計的Linux發行版
    這就是FWUL(一種用於Android調試的自定義Linux發行版)開始發揮作用的地方。它由XDA認可的開發人員steadfasterX 於2年前發布。就在幾周前,開發人員宣布發布了該發行版的3.0版。
  • 了解Android開發規範:性能及UI優化是什麼樣的?
    /385611.htm一、Android編碼規範1.java代碼中不出現中文,最多注釋中可以出現中文2.局部變量命名、靜態成員變量命名只能包含字母,單詞首字母出第一個外,都為大寫,其他字母都為小寫3.常量命名只能包含字母和_,字母全部大寫,單詞之間用_隔開4.layout中的id命名命名模式為:view
  • 5.0 android 平板 - CSDN
    媒體播放:  如果您要實現顯示媒體播放狀態或傳輸控制項的通知,請考慮使用新的Notification.MediaStyle模板,而不是自定義RemoteViews.RemoteView對象。已棄用的 HTTP 類:Android 5.1 中已棄用org.apache.http類和android.net.http. AndroidHttpClient類。這些類將不再保留,您應儘快將使用這些API的任何應用代碼遷移至URLConnection類。
  • Google I/O 2019 Android 應用原始碼現已發布
    手勢導航: 返回上一級界面和主屏https://developer.android.google.cn/preview/features/gesturalnavhttps://medium.com/androiddevelopers/gesture-navigation-going-edge-to-edge-812f62e4e83ehttps://github.com
  • Android的內部存儲和外部存儲
    3.當刪除App時,移除文件 方法:     getFilesDir();  getCache();       //當內存不足時,系統會無警告的刪除該文件夾的內容,儘可能自己規定文件大小,與何時刪除 getFIleOutPutStream();
  • AndroidQ適配(暗黑模式和文件存儲)
    如果某個 View 的需要使用自定義色值適配暗黑模式,我們需要對這個 View 添加這個配置,讓 Force Dark 排除它:android:forceDarkAllowed="false"然後在代碼裡根據當前是否處於暗黑模式,對色值進行動態設置。