アカベコマイリ

HEAR NOTHING SEE NOTHING SAY NOTHING

Android のタブを使いこなす

仕事の 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>

@android:id/tabhost@android:id/tabs@android:id/tabcontent は Android 既定の識別子。タブ画面の大枠は 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 に icon1.png と icon1a.png という 2 つの画像があるとする。これらをタブが非選択・選択の時に分けて表示するなら 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 では selectorshape の XML 編集でインテリセンスが効かないうえ間違った記述をしてもエラーにならない場合があるので注意する。例えば selectoritemitam のようにスペルミスしても無視される。一方 android:drawableandroid:drawabla にした場合はきちんとエラーになるため、この動きを想定して前者のミスをすると思わぬハマリに繋がる。

ちなみに item をスペルミスした状態でビルドされたアプリを実行すると item の定義そのものが無視されるようだ。しかし将来の ADT で厳密なチェックがサポートされる可能性もあるため、この動作に期待しないこと。

Android のサポートしている属性と値については以下が参考になる。

タブを画面の左右に置く

Android の TabWidget水平方向のレイアウトで実装されているため、画面の上下には置けるけれど左右の場合は都合が悪い。しかし端末の向き変更に対応した場合、縦の時は下にタブを置き横では右に配置したくなるだろう。そのような要求へ応えるためにも TabWidget で垂直方向のレイアウトをサポートする方法を調べておく。

既に実現されているかも?とググってみたら、まさにドンぴしゃな記事を見つけた。

ここに書かれている方法を試したところ見事に垂直レイアウトなタブを実現できた。ただし私の環境ではなぜか ViewGroup.LayoutParams.MATCH_PARENT がエラーになるので代わりに FILL_PARENT を使ってみた。前述の記事には Android の TabWidget 実装についても説明しているので原理が非常に分かりやすい。要約するとこのコントロールは水平前提で実装されているが派生クラスの工夫でそれを変更できるといった感じか。

サンプル プログラム

ここまで学んだ内容を使って、簡単なサンプルを作ってみる。仕様は以下のようになる。

  • タブをカスタム描画する
  • タブを上下左右に配置する
  • タブの内容は Activity にする

まず、タブの上下左右を選ぶ画面を作成する。

トップ画面

選ばれたレイアウトに応じて、個別の画面を表示する。Top/Bottom は一般的なレイアウト。

上下 ( 水平 ) タブ

Left/Right は左右に垂直なタブを表示するレイアウト。

左右 ( 垂直 ) タブ

工夫としてタブ画面は TabHostActivity というひとつのクラスで担当させ、レイアウトの分岐はリソース選択としている。タブ画面が複数あるアプリを作成する場合でもそれらが大きく異なることは考えにくい。そのため大抵はこのような実装になるのではなかろうか。

最後にサンプル プログラムのプロジェクトを公開しておく。

Android 2.1 update 1 (API Level 7) でビルドし、エミュレータと初代 Xperia (SO-01B) にて動作確認。

オマケとして今回のサンプルで使用したアイコンの SVG ファイルもつけておいた。もしサンプルのアイコンが小さい・大きいと思ったらこれらを Inkscape などで開き、好みのサイズで PNG ファイルとしてエクスポートしてから利用すること。