android-自定義View解決wrap_content無效的問題
阿新 • • 發佈:2019-01-07
###問題提出 在我們自定義view時,如何需要是當前的view內容自適應,這種平常的使用中,只需要在xml檔案中制定寬高或者長高為wrap_content即可,但是如果該view是我們自定義的,那麼此時再在xml檔案中指定寬高為wrap_content則不能起到內容自適應的效果,並且效果為match_parent。本文即是解決此類問題。 ###預備知識 在講解該問題之前,我們需要了解一些預備知識,以更加清楚的瞭解原理。 - 自定義view過程中,我們通常需要關注3個過程,即 測量 / 佈局 / 繪製 。 本文的問題位於測量過程,所以本文也將著重瞭解測量過程。 - 在測量過程中,其目的是為了得到view的尺寸表示,該表示通過一個封裝類為MeasureSpec所標識。測量過程實際上可以說是得到該view的MeasureSpec的過程。 - MeasureSpec是通過將一個int(32)的陣列成而成的,本質是一個int數,MeasureSpec由兩部分組成:SepcMode 和 SpecSize 。其中SpecMode為MeasueSpec的高2位,SpecSize為MeasureSpec的低30位。SpecMode有3類: - UNSPECIFIED:父容器不對View有任何限制,要多大有多大,這種情況一般用於系統內容。在我們使用過程中,一般不考慮這種模式。 - EXACTLY:父容器已經檢測到了View所需要的精確大小,這個時候View的最終大小就是SpecSize所指定的大小。它對應的LayotParams中的match_parent 和具體的數值。 - AT_MOST: 父容器指定了一個可用大小SpecSize,View的大小不能大於這個值,它對應於LayoutParams中的wrap_content。 - View的MeasureSpec與父容器的MeasureSpec和本身的LayoutParams有關,並且由二者所決定,決定規則如下: ![輸入圖片說明](https://static.oschina.net/uploads/img/201602/17191840_emvA.jpg "在這裡輸入圖片標題") ###問題出現原理 現在我們看一下View的onMeasure方法。該方法實際上就是View測量過程中最重要的方法。 ``` protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) ``` 對於一個View而言,它的onMeasure方法一般由其父容器所呼叫,並且傳入```widthMeasureSpec```和```heighMeasureSpec```,這兩個值就是來源於我們在xml檔案中指定的,值為wrap_content / match_parent / 或者具體數值。 下面我們具體看一下onMeasure方法的具體實現: ``` protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); } ``` 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; } ``` 從上面兩個方法我們可以知道:在預設實現中,AT_MOST和EXACTLY兩種模式都會被設定成specSize。我們也知道AT_MOST對應於wrap_content,而EXACTLY對應於match_parent和具體數值情況。也就說預設情況下wrap_content和match_parent是具有相同的效果的。那有人會問:wrap_content和match_parent具有相同的效果,為什麼是填充父容器的效果呢? 下面講一下View的繪製過程: View的繪製首先起於ViewRootImpl,並且View的三個流程也是通過ViewRootImpl來完成的,在ActivityThread中,當Activity物件被建立完畢後,會將DecorView新增到Window中,同時會建立viewRootImpl物件,並將ViewRootImpl物件和DecorView建立關聯。之後,View的繪製過程從ViewRootImpl的performTraversals方法開始,它經過mwasure、layout、draw三個過程最終將一個View繪製出來,由於DecorView是Android的一個頁面的頂級View,所以繪製過程首先會從DecorView開始,又因為DecorView是一個ViewGroup,它會遍歷繪製所有的子View,我們注意到在protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)方法中會有兩個引數,其實這兩個引數是在ViewGroup中傳入進去的,最初是在DecorView中賦值的,且其值就是螢幕的寬度和高度。 ###問題解決 從上面的分析我們知道:出現這個問題的原因是在測量過程中沒有處理wrap_content的情況,所以我們在自定義View的時候,如果在直接繼承自View,因為View的預設實現是沒有處理這種情況的,所以wrap_content會出現與atch_parent相同的效果,要想解決這個問題,我們就要特殊處理。 下面是一種典型的處理方式: ``` @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int desiredWidth = 100; int desiredHeight = 100; int widthMode = MeasureSpec.getMode(widthMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); int width; int height; //Measure Width if (widthMode == MeasureSpec.EXACTLY) { //Must be this size width = widthSize; } else if (widthMode == MeasureSpec.AT_MOST) { //Can't be bigger than... width = Math.min(desiredWidth, widthSize); } else { //Be whatever you want width = desiredWidth; } //Measure Height if (heightMode == MeasureSpec.EXACTLY) { //Must be this size height = heightSize; } else if (heightMode == MeasureSpec.AT_MOST) { //Can't be bigger than... height = Math.min(desiredHeight, heightSize); } else { //Be whatever you want height = desiredHeight; } //MUST CALL THIS setMeasuredDimension(width, height); } ``` 解決的方法總結: - 1。指定一個預設的內部寬高,例如本方法中的desiredWidth = 100。 - 2。判斷當MeasureSpec的模式為AT_MOST(對應於wrap_content)時,設定結果為設定的值(最大為我們設定的值,如果小於設定值就設定為specSize)。 - 3。判斷當MeasureSpec的模式為非AT_MOST時,直接設定為系統的測量值即可。 - 4。設定值沒有特定的標準,以實際情況為準。