カスタムViewを作る時、onMeasureをどのように実装すべきか

カスタムViewを実装するとき、onMeasureを実装していますか？
別に何もしなくても使えるから何もしてない、って人も多いでしょう。私もそうです。
しかし、Viewの種類にもよりますが、wrap_contentが適切に動作しない場合がありますので、どのように実装すべきかを整理してみます。

# onMeasureとはどういうメソッドか？

`onMeasure`というメソッドは親となるViewGroupが子Viewの大きさを決めるためにコールします。（より正確には子Viewのmeasureをコールし、その中で`onMeasure`がコールされます）
子Viewは`onMeasure`がコールされると、引数で渡されたMeasureSpecの値を元に、自身のViewの大きさを決めます。
戻り値を返すのではなく、onMeasureの中で`setMeasuredWidth`/`setMeasuredHeight`で自身の大きさを設定しておき、親Viewは`getMeasuredWidth`/`getMeasuredHeight`で子Viewが決めた大きさを参照します。

なお、`setMeasuredWidth`/`setMeasuredHeight`では、上位8bitは状態フラグを保持する値を設定します。
`getMeasuredWidth`/`getMeasuredHeight` はマスク後の値を返すため、戻り値をそのままサイズとして扱えます。

# MeasurSpecとは

`onMeasure`の引数は`widthMeasureSpec`と`heightMeasureSpec`という2つのint値が引数となっています。このint値はmodeとsizeの2つの情報のビット和になっていて、以下のようにそれぞれの値を取り出すことができます。

```kotlin
val mode = MeasureSpec.getMode(spec)
val size = MeasureSpec.getSize(spec)
```

sizeの方はピクセル単位のサイズ情報です
modeはMeasureSpecに定義されたUNSPECIFIED/EXACTLY/AT_MOSTの3パターンあります。

|mode|意味|
|-|-|
|UNSPECIFIED|サイズに関する要求なし。ScrollViewのスクロール方向など、上限なしで任意の大きさを取ることができる場合|
|EXACTLY|size情報の大きさにする要求。layout_width/layout_heightに直値を設定したり、match_parentなど各レイアウトにサイズの決定を委ねる指定をした場合|
|AT_MOST|size情報を最大値としてそれ以下の大きさになることを要求。大きさに上限のあるLayout内でwrap_contentを指定した場合|

# ViewのonMeasureの実装

Viewの実装がどうなっているかを確認してみましょう

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

protected int getSuggestedMinimumHeight() {
    return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}

protected int getSuggestedMinimumWidth() {
    return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}

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

`getSuggestedMinimumHeight` / `getSuggestedMinimumWidth` は、自身のminimumWidth/minimumHeightとbackgroundがあれば、そのminimumWidth/minimumHeightの大きい方を返しています。

getDefaultSizeでは、UNSPECIFIEDの場合は引数のsize、AT_MOST/EXACTLYの場合はmeasureSpec指定のサイズを採用しています。
つまり、wrap_contentを指定された場合、最大サイズとなるような動作をします。
これは通常想像するwrap_contentの動作ではないですね、自身のコンテンツの大きさをまでしか広がらないようになって欲しいところですね。

# 結局onMeasureでは何をすればいいの？

wrap_contentを指定されたとき、コンテンツサイズの大きさをとるように動作させるには、resolveSizeAndStateというメソッドがあるのでこちらを使います。

```java
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);
}
```

AT_MOSTの場合は、引数のサイズ、ただし、specのサイズの方がい小さい場合は、MEASURED_STATE_TOO_SMALLフラグをつけてspecサイズに抑えます。EXACTLYの場合はspecのサイズ、UNSPECIFIEDの場合は引数のsizeを採用します。
sizeにコンテンツの大きさを渡せばwrap_contentがイメージ通りに動作しますね。

```kotlin
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    val w = calculateContentWidth() + paddingLeft + paddingRight
    val h = calculateContentHeight() + paddingTop + paddingBottom
    setMeasuredDimension(
        resolveSizeAndState(maxOf(suggestedMinimumWidth, w), widthMeasureSpec, 0),
        resolveSizeAndState(maxOf(suggestedMinimumHeight, h), heightMeasureSpec, 0),
    )
}
```

コンテンツの大きさを計算するメソッドはそれぞれの用途ごとに用意するとして、そのサイズにパディングを加算したサイズが、このViewが取るべき適切な大きさということになります。
また、背景が持つサイズの方が大きい場合はそちらを採用することになるので、大きい方をresolveSizeAndStateの第一引数に渡します。

与えられた制約の範囲内でアスペクト比固定で最大サイズを取りたいなど、Viewごとに特殊な要望があるかもしれません、その場合はそれぞれの事情に合わせて諸々の計算が必要になってきますが、基本はこうすれば良いという最低限の実装を示しました。

以上です。