【Android】ActionBarのカスタマイズ覚書

HoneyCombから追加されたActionBarですが、あまりカスタマイズ性が高くなく、アクロバティックな手法を使わないとカスタマイズ出来ない箇所がいくつかあります。

需要は高そうなんだけどなぜかデフォルトのメソッドやプロパティとして提供されてないカスタマイズ方法をいくつかメモしておきます。

後、なぜかSupport Libraryを使う時のサンプルがほっとんどネット上にないので、それに関してもちょびっと言及します。

ActionBarそのものを画面下に表示する

ActionBarに設置できるオプションメニューを画面下部に設定するのは結構簡単です。

Manifest.xmlにこんなものを追加するだけです。

<activity
    android:label="@string/app_name"
    android:name=".MainActivity"
    android:uiOptions="splitActionBarWhenNarrow">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>

AppCompat(Support Library v7)を使う場合はもう一手間加えます。

<activity
    android:label="@string/app_name"
    android:name=".MainActivity"
    android:uiOptions="splitActionBarWhenNarrow">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
    <meta-data android:name="android.support.UI_OPTIONS"
        android:value="splitActionBarWhenNarrow" />
</activity>

が、これだとオプションメニューしか下に行かない(タイトルやアイコンなんかはそのまま)し、解像度によっては普通に上にいます。まぁ「WhenNarrow」ですからね。

「何が何でもまるっと全部画面下に持っていきたいんじゃ〜い!」って人はこの手法を使うのがいいと思います。

上記の記事で取り上げられている「com.android.internal.widget.ActionBarContainer」は、まぁ、パッケージ名を見てわかる通りActionBarを表示するための内部クラスですし、(一応リソースIDは振られているみたいだけど)findViewByIdを使うことも出来ません。なので、リフレクションを絡めつつ文字列で判断する他ないと思います。

どうやって判断するのかも上記の記事にちゃんと載ってるのでそのまま使えばいいと思いますが、私はあそこまで汎用性が高いものが必要じゃなかったので、ActionBar専用のものをUtilクラスに突っ込んでしまいました。

public static void upsideDown(ActionBarActivity activity) {
    ViewGroup root = (ViewGroup) activity.getWindow().getDecorView();
    
    View firstChild = root.getChildAt(0);
    
    if (!(firstChild instanceof ViewGroup)) return;
    
    List<View> actionBarContainerList = new ArrayList<View>();
    findActionBarContainer(root, actionBarContainerList);
    
    if (actionBarContainerList.isEmpty()) return;
    
    for (View innerView : actionBarContainerList) {
        firstChild.removeView(innerView);
    }
    
    for (View innerView : actionBarContainerList) {
        firstChild.addView(innerView);
    }
}

private static void findActionBarContainer(View v, List<View> viewList) {
    
    String viewName = v.getClass().getName();
    
    // Support Libraryだと「android.support.v7.internal.widget.ActionBarContainer」っぽい
    // ちゃんと検証してないけど…
    if (viewName.equals("android.support.v7.internal.widget.ActionBarContainer")
        || viewName.equals("com.android.internal.widget.ActionBarContainer")) {
        viewList.add(v);
    }
    
    if (v instanceof ViewGroup) {
        ViewGroup g = (ViewGroup) v;
        
        for (int i = 0, count = g.getChildCount(); i < count; i++) {
            findActionBarContainer(g.getChildAt(i), viewList);
        }
    }
}

ただまぁ、これを使ってもActivity読み込み時には一瞬上にいます。これはもうどうしようもないです。「Fxxk Google.」とだけ呟いて諦めましょう。

参考:

[2014/06/01追記]

ちゃんとSupport Libraryでやってみたら落ちました。

原因はfirstChildにActionBarContainerがないのでremoveViewしても何もおこらない→addViewするとIllegalStateExceptionって感じです。

Support LibraryだとfirstChild(FrameLayout)の更に一個下のView(LinearLayout)にActionBarContainerがあるようなので、アドホックなコードを追加しておきます。

public static void upsideDown(ActionBarActivity activity) {
    ViewGroup root = (ViewGroup) activity.getWindow().getDecorView();
    
    View firstChild = root.getChildAt(0);
    
    if (!(firstChild instanceof ViewGroup)) return;
    
    //HONEYCOMB以前ならもうひとつ下のViewを取得する
    if(Build.VERSION.SDK_INT < 11) {
        firstChild = ((ViewGroup)firstChild).getChildAt(0);
    }
    
    List<View> actionBarContainerList = new ArrayList<View>();
    findActionBarContainer(root, actionBarContainerList);
    
    if (actionBarContainerList.isEmpty()) return;
    
    for (View innerView : actionBarContainerList) {
        firstChild.removeView(innerView);
    }
    
    for (View innerView : actionBarContainerList) {
        firstChild.addView(innerView);
    }
}

[2014/06/01追記ここまで]

ActionBarのアイコンを動的に変更する

普通にActionBar#setIconメソッドがあるのでそれで…ってだけだとわざわざ書きません。

このアイコン、android.R.id.homeってリソースIDが振られています。なので、findViewByIdで取ってくることが出来ます。

ImageView actionBarIcon = (ImageView) findViewById(android.R.id.home);

これを悪用活用すると、わざわざDrawableに変換することなくBitmapを設定したり、透過度を設定してみたり、(当たり前だけど)ImageViewに対して出来ることは何でも出来ます。サイズももしかしたら変えられるかもしれませんね。

[2014/06/01追記]

Support Libraryだと当然android.Rから取ってこれません。R.id.homeとすれば解決出来ます。

ImageView actionBarIcon = (ImageView) findViewById(R.id.home);

ただこれ、Support LibraryとSupport Library不要なバージョンでは互換性がありません。

Build.VERSION.SDK_INTで上手く振り分ける必要があります。面倒ですね。

[2014/06/01追記ここまで]

ActionBarのタイトルを修飾する

アイコンと違ってこいつのリソースIDは公開されてません。(振られてはいるんだろうけど、多分internalなID)

ただ、ActionBar#setTitleメソッドはCharSequenceを受けるオーバーロードがあるので、Spannableを使えばちょっとした修飾を加えることができます。

もっと根本的に、って言うか、TextView自体をいじりたい!ってなると、ActionBarのタイトルを非表示にし、代替となるCustom Viewを使用するしかなさそうです。

やり方は全部書いてあるのでいちいち説明しませんが、当然Custom ViewはただのTextViewでも問題ないです。XMLからinflateする必要もないです。(ただ、UI的に「ActionBarのタイトルっぽいデザインのTextView」を作ったほうがいい気はする)

さらにこの方法だとClickableSpanを仕込んだり、onClickのイベントを仕込んだりも出来ます。この辺はどうしてもTextViewの参照を手に入れないと出来ないことなので、ActionBarのタイトルにクリックイベントを設定したい場合は試してみてください。

参考:

オプションメニューの設定(Support Library v7使用時の注意)

XMLをリソースとして使用するか、自分で動的に組み立てるかになります。

XMLの方は今までのものが流用できるのでそんなに難しくないです。ただ、AppCompat(Support Library v7)の時はちょっとだけ注意が必要です。

と言うのも、Support LibraryのShowAsActionは一手間加えないと動いてくれないからです。

<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:title="メニュー1"
        android:id="@+id/menu1"
        android:showAsAction="always"
        android:icon="@android:drawable/menu_1" />
    <item android:title="メニュー2"
        android:id="@+id/menu2"
        android:showAsAction="always"
        android:icon="@android:drawable/menu_2" />
</menu>

Support Libraryを使用していない場合は上記のXMLでちゃんと動きます。メニュー1もメニュー2もalwaysを指定しているので常にアイコンが表示されます。

Support Libraryを使用する場合は、以下の名前空間を使用するようにしないとneverを指定した時と同じ動きになってしまいます。

<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:appcomp="http://schemas.android.com/apk/res-auto">
    <item android:title="メニュー1"
        android:id="@+id/menu1"
        appcomp:showAsAction="always"
        android:icon="@android:drawable/menu_1" />
    <item android:title="メニュー2"
        android:id="@+id/menu2"
        appcomp:showAsAction="always"
        android:icon="@android:drawable/menu_2" />
</menu>

後はまぁ…ここで改めて説明するようなことはないです。

参考:

[2014/06/02追記]

ちなみにActivity#onCreateOptionsMenu(Menu menu)などを使ってコードからオプションメニューを生成する場合、Support Libraryのプロパティを指定するためにMenuItemCompatと言うクラスが用意されています。

Menu#addなどから取得できるMenuItemをstaticなメソッドに放り投げて設定します。上記のxmlでの例をonCreateOptionsMenuでやると、こんな感じです。

@Override
public boolean onCreateOptionsMenu(Menu menu) {
    MenuItemCompat.setShowAsAction(menu.add(Menu.NONE, 0, 1, "メニュー1").setIcon(R.drawable.menu1)
        , MenuItemCompat.SHOW_AS_ACTION_ALWAYS);
    
    MenuItemCompat.setShowAsAction(menu.add(Menu.NONE, 1, 2, "メニュー2").setIcon(R.drawable.menu2)
        , MenuItemCompat.SHOW_AS_ACTION_ALWAYS);
        
    return true;
}

めちゃめちゃ可読性が悪いですね。拡張メソッドが欲しくなります。ないものねだりですが。

[2014/06/02追記ここまで]

まとめ

まだあんまりActionBarを使い倒してないので、とりあえず色々やってみたり調べてみてわかったことだけをまとめてみました。

後日何か「なんだこのクソな挙動…」みたいなことに気づいたら、別に記事をあげるかもしれません。