【Android】RelativeLayout の仕組みについて理解しよう

この記事は RECRUIT MARKETING PARTNERS Advent Calendar 2015 の投稿記事です。

こんにちは。2015年度新卒 Android エンジニアの王でございます。

Android には様々な UI エレメントがあります。FrameLayoutLinearLayoutなどの ViewGroup。ButtonTextViewのようなViewなどなど…。特に開発中においては、RelativeLayout を駆使することが多いです。RelativeLayout は LinearLayout と比べてより自由に UI エレメントの位置を設定することができ、それによってレイアウトのネスト数を削減することが出来るのが特徴です。

今回は、ソースコードを交えつつ RelativeLayout の描画の仕組みについて説明します。

RelativeLayout とは

Androidアプリにおいて全ての画面は View と ViewGroup で構成されています。View は画面上に描画され、ユーザーとの相互作用するもの。ViewGroup は View や ViewGroup を保持するオブジェクトです。

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. サイズを測る
  2. 配置する
  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() 関数です。widthMeasureSpecheightMeasureSpec には親 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つの機能があることが上記からわかります。

  1. 子 View の追加
  2. 子 View の依存グラフを作る。
  3. 独自の順番で子 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);
}

このコードから下記の事が分かります。

  1. sortChildrenでグラフのソートをしている
  2. 水平方向について各 View のサイズを決定していく
  3. 垂直方向について各 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 の描画の仕組みについてざっくりとご紹介しました。次回はパーフォーマンスについてご紹介したいと思います。