1. 程式人生 > >android-自定義View解決wrap_content無效的問題

android-自定義View解決wrap_content無效的問題

###問題提出

在我們自定義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。設定值沒有特定的標準,以實際情況為準。