何気なく使っているFrameworks 第10回 NavigationBar / ナビゲーションバー

NavigationBar

ICSから追加された、画面下にある仮想キーのことです。
Developerサイトには「Virtual buttons in the System Bar」と記載されていますが、ソースコード中にNavigationBarと書かれています。

NavigationBarの実装方法

NavigationBarはSystemUI/StatusBarの一部として実装されています。
以下の説明を読んでいただくとご理解いただけると思いますが、ただのViewです。

\frameworks\base\packages\SystemUI\src\com\android\systemui\statusbar\phone
- PhoneStatusBar.java

        try {
            boolean showNav = mWindowManager.hasNavigationBar();
            if (showNav) {
                mNavigationBarView =
                    (NavigationBarView) View.inflate(context, R.layout.navigation_bar, null);

                mNavigationBarView.setDisabledFlags(mDisabled);
            }
        } catch (RemoteException ex) {
            // no window manager? good luck with that
        }

layoutファイルはこちら。
\frameworks\base\packages\SystemUI\res\layout
- navigation_bar.xml




ちなみにTabletの場合、こちらのソースです。

\frameworks\base\packages\SystemUI\src\com\android\systemui\statusbar\tablet
- TabletStatusBar.java

        try {
            // Sanity-check that someone hasn't set up the config wrong and asked for a navigation
            // bar on a tablet that has only the system bar
            if (mWindowManager.hasNavigationBar()) {
                throw new RuntimeException(
                        "Tablet device cannot show navigation bar and system bar");
            }
        } catch (RemoteException ex) {
        }

Tabletの場合、NavigationBarが有効になっているとRuntimeExceptionとなります。
Systembarとして組み込んでいるからNavigationBarは不要でしょうっということみたいです。


NavigationBarの有無

ICSだからといって、すべての端末でNavigationBarが表示されるわけではありません。
実際、GalaxyNexusは表示あり、Nexus Sでは表示なしとなっています。
では、その切り分けはどこで行っているかというと、
mWindowManager.hasNavigationBar()を見ていくとたどり着きます。
最終的には、PhoneWindowManager#setInitialDisplaySize()で判定を行っています。


        // Determine whether the status bar can hide based on the size
        // of the screen.  We assume sizes > 600dp are tablets where we
        // will use the system bar.
        int shortSizeDp = shortSize
                * DisplayMetrics.DENSITY_DEFAULT
                / DisplayMetrics.DENSITY_DEVICE;
        mStatusBarCanHide = shortSizeDp < 600;
        mStatusBarHeight = mContext.getResources().getDimensionPixelSize(
                mStatusBarCanHide
                ? com.android.internal.R.dimen.status_bar_height
                : com.android.internal.R.dimen.system_bar_height);

        mHasNavigationBar = mContext.getResources().getBoolean(
                com.android.internal.R.bool.config_showNavigationBar);
        // Allow a system property to override this. Used by the emulator.
        // See also hasNavigationBar().
        String navBarOverride = SystemProperties.get("qemu.hw.mainkeys");
        if (! "".equals(navBarOverride)) {
            if      (navBarOverride.equals("1")) mHasNavigationBar = false;
            else if (navBarOverride.equals("0")) mHasNavigationBar = true;
        }


どうやら、com.android.internal.R.bool.config_showNavigationBarという値を参照するようです。
この値は以下のxmlに定義されています。

\frameworks\base\core\res\res\values
- config.xml

    <!-- Whether a software navigation bar should be shown. NOTE: in the future this may be
         autodetected from the Configuration. -->
    <bool name="config_showNavigationBar">false</bool>

ICS標準としてはNavigationBarは非表示のみたいですね。
では、各端末固有のデータをみてみましょう。

\device\samsung\tuna\overlay\frameworks\base\core\res\res\values
- config.xml

に上書き用の値が定義されていました。これによりGalaxyNexus上でNavigationBar
が表示可能となります。


NavigationBarの表示レイヤー

NavigationBarの表示レイヤーを確認します。
WindowManagerServiceへのaddViewの処理をみると、WindowManager.LayoutParams.TYPE_NAVIGATION_BARというパラメータが指定されています。

\frameworks\base\packages\SystemUI\src\com\android\systemui\statusbar\phone
- PhoneStatusBar.java

    private void addNavigationBar() {
        if (mNavigationBarView == null) return;

        prepareNavigationBarView();

        WindowManagerImpl.getDefault().addView(
                mNavigationBarView, getNavigationBarLayoutParams());
    }

    private WindowManager.LayoutParams getNavigationBarLayoutParams() {
        WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
                LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT,
                WindowManager.LayoutParams.TYPE_NAVIGATION_BAR,
                    0
                    | WindowManager.LayoutParams.FLAG_TOUCHABLE_WHEN_WAKING
                    | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                    | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
                    | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH
                    | WindowManager.LayoutParams.FLAG_SLIPPERY,
                PixelFormat.OPAQUE);
        // this will allow the navbar to run in an overlay on devices that support this
        if (ActivityManager.isHighEndGfx(mDisplay)) {
            lp.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
        }

        lp.setTitle("NavigationBar");
        lp.windowAnimations = 0;

        return lp;
    }

NAVIGATION_BAR_LAYERはシステムエラー通知(SYSTEM_ERROR_LAYER)のすぐ下と、上位に表示されるレイヤーとなっています。詳しくは以下の記事を参照。

ICSの表示レイヤーについて
http://d.hatena.ne.jp/baroqueworksdev/20111124/1322114804

NavigationBarのキーイベント

NavigationBarのHome/Backなどをタッチすると、ハードキーと同等の動作をします。
Android Frameworksとしてはキーイベントと同じ振る舞いとなるよう、
以下のようにJava層からNative層へイベントを送っていました

KeyButtonView#sendEvent
 - WindowManagerService#injectInputEventNoWait
   - InputManager#injectInputEvent
     - InputManager#nativeInjectInputEvent
       - android_server_InputManager_nativeInjectInputEvent
       - InputDispatcher#injectInputEvent

簡易クラス図

Java層だけではなく、InputDispatcher.cppまで確認することをお勧めします。