1. ホーム
  2. Android

Android LayoutInflaterの原則の分析は、ビュー(a)のステップの深い理解によってあなたのステップを取る

2022-02-17 07:57:15

以下より許可を得て転載 http://blog.csdn.net/guolin_blog/article/details/12921889


久しぶりにブログを書くので、ちょっとさびしくなっています。ここしばらく仕事に追われていて、ようやくまた書く時間ができました。


Viewについて記事を書いてほしい、その仕組みやカスタマイズの方法を話してほしいという反応を何人もの友人からもらいました。このトピックを徹底的にカバーするために、Viewに関する記事をもう少し書いてみようと思っています。というわけで、今日はLayoutInflaterから始めましょう。


Androidに長く触れている方ならLayoutInflaterに馴染みがないことはないと思いますし、主にレイアウトの読み込みに使われていることもご存知でしょう。新しいAndroid仲間は、レイアウトの読み込み作業は、通常、ActivityのsetContentView()メソッドを呼び出して行うので、LayoutInflaterに馴染みがないかもしれません。実は、setContentView()の内部メソッドもレイアウトの読み込みにLayoutInflaterを使っているのですが、この部分はソースコードの内部で、簡単に見ることはできません。そこで今日は、LayoutInflaterのワークフローを詳しく見て、もしかしたら長年の疑問が解消されるかもしれませんよ。


LayoutInflater の基本的な使い方を見てみましょう。とても簡単で、まず LayoutInflater のインスタンスを取得する必要があります。取得方法は 2 つあり、1 つ目は以下のように記述します。

LayoutInflater layoutInflater = LayoutInflater.from(context);

もちろん、同じ効果を得るための別の書き方もある。
LayoutInflater layoutInflater = (LayoutInflater) context
		.getSystemService(Context.LAYOUT_INFLATER_SERVICE);

1つ目の書き方は、実は簡単なのですが、Androidはそれをラップしているだけです。LayoutInflater のインスタンスを取得したら、以下のようにその inflate() メソッドを呼び出してレイアウトを読み込ませることができます。
layoutInflater.inflate(resourceId, root);

最初のパラメータは読み込むべきレイアウトのIDで、2番目のパラメータはレイアウトの外側にネストされる親レイアウト、または不要な場合はNULLです。これはレイアウトのインスタンスを正常に作成し、それを指定された場所に追加して表示させるものです。


LayoutInflater の使用方法を、非常に簡単な例でより視覚的に見てみましょう。例えば、MainActivity のレイアウト ファイルが activity_main.xml で、コードが次のようなプロジェクトがあるとします。

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/main_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

</LinearLayout>

このレイアウトファイルの中身は非常にシンプルで、空のLinearLayoutで中にコントロールがないため、インターフェースには何も表示されないはずです。


次に、別のレイアウトファイルを定義し、button_layout.xmlという名前を付けて、次のようなコードを記述します。

<Button xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Button" >

</Button>

このレイアウトファイルも非常にシンプルで、Buttonボタンがあるだけです。あとは、LayoutInflaterを使って、メインレイアウトファイルのLinearLayoutにbutton_layoutレイアウトを追加する方法を考えましょう。先ほどの使い方を踏まえて、MainActivityのコードを以下のように修正します。
public class MainActivity extends Activity {

	private LinearLayout mainLayout;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		mainLayout = (LinearLayout) findViewById(R.id.main_layout);
		LayoutInflater layoutInflater = LayoutInflater.from(this);
		View buttonLayout = layoutInflater.inflate(R.layout.button_layout, null);
		mainLayout.addView(buttonLayout);
	}

}

ご覧の通り、ここではまず LayoutInflater のインスタンスを取得し、その inflate() メソッドを呼び出してレイアウト button_layout をロードし、最後に LinearLayout の addView() メソッドを呼び出してそれを LinearLayout に加えています。


これでプログラムを実行すると、以下のような結果が得られます。



ボタンがインターフェイスに表示されました LayoutInflaterのテクニックは、Viewを動的に追加する必要があるときに広く使われています。例えば、ScrollViewやListViewでは、LayoutInflaterをよく見かけますね。


もちろん、LayoutInflaterの使い方を説明するだけでは、知りたいことを知る欲求を満たすにはほど遠いのは明らかです。


どの inflate() メソッドのオーバーロードを使用していても、最終的には LayoutInflater 内の次のコードにロールオーバーされます。

public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) {
    synchronized (mConstructorArgs) {
        final AttributeSet attrs = Xml.asAttributeSet(parser);
        mConstructorArgs[0] = mContext;
        View result = root;
        try {
            int type;
            while ((type = parser.next()) ! = XmlPullParser.START_TAG &&
                    type ! = XmlPullParser.END_DOCUMENT) {
            }
            if (type ! = XmlPullParser.START_TAG) {
                throw new InflateException(parser.getPositionDescription()
                        + ": No start tag found!");
            }
            final String name = parser.getName();
            if (TAG_MERGE.equals(name)) {
                if (root == null || !attachToRoot) {
                    throw new InflateException("merge can be used only with a valid "
                            + "ViewGroup root and attachToRoot=true");
                }
                rInflate(parser, root, attrs);
            } else {
                View temp = createViewFromTag(name, attrs);
                ViewGroup.LayoutParams params = null;
                params = null; if (root ! = null) {
                    params = root.generateLayoutParams(attrs);
                    if (!attachToRoot) {
                        temp.setLayoutParams(params);
                    }
                }
                rInflate(parser, temp, attrs);
                if (root ! = null && attachToRoot) {
                    root.addView(temp, params);
                }
                if (root == null || !attachToRoot) {
                    result = temp;
                }
            }
        } catch (XmlPullParserException e) {
            InflateException ex = new InflateException(e.getMessage());
            ex.initCause(e);
            throw ex;
        } catch (IOException e) {
            InflateException ex = new InflateException(
                    parser.getPositionDescription()
                    + ": " + e.getMessage());
            ex.initCause(e);
            throw ex;
        }
        return result;
    }
}


ここから、LayoutInflater は実際には Android が提供する pull parsing メソッドを使用してレイアウト ファイルを解析していることがよくわかります。プル解析メソッドについてよく知らない場合は、ネットで検索すれば多くのチュートリアルがあるので詳細は省きますが、ここでは23行目に注目します。 createViewFromTag()メソッドが呼び出され、ノード名とパラメータが渡されます。このメソッド名から、ノード名からViewオブジェクトを生成するためのメソッドであることが推測できます。そして、それは createViewFromTag()メソッドはcreateView()メソッドを呼び出し、リフレクションを使用してViewのインスタンスを作成し、それを返します。


もちろん、これはルートレイアウトのインスタンスを作成するだけで、31行目で呼び出されます。 31行目のrInflate()メソッドで、以下のようにルートレイアウトの子要素を繰り返し処理します。

private void rInflate(XmlPullParser parser, View parent, final AttributeSet attrs)
        throws XmlPullParserException, IOException {
    final int depth = parser.getDepth();
    int type;
    while (((type = parser.next()) ! = XmlPullParser.END_TAG ||
            parser.getDepth() > depth) && type ! = XmlPullParser.END_DOCUMENT) {
        if (type ! = XmlPullParser.START_TAG) {
            START_TAG) { continue;
        }
        final String name = parser.getName();
        if (TAG_REQUEST_FOCUS.equals(name)) {
            parseRequestFocus(parser, parent);
        } else if (TAG_INCLUDE.equals(name)) {
            if (parser.getDepth() == 0) {
                throw new InflateException("<include /> cannot be the root element");
            }
            parseInclude(parser, parent, attrs);
        } else if (TAG_MERGE.equals(name)) {
            throw new InflateException("<merge /> must be the root element");
        } else {
            final View view = createViewFromTag(name, attrs);
            final ViewGroup viewGroup = (ViewGroup) parent;
            final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
            rInflate(parser, view, attrs);
            viewGroup.addView(view, params);
        }
    }
    parent.onFinishInflate();
}

ご覧の通り、21行目で同じように createViewFromTag()メソッドでViewのインスタンスを作成し、さらに24行目で再帰的に 24行目のrInflate()メソッドでこのViewの下の子要素を見つけ、再帰が完了するたびにViewを親レイアウトに追加していきます。


この場合、レイアウトファイル全体がパースされて完全な DOM 構造が形成され、最終的にトップレベルのルートレイアウトが返されて inflate() プロセスが完了します。


注意深い人は、inflate()メソッドにも、次のような構造で3つの引数を取るメソッドのオーバーロードがあることに気づくかもしれません。

inflate(int resource, ViewGroup root, boolean attachToRoot)

では、この第3引数attachToRootは何を意味するのでしょうか?実は、上のソースコードをよく読めば、自分で答えを分析できるはずなので、ここでは結論から述べることにします。

<スパン

<スパン 1. rootがNULLの場合 attachToRoot は無意味であり、いかなる値を設定しても意味がない。

<スパン <スパン 2. rootがNULLでない場合 attachToRootがtrueに設定されると、読み込まれたレイアウトファイルに対して親レイアウトであるrootが割り当てられます。

<スパン <スパン <スパン 3. rootがNULLでない場合 attachToRootがfalseに設定されると、レイアウトファイルの一番外側のレイヤーにあるすべてのレイアウトプロパティが設定され、ビューが親ビューに追加されると、これらのレイアウトプロパティが自動的に適用されます。

<スパン <スパン <スパン 4. を設定せずに attachToRoot パラメータは、root が NULL でない場合は attachToRoot パラメータのデフォルトは true です。


さて、LayoutInflaterの動作原理と流れが明確になったので、これで満足いただけると思います。この例のボタンは少し小さく見えるので、もっと大きくしたいと思いませんか?それは簡単です。button_layout.xmlのコードを以下のように変更します。

<Button xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="300dp"
    android:layout_height="80dp"
    android:text="Button" >

</Button>

ここでは、ボタンの幅を300dpに、高さを80dpに変更していますが、十分な大きさでしょうか?では、もう一度プログラムを実行して、その効果を確認してみましょう。あれ?ボタンの大きさが変わっていませんね。ボタンの大きさがまだ足りないから、もうちょっと大きくしてみようか?それでもダメです。


実は、ここでButtonのlayout_widthとlayout_heightの値をいくら変更しても、何の効果もありません。この2つの値は、今や全く意味がないからです。通常、私たちがよく使うのは layout_width と layout_height を使用して View のサイズを設定すると、常に実際に View のサイズを設定したかのように動作します。つまり、まずビューがレイアウト内に存在する必要があり、次に layout_width が match_parent に設定されると、ビューの幅がレイアウトいっぱいになることを意味し、wrap_content に設定されると、ビューの幅がちょうどコンテンツを含むのに十分な幅であることを意味し、特定の値に設定されると、ビューの幅は対応する値となります。このため、これら二つの属性は layout_widthとlayout_heightは、widthとheightというより、layout_heightです。


もう一度、私たちの button_layout.xml を見ると、Button コントロールは現時点ではどのレイアウトにも存在しないことがわかります。 layout_widthとlayout_heightプロパティは当然ながら意味がありません。では、ボタンの大きさを変えるには、どのように修正すればよいのでしょうか?最も簡単な方法は、以下のように、ボタンの外側にレイアウトの別のレイヤーをネストすることです。

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <Button
        android:layout_width="300dp"
        android:layout_height="80dp"
        android:text="Button" >
    </Button>

</RelativeLayout>


ここでは、別のRelativeLayoutを追加しており、ButtonはRelativeLayoutの中に存在することがわかります。 layout_widthとlayout_heightプロパティが登場します。もちろん、一番外側の階層にあるRelativeLayoutには layout_widthとlayout_heightは何の効果もありません。さて、プログラムを再実行すると、次のようになります。


<スパン <スパン <スパン <スパン <スパン

<スパン <スパン <スパン <スパン <スパン

<スパン <スパン <スパン <スパン <スパン OK! ボタンのが大きくなったので、ようやくみんなの要求を満たせるようになりました。


<スパン これを見て、もしかしたら皆さんの中には大きな疑問を持っている人がいるかもしれません。それはおかしい!」と。Activityでレイアウトファイルを指定するときに、一番外側のレイアウトのサイズを指定することができるんです。 layout_widthとlayout_heightはどちらも便利です。実際、これは主にsetContentView()メソッドにおいて、Androidが自動的にレイアウトファイルの一番外側の層に別のFrameLayoutをネストさせるためです。 layout_widthとlayout_heightのプロパティが効果を発揮します。そこで、MainActivityのコードを以下のように修正して確認してみましょう。

public class MainActivity extends Activity {

	private LinearLayout mainLayout;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		mainLayout = (LinearLayout) findViewById(R.id.main_layout);
		ViewParent viewParent = mainLayout.getParent();
		Log.d("TAG", "the parent of mainLayout is " + viewParent);
	}

}

ご覧のように、ここでは、activity_mainレイアウトの一番外側のLinearLayoutオブジェクトをfindViewById()メソッドで取得し、その親レイアウトをgetParent()メソッドを呼んで取得し、それをLogで出力しています。ここで、プログラムを再実行すると、次のように表示されます。

<スパン <スパン <スパン

<スパン <スパン <スパン

<スパン <スパン <スパン

<スパン まさにその通り LinearLayoutの親レイアウトは、確かに FrameLayout であり、この FrameLayoutは、私たちのためにシステムが自動的に追加してくれます。


とはいえ、誰もが使っているsetContentView()メソッドですが、Androidのインターフェース表示の原理は、実は私たちが見ているよりもずっと複雑なものなのです。Activityに表示されるインターフェースは、実はタイトルバーとコンテンツレイアウトの2つの主要な部分から構成されています。タイトルバーは多くのインターフェースの上部に表示される部分で、例えばこの例ではタイトルバーがありますが、これを表示するかしないかはコードで制御できます。コンテンツレイアウトはFrameLayoutで、このレイアウトのidはcontentと呼ばれ、このレイアウトのために setContentView()メソッドを呼び出す際に渡すレイアウトは、実際にはこの中に入れています。 このメソッドが setView() ではなく setContentView() と呼ばれるのはそのためです。


<スパン <スパン <スパン <スパン <スパン 最後に、より直感的に理解できるように、Activityウィンドウの構成図を載せておきましょう。

<スパン <スパン <スパン <スパン <スパン <スパン

<スパン <スパン <スパン <スパン <スパン <スパン

<スパン <スパン <スパン <スパン <スパン さて、今日はここまで、サポーター、荒らし、質問、通りすがりのお友達はコメントをどうぞ^v^ 興味のあるお友達は続きを読んでください。 Androidのビューの描画処理を完全に説明し、ビューのステップの深い理解によってあなたのステップを取る (II)  .

<スパン <スパン <スパン <スパン <スパン <スパン

技術公開ページでは、質の高い技術記事を毎日更新しています。仕事や勉強に疲れた時の息抜きに、エンターテイメントサイトをフォローしてください。

以下のQRコードをWeChatで掃引し、フォローしてください。