カスタム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つの情報のビット和になっていて、以下のようにそれぞれの値を取り出すことができます。
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ごとに特殊な要望があるかもしれません、その場合はそれぞれの事情に合わせて諸々の計算が必要になってきますが、基本はこうすれば良いという最低限の実装を示しました。
以上です。