2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Posted at

カスタム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の引数はwidthMeasureSpecheightMeasureSpecという2つのint値が引数となっています。このint値はmodeとsizeの2つの情報のビット和になっていて、以下のようにそれぞれの値を取り出すことができます。

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の実装がどうなっているかを確認してみましょう

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というメソッドがあるのでこちらを使います。

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がイメージ通りに動作しますね。

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ごとに特殊な要望があるかもしれません、その場合はそれぞれの事情に合わせて諸々の計算が必要になってきますが、基本はこうすれば良いという最低限の実装を示しました。

以上です。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?