androidで厄介なレイアウト回り、onMeasureとonLayoutを理解し、オーバーライドするとより理想のレイアウトに近づけます。
結構挙動が難しいので、長くなってしまいますがこれらの概要です。
(※おおざっぱな流れで細かい部分は違いますが、イメージはつかめると思います)
そもそもこの2つは?
AndroidではView生成時や、Viewの内容が更新される(TextView#setText()等)で、
View自体の再レイアウトが必要なときに2つが呼び出されます。
以下のようなイメージです。
- 親ViewのonMeasureが呼び出される
- 親Viewが子Viewのmeasureを呼び、幅高さを計測させる
- (2)の情報をもとに、親Viewが自分自身の幅高さを設定する
- 親ViewのonLayoutが呼び出される
- 親Viewが子Viewのlayoutを呼び、子Viewの場所を確定させる
このようなフローで各Viewのサイズ、場所が決まるわけです。
※ここでは単一のViewGroupで書きましたが、実際はこの流れが再帰的に発生します。
簡単にいうと、onMeasureは自分自身の幅高さを確定させるもの、onLayoutは子Viewの位置を決めるもの です。
詳細を見ていきましょう。
onMeasure
onMeasureですべきことは以下の2つです
- (ViewGroupの場合)子Viewのサイズを measureメソッドを用い、適切なサイズに設定する
- 引数や子Viewから、自分自身のサイズを setMeasuredDimension で確定させる
ただしonMeasureの引数はちょっと面倒で、サイズ情報とモード情報の2つが入ってます。
@Override
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
//高さも同様
//サイズ確定のためがこの後に続く
}
sizeはわかると思います。ではこのModeとは何なのか?
Modeには以下の3種類が定義されています
- MeasureSpec.EXACTLY : sizeで指定した値に幅(高さ)を設定すること。子Viewもこの範囲に収まるようにサイズを決める。
- MeasureSpec.UNSPECIFIED : 特に指定なし。自由で構いません
- MeasureSpec.AT_MOST : sizeで指定した値以下にする。子Viewもそれ前提でサイズを決める
これらが幅と高さで独立して渡されます。
つまり 「幅は240で高さは自由で」「幅は自由だけど高さ100以内」 という指定がされるわけです。
これらはよく複数回連続して呼び出されます。
仮に次のようなXMLを考えてみましょう(記述を端折ってます)
<LinearLayout width="fill" height="wrap" orientation="horizontal">
<TextVIew width="0dip" height="wrap" weight="1"/>
<Button width="wrap" height="wrap"/>
</LinearLayout>
よくあるパターンですね。この場合はおおざっぱに以下のようになります。
(幅480、高さ800 とした場合)
- LinearLayout の onMeasure が
W=480|EXACTRY, H=0|UNSPECIFIED
で呼び出される - LinearLayout が
W=0|EXACTLY, H=0|UNSPECIFIED
で TextView#measure を呼び出す - LinearLayout が
W=0|UNSPECIFIED, H=0|UNSPECIFIED
で Button#measure を呼び出す - 子Viewの仮の幅の計算完了。余白を計算できるので、TextViewとButtonの幅が確定する
- TextViewとButton の measure を
W=確定幅|EXACTLY, H=0|UNSPECIFIELD
で呼び出す。 - これで子Viewのサイズ確定処理が完了。LinearLayout 自身のサイズを決める
- 幅は引数がEXACTLYなので480 / 高さはTextViewとButtonの高さの大きいほうを使う
- setMeasuredDimensionを呼び出して幅高さを確定させる
という流れになります。(※実際はもっと複雑です)
子Viewのmeasureに渡す引数には、MeasureSpec.makeMeasureSpec(int size, int mode)
を用います。
また、子Viewの確定サイズを知るには、 getMeasuredWidth(), getMeasuredHeight()
を用います。
onLayout
これでようやくonMeasureが終わりました。
あとは簡単です。それをもとにonLayoutをオーバーライドしてレイアウトするだけです。
このときの注意点は以下の通りです。
- 子Viewのサイズは必ずgetMeasuredWidth(),getMeasuredHeight() で返ってくる値を使ってください
- 特に初回レイアウト時、getWidth()/getHeight()は0で値がないです。
- getMeasuredWidth(),Height()と違う値を指定しないでください。どうなるかわかりません。
- 自分自身の左上に配置したいときは(0, 0)に配置します。引数のleff, topは無視でOKです。
もちろん、ちゃんと作るには各ViewのLayoutParamからマージン等を計算する必要ありますし
Gravity等も考慮必要です。
そこでよく使うのが、親Viewに配置させて、ちょっとだけ場所をずらすパターンです。
@Override
public void onLayout(int left, int top, int right, int bottom, boolean changed) {
super.onLayout(left, top, right, bottom, changed);
View view = findViewById(R.id.view_id);
int vl = view.getLeft(); //super.onLayoutでViewのleft,top等には値が入っている
int vt = view.getTop();
int vr = view.getRight();
int vb = view.getBottom();
//ちょっと左にずらしますよ...
vl -= 100;
vr -= 100;
view.layout(vl, vt, vr, vb); //このViewだけ位置をずらす
}
FrameLayoutやLinearLayoutの基本のパターンからちょっとずらしたいときに非常に有効です。
逆に親Viewに頼らない自作Viewの場合、特定画面でのみ使うことが多いので、GravityやMargin属性を全く考慮せず(XML側でも指定せずに)レイアウトすることが多いです。
まとめ
- onMeasureはサイズを確定させる
- onMeasureの引数にはモードとサイズが2つ含まれているので注意
- onMeasureでは必ずsetMeasuredDimensionを呼び出す
- onLayoutでの子Viewの位置を決める
- 両方とも、まずsuper.onXXXを呼び出した後にカスタムすると楽できる
うまく使いこなせばかなりレイアウトの自由度があがります。
慣れるまでは難しいと思いますが積極的にトライしてみてください。
※おまけ
ちゃんとやろうとすると、onMeasureは相当面倒です。
LinearLayoutの内部処理がいかにすごいことをしているか....
逆にいうと、それだけ重い処理になります 。
しかもView階層が深いとその重い処理がView階層分だけ再帰的に処理が走りますし。
深すぎるView階層には注意です。階層を浅くするため、自作ViewGroupやRelativeLayoutをうまく使いましょう