仕事の Android 開発でタブを使ったレイアウトが必要になったので、サンプルを作りながら使用方法を学んでみる。
もくじ
タブの約束事
タブを使った画面を作る場合、レイアウト指定には約束事がある。例えば画面の上側にタブのつくレイアウトの場合、以下のように指定する。
<?xml version="1.0" encoding="utf-8"?> <TabHost xmlns:android="http://schemas.android.com/apk/res/android" android:id="@android:id/tabhost" android:background="#0094FF" android:layout_width="fill_parent" android:layout_height="fill_parent"> <LinearLayout android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent"> <!-- タブ --> <TabWidget android:id="@android:id/tabs" android:layout_width="fill_parent" android:layout_height="wrap_content" /> <!-- セパレータ --> <FrameLayout android:background="#222222" android:layout_width="fill_parent" android:layout_height="1dp" /> <!-- タブの内容 --> <FrameLayout android:id="@android:id/tabcontent" android:layout_weight="1" android:layout_width="fill_parent" android:layout_height="fill_parent" /> </LinearLayout> </TabHost>
規定の id を指定している 4、16、28 行目をハイライトしておいた。
タブ画面の大枠は TabHost という要素になる。id には android:id/tabhost を指定する必要がある。この中に各種要素を配置してゆく。
まず、タブとそれに対応する内容のレイアウトを定義する。これはタブの位置を考慮して決める。
通常は LinearLayout を指定しておくのがよいだろう。タブが内容へ重なるように表示したい場合は RelativeLayout などを使う。
タブは TabWidget という要素で定義して、id は @android:id/tabs、タブの内容は FrameLayout 要素で定義し、id に @android:id/tabcontent を指定する。
このルールだけ守れば、後のレイアウトは自由である。
例えば前述の定義のように、タブと内容の間へセパレータを入れたり、タブ画面全体のメニュー的なものをつけてもよい。
次にタブの追加だが、これはタブを定義したレイアウト XML に対応する TabActivity 派生クラスでおこなう。
TabActivity.getHost メソッドを呼び出すと、タブを管理している TabHost インスタンスへの参照を得られる。これの addTab メソッドに、タブの内容を設定した TabSpec インスタンスを指定すれば、それがタブとして追加される。
規定のアイコンとテキストで構成されるタブを利用する場合は、以下のようになる。
@Override public void onCreate( Bundle savedInstanceState ) { super.onCreate( savedInstanceState ); this.setContentView( R.layout.tab_top ); TabHost host = this.getTabHost(); TabSpec spec = host.newTabSpec( "タブ 1" ); // アイコンとテキストを指定 { Resources r = this.getResources(); spec.setIndicator( "タブ 1", r.getDrawable( R.id.tab_icon ) ); } // 内容となる画面 ( Activity ) の指定 { Intent intent = new Intent(); intent.setClass( this, TestActivity.class ); spec.setContent( intent ); } host.addTab( spec ); }
カスタマイズ
TabSpec.setIndicator メソッドには View を引数に取るオーバーロードがある。これを利用すれば、独自の表示内容を指定できる。
例えば以下のようにレイアウトを定義しておいて、
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:background="@drawable/selector_tab_v" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent"> <!-- セパレータ --> <FrameLayout android:id="@+id/tab_item_separator" android:background="#222222" android:layout_width="fill_parent" android:layout_height="1dp" /> <!-- タブ --> <LinearLayout android:orientation="vertical" android:gravity="center" android:padding="8dp" android:layout_width="fill_parent" android:layout_height="fill_parent"> <!-- アイコン --> <ImageView android:id="@+id/tab_item_icon" android:layout_marginBottom="4dp" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <!-- テキスト --> <TextView android:id="@+id/tab_item_text" android:textColor="#FFFFFF" android:textStyle="bold" android:textSize="12dp" android:layout_width="wrap_content" android:layout_height="wrap_content" /> </LinearLayout> </LinearLayout>
TabActivity 派生クラスに以下のようなメソッドを定義しておけば、
/** * タブを生成します。 * * @param owner タブ画面。 * @param icon アイコンのリソース識別子。 * @param text テキスト。 * @param layout タブのレイアウトを示すリソース識別子。 * @param isFirst はじめのタブなら true。それ以外は false。 * * @return 生成されたタブ情報。 */ private TabSpec createTabSpec( TabHost host, int icon, String text, int layout, boolean isFirst ) { View v = LayoutInflater.from( this ).inflate( layout, null ); // 始点なら区切り線を消す if( isFirst ) { v.findViewById( R.id.tab_item_separator ).setBackgroundColor( 0 ); } ( ( ImageView )v.findViewById( R.id.tab_item_icon ) ).setImageResource( icon ); ( ( TextView )v.findViewById( R.id.tab_item_text ) ).setText( text ); TabSpec spec = host.newTabSpec( text ); spec.setIndicator( v ); Intent intent = new Intent(); intent.setClass( this, TestActivity.class ); spec.setContent( intent ); return spec; }
以下のように呼び出せる。
@Override public void onCreate( Bundle savedInstanceState ) { super.onCreate( savedInstanceState ); this.setContentView( R.layout.tab_top ); TabHost host = this.getTabHost(); int layout = R.layout.tab_item_h; host.addTab( this.createTabSpec( host, R.drawable.tab_icon_1, "タブ 1", layout, true ) ); host.addTab( this.createTabSpec( host, R.drawable.tab_icon_2, "タブ 2", layout, false ) ); host.addTab( this.createTabSpec( host, R.drawable.tab_icon_3, "タブ 3", layout, false ) ); host.addTab( this.createTabSpec( host, R.drawable.tab_icon_4, "タブ 4", layout, false ) ); }
状態によって描画方法を変える
Android のリソースには selector という仕組みがあり、状態に依存して描画方法を変更できるようになっている。
例えば drawable として icon_1.png と icon_1a.png という二つの画像があるとする。これらをタブが非選択・選択の時に分けて表示する場合、まずは drawable に以下のような selector を定義する。
<?xml version="1.0" encoding="utf-8"?> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:drawable="@drawable/icon_1" android:state_selected="false" /> <item android:drawable="@drawable/icon_1a" android:state_selected="true" /> </selector>
selector の子として item を定義し、その中に android:state_~ 属性を設定すると、それに応じた android:drawable が反映される。
この仕組みを利用すると、レイアウトは共通でコンテンツ部分だけ可変、といった設計を実現しやすい。コントロールをカスタム描画する時は、真っ先に検討すべき方法だと思う。
ただし現時点の Eclipse/ADT では、selector や shape の XML 編集でインテリセンスが効かないうえ、間違った記述をしてもエラーにならない場合があるので注意する。
例えば selector の item を、itam のようにスペルミスしても無視される。
一方、android:drawable を android:drawabla にした場合はきちんとエラーになるため、この動きを想定して前者のミスをすると、思わぬハマリに繋がる。
ちなみに item をスペルミスした状態でビルドされたアプリを実行すると、item の定義が無視されるようだ。しかし将来の ADT で厳密なチェックがサポートされる可能性もあるため、この動作に期待しないこと。
Android のサポートしている属性と値については、以下が参考になる。
タブを画面の左右に置く
Android の TabWidget は水平方向のレイアウトで実装されているため、画面の上下には置けるけれど、左右の場合は都合が悪い。
しかし端末の向き変更に対応した場合、縦の時は下にタブを置き、横では右に配置したくなるかもしれない。そのような要求に応えるためにも、TabWidget で垂直方向のレイアウトをサポートする方法を調べておこうと思った。
もしかして、既にそれを実現している人がいるかもしれない、とググってみたら、まさにドンぴしゃな記事を見つけた。
ここに書かれている方法を試したところ、見事に垂直レイアウトなタブを実現できた。
ただ、なぜか私の環境では ViewGroup.LayoutParams.MATCH_PARENT がエラーになるので、代わりに FILL_PARENT を使ってみた。
前述の記事には Android の TabWidget 実装についても説明しているので、原理が非常に分かりやすい。要約すると、このコントロールは水平前提で実装されているが、派生クラスの工夫でそれを変更できる、といった感じか。
サンプル プログラム
ここまで学んだ内容を使って、簡単なサンプルを作ってみる。仕様は以下のようになる。
- タブをカスタム描画する
- タブを上下左右に配置する
- タブの内容は Activity にする
まず、タブの上下左右を選ぶ画面を作成する。
選ばれたレイアウトに応じて、個別の画面を表示する。
Top/Bottom は一般的なレイアウト。
Left/Right は左右に垂直なタブを表示するレイアウト。
工夫した点として、タブ画面は TabHostActivity というひとつのクラスで担当させ、レイアウトの分岐はリソース選択としている。
実際、タブ画面が複数あるアプリを作成する場合でも、それらが大きく異なることは考えにくいので、このような感じの実装になるのではなかろうか。
最後に、サンプル プログラムのプロジェクトを公開しておく。
TestTabActivity.zip 43.3KB
Android 2.1 update 1 ( API Level 7 ) でビルドし、エミュレータと初代 Xperia ( SO-01B ) にて動作確認をおこなった。
また、オマケとして今回のサンプルで使用したアイコンの SVG ファイルをつけておいた。
もしサンプルのアイコンが小さい・大きいと思ったら、これらを Inkscape などで開き、好みのサイズで PNG ファイルとしてエクスポートし、それを利用する。