ActionBar を非表示にし、かつメニューも有効にしたい!

前提

2014/5/2 時点の最新の開発環境を使った場合のお話です。また、テストには自前の Android 端末(Android OS 4.1.2)を使っています。

将来的にはサポートライブラリの改修などで問題が解消される可能性があると思うのですが、現時点でどうしたら良いかという1つの解決策です。

ここで紹介する方法はメニューを有効にするという解決策ですが、メニューを無効にするという解決策もあります。

以下のページが参考になります。

問題

ActionBar が不要(非表示)なアプリケーションを開発するとしましょう。

非表示にするには標準の Theme.AppCompat.Light に android:windowNoTitle = true のスタイルを適用します。

以下、編集後の styles.xml です。

<resources>

    <style name="AppBaseTheme" parent="Theme.AppCompat.Light"></style>

    <style name="AppTheme" parent="AppBaseTheme">
        <item name="android:windowNoTitle">true</item>
    </style>

</resources>

これだけです。

しかし、このアプリケーションを起動しメニューボタンを押すといきなりアプリケーションが強制終了して驚くことになります。

ログを見ると原因と思われる以下の例外が出力されていると思います。

FATAL EXCEPTION: main
java.lang.NullPointerException
	at android.support.v7.app.ActionBarImplICS.getThemedContext(ActionBarImplICS.java:302)
	at android.support.v7.app.ActionBarImplJB.getThemedContext(ActionBarImplJB.java:20)
	at android.support.v7.app.ActionBarActivityDelegate.getActionBarThemedContext(ActionBarActivityDelegate.java:208)
	at android.support.v7.app.ActionBarActivityDelegate.getMenuInflater(ActionBarActivityDelegate.java:98)
	at android.support.v7.app.ActionBarActivity.getMenuInflater(ActionBarActivity.java:71)
	at com.example.exapp.MainActivity.getMenuInflater(MainActivity.java:14)
	at android.app.Activity.onCreatePanelMenu(Activity.java:2732)
	at android.support.v4.app.FragmentActivity.onCreatePanelMenu(FragmentActivity.java:224)
	at android.support.v7.app.ActionBarActivity.superOnCreatePanelMenu(ActionBarActivity.java:232)
	at android.support.v7.app.ActionBarActivityDelegateICS.onCreatePanelMenu(ActionBarActivityDelegateICS.java:146)
	at android.support.v7.app.ActionBarActivity.onCreatePanelMenu(ActionBarActivity.java:199)
	at android.support.v7.app.ActionBarActivityDelegateICS$WindowCallbackWrapper.onCreatePanelMenu(ActionBarActivityDelegateICS.java:293)
	at com.android.internal.policy.impl.PhoneWindow.preparePanel(PhoneWindow.java:546)
	at com.android.internal.policy.impl.PhoneWindow.onKeyDownPanel(PhoneWindow.java:934)
	at com.android.internal.policy.impl.PhoneWindow.onKeyDown(PhoneWindow.java:1620)
	at com.android.internal.policy.impl.PhoneWindow$DecorView.dispatchKeyEvent(PhoneWindow.java:2093)
	at android.view.ViewRootImpl.deliverKeyEventPostIme(ViewRootImpl.java:3609)
	at android.view.ViewRootImpl.handleImeFinishedEvent(ViewRootImpl.java:3579)
	at android.view.ViewRootImpl$ViewRootHandler.handleMessage(ViewRootImpl.java:2825)
	at android.os.Handler.dispatchMessage(Handler.java:99)
	at android.os.Looper.loop(Looper.java:137)
	at android.app.ActivityThread.main(ActivityThread.java:4909)
	at java.lang.reflect.Method.invokeNative(Native Method)
	at java.lang.reflect.Method.invoke(Method.java:511)
	at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:790)
	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:557)
	at dalvik.system.NativeStart.main(Native Method)

なぜ ActionBar を消しただけでメニューの表示時にエラーが発生してしまうのか...というところでビックリしました。

ActionBar とメニューには関係があり、ActionBar からメニューにアクセスできたりするので、 ActionBar を消してしまった関係上 null 参照がどこかで起きてしまっているようです。

それにしても ActionBar は要らないがメニューを表示したい場合はあるだろうと思うので、色々と調べて対処することにしました。

解決方法

ActionBar を非表示にし、かつメニューも有効にする解決方法が主に2つあります。

  1. android.support.v7.app.ActionBarActivity の使用を止める。
  2. アクティビティの getMenuInflater をオーバーライドする。

それぞれの方法の説明に入る前に ActionBarActivity を提供してくれているサポートライブラリについての理解を深める必要があります。

サポートライブラリというのはバージョンの互換性を高めてくれる Android 標準のライブラリです。

具体的に言うと ActionBar という機能は API level 11 で提供されている機能ですが、サポートライブラリを使用することで API level 7 から ActionBar の機能が使えるようになります(これすごいな!!)。

サポートライブラリには v4・v7・v8・v13 などがあり、それぞれの API level に対応しています。v4 と v7 には依存関係があり、v4 の機能をさらに拡張する形で v7 が作られています。

android.support.v7.app.ActionBarActivity は v7 のサポートライブラリで提供されている機能の1つです。

解決方法1:ActionBarActivity の使用を止める。

ActionBar をそもそも使わないのであれば、ActionBarActivity の使用を止めれば問題は解決します。

では代わりに何を使うのかというのにも以下の選択肢があります。

  1. android.app.Activity を使う。
  2. android.support.v4.app.FragmentActivity を使う。

android.app.Activity は最もシンプルなものです。

FragmentActivity は Fragment の利用をサポートしてくれるものです。Fragment という機能は API level 11 で提供されている機能ですが、サポートライブラリを使用することで API level 4 から使えるようになります(まじで...)。

その他、必要な機能の有無に応じて選択すると良いでしょう。

解決方法2:アクティビティの getMenuInflater をオーバーライドする。

android.support.v7.app.ActionBarActivity を使いたいし、ActionBar が非表示のときにメニューも表示したいという欲張りなあなたへの解決法です。

package com.example.myapp;

import android.app.Activity;
import android.os.Bundle;
import android.support.v7.app.ActionBarActivity;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;

public class MainActivity extends ActionBarActivity {

	@Override
	public MenuInflater getMenuInflater() {
		try {
			return super.getMenuInflater();
		} catch (NullPointerException npe) {
			try {
				return (MenuInflater) Activity.class.getMethod(
						"getMenuInflater").invoke(this);
			} catch (Exception e) {
				return new MenuInflater(this);
			}
		}
	};

	// 以下、省略 ...

例えば ActionBar を状況に応じて表示・非表示切り替える場合などでしょうか?

もしくは(何があるか知らないけども...)v7 の他の機能を使う必要性がある場合などでしょう。

解決方法2の解説

なんとか自分の手が届く範囲内で問題を解決できないかと考えた末の解決法です。

改めて例外のスタックトレースを見てみましょう。

FATAL EXCEPTION: main
java.lang.NullPointerException
	at android.support.v7.app.ActionBarImplICS.getThemedContext(ActionBarImplICS.java:302)
	at android.support.v7.app.ActionBarImplJB.getThemedContext(ActionBarImplJB.java:20)
	at android.support.v7.app.ActionBarActivityDelegate.getActionBarThemedContext(ActionBarActivityDelegate.java:208)
	at android.support.v7.app.ActionBarActivityDelegate.getMenuInflater(ActionBarActivityDelegate.java:98)
	at android.support.v7.app.ActionBarActivity.getMenuInflater(ActionBarActivity.java:71)
	at com.example.exapp.MainActivity.getMenuInflater(MainActivity.java:14)
	at android.app.Activity.onCreatePanelMenu(Activity.java:2732)
	at android.support.v4.app.FragmentActivity.onCreatePanelMenu(FragmentActivity.java:224)
	at android.support.v7.app.ActionBarActivity.superOnCreatePanelMenu(ActionBarActivity.java:232)
	at android.support.v7.app.ActionBarActivityDelegateICS.onCreatePanelMenu(ActionBarActivityDelegateICS.java:146)
	at android.support.v7.app.ActionBarActivity.onCreatePanelMenu(ActionBarActivity.java:199)
	at android.support.v7.app.ActionBarActivityDelegateICS$WindowCallbackWrapper.onCreatePanelMenu(ActionBarActivityDelegateICS.java:293)
	at com.android.internal.policy.impl.PhoneWindow.preparePanel(PhoneWindow.java:546)
	at com.android.internal.policy.impl.PhoneWindow.onKeyDownPanel(PhoneWindow.java:934)
	at com.android.internal.policy.impl.PhoneWindow.onKeyDown(PhoneWindow.java:1620)
	at com.android.internal.policy.impl.PhoneWindow$DecorView.dispatchKeyEvent(PhoneWindow.java:2093)
	at android.view.ViewRootImpl.deliverKeyEventPostIme(ViewRootImpl.java:3609)
	at android.view.ViewRootImpl.handleImeFinishedEvent(ViewRootImpl.java:3579)
	at android.view.ViewRootImpl$ViewRootHandler.handleMessage(ViewRootImpl.java:2825)
	at android.os.Handler.dispatchMessage(Handler.java:99)
	at android.os.Looper.loop(Looper.java:137)
	at android.app.ActivityThread.main(ActivityThread.java:4909)
	at java.lang.reflect.Method.invokeNative(Native Method)
	at java.lang.reflect.Method.invoke(Method.java:511)
	at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:790)
	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:557)
	at dalvik.system.NativeStart.main(Native Method)

NullPointerException が発生している場所は android.support.v7.app.ActionBarImplICS というクラスですが、このクラスに手を加えることはできません。

インスタンスを作成している箇所は内部の奥深くですし、 ICS と名前がついていることからわかるように、これは Android OS のバージョン毎に実装があると想像が付きます。 もし手を加える方法が見つかったとしても特定のバージョンのものに対する対処にしかなりませんし、全てのバージョンに対処するよう手を加えるのは困難です。

スタックトレースを上から順に見て行って手が届きやすく、修正の最も簡単な方法であるメソッドのオーバーライドで対処できそうな場所は getMenuInflater メソッドだけです。

解決方法1で既に分かっている通り android.app.Activity ではメニューの表示が正常にできるので、 android.support.v7.app.ActionBarActivity の getMenuInflater の代わりに android.app.Activity の getMenuInflater を呼び出しているのです。

getMenuInflater は android.support.v7.app.ActionBarActivity によってオーバーライドされてしまっているので、 元の android.app.Activity の getMenuInflater を呼び出すためにリフレクションを使っています。

理屈がわかれば他の書き方をしても大丈夫だと思います。

より詳しくは Android の内部のソースコードを読むと良いです。以下、Activity の最新のソースコードへの参照です。

他にも意見があればコメント頂ければと思います。

基本的には FragmentActivity を使う方法が良いのかなぁと現時点では思っています。