レイアウト(8)-Viewの階層構造をあやつる
setContentViewとaddContentView
ActivityのContentViewを設定するには、おなじみのsetContentViewメソッドを使うが、 addContentViewというあまり見かけないメソッドもある。
addContentViewメソッドを使うと、Activity画面に複数のContentViewを重ね合わせて表示させる事ができる。
以下に、その例を示す。
リスト1_1(ViewTree1Activity.java)
実行画面
main.xmlとmain2.xmlの2つのレイアウトファイルにButtonを1つずづ配置し、 このレイアウトファイルを使う2つのContentViewを重ね合わせて表示させている。
それぞれのButtonをクリックすると、それに対応したonClickイベントが発生する。
あたかも、FrameLayoutに配置するように複数のContentViewを配置できることになる。
実は、ContentViewの親ウィジェットはFrameLayoutになっている。 (後述の「Hierarchy Viewer」を参照。)
Viewの階層構造をあやつる
ViewGroupクラスにはレイアウト階層の子ウィジェットを操作するメソッドが, Viewクラスには親ウィジェットを取得するメソッドが存在し、これらのメソッドを使う事で レイアウトの階層構造をあやつることができる。
子ウィジェットをたどる。-getChildCountメソッドとgetChildAtメソッド
ViewGroupクラスは、その内部に表示する複数の子ウィジェットを保持する ArrayListのようなコレクションクラス、とみる事もできる。
ViewGroupクラスの内部にどのような子ウィジェットが含まれているか列挙するには、 ViewGroupクラスのgetChildCountメソッドとgetChildAtメソッドを使う。
以下にその例を示す。
getChildCountメソッドは、ViewGroup内部の子ウィジェットの数を返す。
getChildAtメソッドは、引数で指定された位置にある子ウィジェットを返す。
以下のコードは、idがR.id.layoutであるウィジェットをだどって、 そのレイアウト階層の下層にあるすべてのウィジェットのクラス名をLogに出力するプログラムの例である。
リスト3_1(ViewTree2Activity.java)
以下のようなレイアウトファイルを使って、上記のコードを実行してみる。
LogCatビューには、下図のようにレイアウトの階層構造が表示される。
このリスト3_1のコードはシンプル過ぎて、逆にわかりずらいかもしれない。
蛇足ではあるがjava初心者の方のために、簡単に補足しておく。
18行目のshowChildメソッドに、レイアウト階層を下にたどるすべての処理が含まれている。
19行目のgetSimpleNameメソッドは、リフレクションを使って変数vのクラス名を返す。
sbTabsという変数は、StringBuilderという文字列の連結を効率的に処理するクラスのインスタンスで、 Logに表示する際のインデントを付けるために使っている。
20行目のinstanceof演算子の部分は、変数vがViewGroupクラスまたはViewGroupクラスを継承したクラスのインスタンスであればtrueになる。
もし、変数vがViewGroupクラスを継承したクラスのインスタンスであるならば、子クラスを保持している可能性があるので、さらにさかのぼってその子クラスを調べなさいという意味になる。
24行目でshowChildメソッドより自分自身を呼び出す(再帰という)ことにより、繰り返し 下の階層のウィジェットを表示する処理を実行している。
親ウィジェットをたどる。-getParentメソッド
ViewクラスのgetParentメソッドにより、親ウィジェットを取得できる。
getParentメソッドの戻り値の型は 、ViewParentという聞きなれない名前のインターフェースである。
ViewGroupクラスは、ViewParentインターフェースを実装しており、 実質的にはViewGroupクラスのインスタンスを返すと思って良い。
以下は、getParentメソッドを使ってレイアウト階層のルートウィジェットまで、 親ウィジェットをたどってそのクラス名をLogに表示する例である。
リスト3_2のレイアウトに対して、このプログラムを実行するとLogCatビューは下図のように表示される。
レイアウト階層のルート要素は、DecorViewというクラスになっている事がわかる。
DecorViewって何だろう。
とりあえずググッてみる、この辺が参考になるかな?
子ウィジェットの削除,追加,挿入,置換え,移動
これらの目的に有効と思われる、ViewGroupクラスのメソッドをピックアップしてみた。
- void removeView(View view)
引数で指定された子ウィジェットを削除。 - void removeViewAt(int index)
引数indexで指定された位置の子ウィジェットを削除。 - void removeViews(int start, int count)
引数startで指定されたインデックス位置の子ウィジェットから、count個分のウィジェットを削除。 - void removeAllViews()
すべての子ウィジェットを削除。 - void addView(View child)
引数で指定された子ウィジェットを追加。(おなじみのメソッドなので、得に説明の必要は無いと思う。) - void addView(View child, int index)
引数で指定された子ウィジェットを、index番目の位置に追加。 - void addView(View child, int index, ViewGroup.LayoutParams params)
引数で指定された子ウィジェットを引数paramsに従って、index番目の位置に追加。 - int indexOfChild(View child)
引数で指定された子ウィジェットの位置(インデックス)を取得。
削除,追加,挿入,置換え
以下のプログラムは削除,追加,挿入,置換えの例である。
リスト5_1(ViewTree4Activity.java)
リスト5_1の10行目のPOSITION定数を変更することで挿入,置換をおこなう位置を変更できる。
削除はremoveXXXメソッドを,追加・挿入はaddViewメソッドを, 置換えは削除した後、削除した位置に置き換えるウィジェットを追加すれば実現できる事になる。
このプログラムを実行して、追加,挿入,置換,削除ボタンを何回か押下すると、以下のような画面が表示される。
移動
異なるレイアウト階層の位置に、ウィジェットを移動する事もできる。
以下に、その例を示す。
リスト6_1(ViewTree5Activity.java)
上記のプログラムは、 Buttonが押されるとリスト6_1の28,29行目の処理により、 idがchildLayoutのウィジェットの子ウィジェットの位置から、 idがlayoutの先頭(インデックスが0)の子ウィジェットの位置に移動する。
もう一度Buttonが押されると、 今度は、32,33行目の処理により 元(childLayoutのインデックスが1の子ウィジェット)の位置に戻る。
元の位置 | 移動後の位置 | |
Hierarchy Viewer - 階層ビューア
と、ここまで、手探りでレイアウトの階層構造を探ってきたが、androidのSDKには「Hierarchy Viewer」という便利なツールが付いている。
「Hierarchy Viewer」の表示
「Hierarchy Viewer」をeclipseから表示するには、 メニューより「Windows(W)」→「パースペクティブを開く(O)」→「「階層」ビュー」を選択する。
「Hierarchy Viewer」は、eclipseからではなく単独でも起動できる。
エクスプローラより「SDKインストールデレクトリ\tools」デレクトリ配下にある「hierarchyviewer.bat」をダブルクリックする。
右のペインに表示されている「Windows」ビューより、起動中のアプリケーションを選択して 「Windows」ビューの右上に表示される「Load the view hierarchy into the tree view」アイコンをクリックする。
「Windows」ビューの手前に「プロパティの表示ビュー」が表示されるとともに、中央の「Tree View」に階層ツリーが表示される。
階層ツリーよりウィジェットを選択すると、「プロパティの表示ビュー」にそのウィジェットの属性が一覧で表示されるので便利である。
階層ツリーをたどっていくと、確かにルートに「DecorView」があるのが確認できる。
ActivityのContentViewに指定したLinerLayoutの、すぐ上の階層に FrameLayoutが配置されている。
これにより、ContentViewの親ウィジェットがFrameLayoutになっていて、addContentViewメソッドによりViewを重ね合わせて表示させる事が納得できる。
ContentViewであるLinerLayoutの上に表示されているTextViewは、Windowのタイトルバーとして使用されているものである事がわかる。
ContentViewの取得
Activityには、setContentViewというメソッドが存在するが、 ContentViewを取得するためのgetContentViewというメソッドは存在しない。
では、ContentViewを取得する方法は無いのだろうか?
「Hierarchy Viewer」を覗いてみると、 ContentViewの親ウィジェットのidはandroid.R.id.contentとなっている事がわかる。
従って、idがandroid.R.id.contentである子ウィジェットの最初の子ウィジェットが、setContentViewで指定したContentViewに該当する事が理解できる。
同様に、addContentViewで追加したContentViewは、2番目(インデックスが1)以降の子ウィジェットという事になる。
ちょっとイタズラ
これまでに調べた結果を基に、タイトルバーにアイコンを追加してみる。
リスト8(HelloAndroidActivity.java)
タイトルバーとして使われるTextViewのidは、「Hierarchy Viewer」を覗いてみるとandroid.R.id.titleなので、 このTextViewの親ウィジェットを取得して、その親ウィジェットにImageViewを追加する。
反則技かもしれない(?)が、ViewTreeをたどる事でいろいろな事ができそうな。
レイアウトの効率化
レイアウトを効率的におこなうには、階層構造を深くしない方がいいらしい。
「b. レイアウトトリック:効率的なレイアウトの作成 - ソフトウェア技術ドキュメントを勝手に翻訳」を参照。
レイアウト階層を浅くすると、ウィジェットをレイアウトに配置する処理が少なくて済み、処理効率を上げる事ができる。
sdkにはlayoutoptというツールが用意されていて、レイアウトファイルのウィジェットが効率的に配置されているかどうか、調べてくれるらしい。
詳しくは、「便利な開発ツール:レイアウトを最適化する layoutopt (Android Developers - Dev Guide和訳) - Android(アンドロイド)情報-ブリリアントサービス」を参照。
レイアウトファイルに関する話題を
レイアウトファイルの書式については「 7.5.4 レイアウトリソース - ソフトウェア技術ドキュメントを勝手に翻訳 」を参照。
Androidには、ListView(「ListViewとListActivity(1)-基礎編 - 愚鈍人」を参照)で使われる「simple_list_item_1」等、定義済みのレイアウトファイルが存在する。
ここを参照すると、そのレイアウトのソースが見れるようだ。
レイアウトをインクルード
レイアウトファイルに、別のレイアウトファイルをインクルードすることができる。
詳しくは、 a. レイアウトトリック:再利用可能な UI コンポーネントの作成 - ソフトウェア技術ドキュメントを勝手に翻訳 を参照。
同じレイアウトを何度も使う時には、便利そう。
インクルードのネストが可能かどうか、ためしにてみた。
main.xmlよりinclude1.xmlをインクルード,さらにinclude1.xmlよりinclude2.xmlインクルード。
このプログラムを実行すると、以下のような画面が表示される。
「Hierarchy Viewer」で階層構造を確認してみる。
includeされたLinearLayoutのidが、includeタグで指定したidに置き換わっているのが、確認できる。
注意すべき点は、includeタグのlayout属性には「android:」の接頭辞を付けない事である。
レイアウトをマージ
レイアウトファイルのxmlのルート要素にmageタグを使うと、レイアウト階層を一つ少なくすることができ、レイアウトを効率的におこなう事ができる。
詳しくは、「 d. レイアウトトリック: レイアウトのマージ - ソフトウェア技術ドキュメントを勝手に翻訳 」を参照。
ContentViewにFrameLayoutが必要な場合は、 レイアウトファイルのxmlのルート要素にmergeタグを指定する事で、 ContentViewの親ウィジェットとなるべきFrameLayoutをContentViewの換わりとして使う事ができ、 レイアウト階層を一つ減らすことができる。
次の例は、インクルードされるxmlレイアウトファイルにmergeタグを使用した例である。
インクルードされる側のxmlレイアウトファイルのルート要素にmergeタグを指定することで、 mergeタグの内部の2つTextViewの親要素として、 インクルード元のレイアウトファイルのincludeタグの親要素であるLinerLayoutを使う事ができる。
このプログラムを実行すると、以下のような画面が表示される。
インクロード元のTextViewと、インクルードされる側の2つのTextViewが同じ階層に並んでいる事がわかる。
ViewStub
ViewStubウィジェット(ViewStub | Android Developers)を使用すると、必要になった時にレイアウトファイルをインクルードする事ができる。
詳しくは、「 c. レイアウトトリック: ViewStub の使用 - ソフトウェア技術ドキュメントを勝手に翻訳 」を参照。
たまにしか使われる事のないウィジェットに対して、表示が必要になるまでレイアウトの使用をおさえ、レイアウトを効率的に配置する事ができる。
以下に、ViewStubを配置したレイアウトファイルの例を示す。
リスト11_3(ViewTree6Activity.java)
プログラム起動直後は、ViewStubが置かれた領域には何も表示されないが、 inflateの実行ボタンを押下するとViewStubの領域がsub.xmlのレイアウトに置き換わる。
inflateの実行前 | inflateの実行後 | |
includeタグをViewStubに置き換えた時に間違いやすい点として、ViewStubの場合にはlayout属性の接頭辞に「android:」が付く事、 layout_width属性とlayout_height属性の指定が必須となっている事である。
layout_width属性とlayout_height属性を省略した場合には、 インクルード先のウィジェットのlayout_width属性とlayout_height属性が使われるかと思ったらそうでもないようだ。
以下は、ViewStubをレイアウトファイルを使わずにコードで記述 (リスト11_1のプログラムのmain.xmlの部分をコードに置き換る)した場合の例である。
ViewStubウィジェットは、それなりに便利なのであるが、 mergeタグが使えない, 一旦インフレートしてしまうと、ViewStubウィジェットがインクルードされたレイアウトに置き換わってしまい 元の状態に戻すことができない等の問題もある。
最後に
レイアウトファイルのincludeタグ,mergeタグ,ViewStubはそれなりに便利なのだが、制限もある。
もっと、自分の想うとおり,自由にレイアウトを操作したいならば、面倒ではあるが、 getParent,getChildAt,addView,removeViewなどのメソッドと LayoutInflaterクラスの操作を組み合わせるなどして、 コードを使ってプログラムを書いた方が、 ほとんど,なんでもできてしまうのでいいと思う。