【Android】RelativeLayout の仕組みについて理解しよう
王セン
この記事は RECRUIT MARKETING PARTNERS Advent Calendar 2015 の投稿記事です。
こんにちは。2015年度新卒 Android エンジニアの王でございます。
Android には様々な UI エレメントがあります。FrameLayout
やLinearLayout
などの ViewGroup。Button
やTextView
のようなViewなどなど…。特に開発中においては、RelativeLayout を駆使することが多いです。RelativeLayout は LinearLayout と比べてより自由に UI エレメントの位置を設定することができ、それによってレイアウトのネスト数を削減することが出来るのが特徴です。
今回は、ソースコードを交えつつ RelativeLayout の描画の仕組みについて説明します。
RelativeLayout とは
Androidアプリにおいて全ての画面は View と ViewGroup で構成されています。View は画面上に描画され、ユーザーとの相互作用するもの。ViewGroup は View や ViewGroup を保持するオブジェクトです。
今回説明する RelativeLayout は ViewGroup を継承しています。
下記は RelativeLayout を使った簡単な xml ファイルの例です。center_button
ボタン を画面の中心に置き、right_button
ボタンをcenter_button
ボタンの右側に設置しています。
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="@+id/center_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="center" />
<Button
android:id="@+id/right_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_toRightOf="@+id/center_button"
android:text="right" />
</RelativeLayout>
id=center_button
のボタンはandroid:layout_centerInParent="true"
を指定しているため中心に設置され、id=right_button
のボタンはandroid:layout_centerVertical="true"
とandroid:layout_toRightOf="@+id/center_button"
を指定しているので垂直方向に中心かつ中心に置かれたボタンの右側に設置されます。親に対してどの位置に設定するか、他の View に対してどこに設置するかということが簡単にできる。これが RelatveLayout です。
通常の View の描画の仕組み
RelativeLayout の描画の仕組みについて説明する前に、まずは View の描画の仕組みについて説明します。View を画面に表示する際は下記の3ステップを踏みます。
- サイズを測る
- 配置する
- 描画する
それでは各ステップについて説明していきます。
1. onMeasure
View を描画するためにはまず View のサイズを決めなければなりません。サイズが決まるタイミングは onMeasure()
関数が呼ばれた時で、その中の setMeasuredDimension()
でサイズを決定しています。ただし、継承などをして setMeasuredDimension()
が呼ばれない場合、サイズは 0 になります。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
上記が View の onMeasure() 関数です。widthMeasureSpec
と heightMeasureSpec
には親 View から『この View はどのようなサイズになってほしいか』という情報が入ってきます。値は int 型ですが、モード情報が上位 2bit に、サイズ情報がそれ以外に入っています。MeasureSpec には3つのモードあります。
MeasureSpec.UNSPECIFIED |
00 |
特に何も指定しないモード |
---|---|---|
MeasureSpec.EXACTLY |
01 |
値を指定するモード |
MeasureSpec.AT_MOST |
10 |
指定した値以下にするモード |
この onMeasure が親から子へ、さらに子から孫へと伝わっていくことで、それぞれのサイズが決まっていきます。
2. onLayout
サイズが決まったところで、次は配置をしていきます。対象の関数はonLayout()
です。
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {}
上記は View の onLayout() 関数です。見ての通り処理がありません。ViewGroup でない View には配置する対象の子供の View がいないので、 空の関数となっております。ちなみに ViewGroup では onLayout は abstract で定義されています。
3. draw
サイズを決めて配置が完了したら、最後に描画をします。View 及び子 View の位置や色などに応じて実際に描画していきます。まずは onDraw() 関数を見てみましょう。
/**
* Implement this to do your drawing.
*
* @param canvas the canvas on which the background will be drawn
*/
protected void onDraw(Canvas canvas) {
}
onDraw()
関数は開発者が実装するために用意されているだけで、空の関数となっています。実際の処理は draw()
関数で行っております。
public void draw(Canvas canvas) {
final int privateFlags = mPrivateFlags;
final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE && (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
// Step 1, draw the background, if needed
if (!dirtyOpaque) {
drawBackground(canvas);
}
...
// Step 2, save the canvas' layers
...
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
// Step 5, draw the fade effect and restore layers
...
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
}
背景 > 自身 > 子 View > 装飾品 ( スクロールバーなど ) という順番で描画されていきます。また dispatchDraw 関数は子 View を描画するための関数で、ViewGroup がこれをオーバーライドします。
以上が View の描画の仕組みとなります。
【本題】RelativeLayout の描画の仕組み
通常の View の描画の流れが分かったところで、本題の RelativeLayout の描画の仕組みを見ていきます。
DependencyGraph について
onMeasure などの関数の説明に行く前に、RelativeLayout の描画の仕組みを説明する上では欠かせない DependencyGraph の説明をします。
早速ソースコードを見てみます。
private static class DependencyGraph {
// すべての子 View 集
private ArrayList < Node > mNodes = new ArrayList < Node > ();
// 子 View のidをkeyとしたマップ
private SparseArray < Node > mKeyNodes = new SparseArray < Node > ();
// グラフのルート群
private LinkedList < Node > mRoots = new LinkedList < Node > ();
// グラフのクリア
void clear() {...}
// グラフに View を追加する
void add(View view) {...}
// 依存関係のruleに従って、子 View 達をソートする関数。
// たとえば、CがAに依存し、AがBに依存している場合、B -> A -> Cという順番になる。
void getSortedViews(View[] sorted, int...rules) {
final LinkedList < Node > roots = findRoots(rules);
int index = 0;
while (roots.size() > 0) {
final Node node = roots.removeFirst();
final View view = node.view;
final int key = view.getId();
sorted[index++] = view;
final HashSet < Node > dependents = node.dependents;
// nodeがsortedに追加しましたため、nodeに依存しているノードから、nodeを外す
for (Node dependent: dependents) {
final SparseArray < Node > dependencies = dependent.dependencies;
dependencies.remove(key);
if (dependencies.size() == 0) {
roots.add(dependent);
}
}
}
}
// グラフの依存関係rulesFilterによって、グラフを生成しながら、ルート群を取得する関数。
private LinkedList < Node > findRoots(int[] rulesFilter) {
final SparseArray < Node > keyNodes = mKeyNodes;
final ArrayList < Node > nodes = mNodes;
final int count = nodes.size();
...// グラフのクリア処理
// グラフの生成
for (int i = 0; i < count; i++) {
final Node node = nodes.get(i);
final LayoutParams layoutParams = (LayoutParams) node.view.getLayoutParams();
// rules はこの子 View が参照している他の子 View の id 集
final int[] rules = layoutParams.mRules;
final int rulesCount = rulesFilter.length;
for (int j = 0; j < rulesCount; j++) { final int rule = rules[rulesFilter[j]]; if (rule > 0) {
// 依存されるノード
final Node dependency = keyNodes.get(rule);
if (dependency == null || dependency == node) {
continue;
}
dependency.dependents.add(node);
node.dependencies.put(rule, dependency);
}
}
}
final LinkedList < Node > roots = mRoots;
roots.clear();
// ルート集の生成
for (int i = 0; i < count; i++) {
final Node node = nodes.get(i);
if (node.dependencies.size() == 0) roots.add(node);
}
return roots;
}
}
DependencyGraph には主に3つの機能があることが上記からわかります。
- 子 View の追加
- 子 View の依存グラフを作る。
- 独自の順番で子 View 達をソートする。
確かに RelativeLayout においては依存関係に従った順番で View のサイズを決める必要がありそうです。そのためにこの DependencyGraph というクラスが作られています。ちなみに DependencyGraph 内で使用されている Node の定義は下記となります。
static class Node implements Poolable<Node> {
// このノードが表示している子 View
View view;
// このノードに依存しているノード群
final HashSet<Node> dependents = new HashSet<Node>();
// このノードがどのノードに依存しているかのマップ
// Keyは依存される子 View の id
// Valueは依存されるノード
final SparseArray<Node> dependencies = new SparseArray<Node>();
...
}
ご覧の通り Node は子 View と 1:1 になっており、依存関係などの情報を保持します。
onMeasure
RelativeLayout 内での子 View の依存関係に従った順番を整理した後に通常のフローに則ってサイズを決定します。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mDirtyHierarchy) {
mDirtyHierarchy = false;
// ここで、グラフの子 View 達をソートします
sortChildren();
}
int myWidth = -1;
int myHeight = -1;
int width = 0;
int height = 0;
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
// 親のMeasureSpecモードに応じて、大きさを初期設定します
if (widthMode != MeasureSpec.UNSPECIFIED) {
myWidth = widthSize;
}
if (heightMode != MeasureSpec.UNSPECIFIED) {
myHeight = heightSize;
}
if (widthMode == MeasureSpec.EXACTLY) {
width = myWidth;
}
if (heightMode == MeasureSpec.EXACTLY) {
height = myHeight;
}
...
int left = Integer.MAX_VALUE;
int top = Integer.MAX_VALUE;
int right = Integer.MIN_VALUE;
int bottom = Integer.MIN_VALUE;
...
// 水平measure
View[] views = mSortedHorizontalChildren;
int count = views.length;
for (int i = 0; i < count; i++) {
View child = views[i];
if (child.getVisibility() != GONE) {
LayoutParams params = (LayoutParams) child.getLayoutParams();
// 水平の rule に応じて、子 View の params を設置します
applyHorizontalSizeRules(params, myWidth);
// 中に child の measure を呼びます。つまり、子 View を measure します
measureChildHorizontal(child, params, myWidth, myHeight);
// 子 View の params を measure したサイズで設置します
if (positionChildHorizontal(child, params, myWidth, isWrapContentWidth)) {
offsetHorizontalAxis = true;
}
}
}
// 垂直measure
views = mSortedVerticalChildren;
count = views.length;
for (int i = 0; i < count; i++) {
View child = views[i];
if (child.getVisibility() != GONE) {
LayoutParams params = (LayoutParams) child.getLayoutParams();
applyVerticalSizeRules(params, myHeight);
measureChild(child, params, myWidth, myHeight);
if (positionChildVertical(child, params, myHeight, isWrapContentHeight)) {
offsetVerticalAxis = true;
}
if (isWrapContentWidth) {
width = Math.max(width, params.mRight);
}
if (isWrapContentHeight) {
height = Math.max(height, params.mBottom);
}
if (child != ignore || verticalGravity) {
left = Math.min(left, params.mLeft - params.leftMargin);
top = Math.min(top, params.mTop - params.topMargin);
}
if (child != ignore || horizontalGravity) {
right = Math.max(right, params.mRight + params.rightMargin);
bottom = Math.max(bottom, params.mBottom + params.bottomMargin);
}
}
}
... // 他の処理
// measureした大きさをRelativeLayoutに設定します
setMeasuredDimension(width, height);
}
このコードから下記の事が分かります。
sortChildren
でグラフのソートをしている- 水平方向について各 View のサイズを決定していく
- 垂直方向について各 View のサイズを決定しつつ、RelativeLayout のサイズも決定する
onLayout
RelativeLayout の onLayout() 関数は下記です。
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int count = getChildCount();
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
if (child.getVisibility() != GONE) {
RelativeLayout.LayoutParams st = (RelativeLayout.LayoutParams) child.getLayoutParams();
child.layout(st.mLeft, st.mTop, st.mRight, st.mBottom);
}
}
}
ここでは依存関係など気にせずに単純に各子 View の layout 関数を呼んでいるだけです。
dispatchDraw
RelativeLayout は ViewGroup の dispatchDraw 関数をオーバーライドせずにそのまま使っています。
protected void dispatchDraw(Canvas canvas) {
boolean usingRenderNodeProperties = canvas.isRecordingFor(mRenderNode);
final int childrenCount = mChildrenCount;
final View[] children = mChildren;
int flags = mGroupFlags;
...
// フラグの設定
mGroupFlags &= ~FLAG_INVALIDATE_REQUIRED;
...
for (int i = 0; i < childrenCount; i++) {
...
int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i;
final View child = (preorderedList == null) ? children[childIndex] : preorderedList.get(childIndex);
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
more |= drawChild(canvas, child, drawingTime);
}
}
...
if ((flags & FLAG_INVALIDATE_REQUIRED) == FLAG_INVALIDATE_REQUIRED) {
invalidate(true);
}
...
}
こちらも onLayout 同様、順番に描画しているだけですね。
まとめ
いかがでしたでしょうか。今回は RelativeLayout の描画の仕組みについてざっくりとご紹介しました。次回はパーフォーマンスについてご紹介したいと思います。