Android手机可分为有导航栏以及没导航栏两种,一般有物理按键的机器不会带有导航栏,而没有物理按键的机器则基本会带,比如华为的手机基本都是带导航栏的。

导航栏是如何加载到桌面上?是如何实现与物理按键相同的功能的呢?带着种种疑问,我们来read the fucking source code。

导航栏是属于系统界面的一部分,也就是SystemUI的一部分。在SystemUI中导航栏实质上是一个继承LinearLayout的ViewGroup:NavigationBarView,在系统界面初始化的时候在PhoneStatusBar.java的makeStatusBarView方法中通过以下代码初始化中:

try {
            boolean showNav = mWindowManagerService.hasNavigationBar();
            if (DEBUG) Log.v(TAG, "hasNavigationBar=" + showNav);
            if (showNav) {
                /// M: add for multi window @{
                int layoutId = R.layout.navigation_bar;
                if(MultiWindowProxy.isSupported()) {
                    layoutId = R.layout.navigation_bar_float_window;
                }
                mNavigationBarView = (NavigationBarView) View.inflate(context,
                        /*R.layout.navigation_bar*/layoutId, null);
                /// @}

                mNavigationBarView.setDisabledFlags(mDisabled1);
                mNavigationBarView.setBar(this);
                mNavigationBarView.setOnVerticalChangedListener(
                        new NavigationBarView.OnVerticalChangedListener() {
                    @Override
                    public void onVerticalChanged(boolean isVertical) {
                        if (mAssistManager != null) {
                            mAssistManager.onConfigurationChanged();
                        }
                        mNotificationPanel.setQsScrimEnabled(!isVertical);
                    }
                });
                mNavigationBarView.setOnTouchListener(new View.OnTouchListener() {
                    @Override
                    public boolean onTouch(View v, MotionEvent event) {
                        checkUserAutohide(v, event);
                        return false;
                    }});
            }
        } catch (RemoteException ex) {
            // no window manager? good luck with that
        }

首先在第2行通过mWindowManagerService.hasNavigationBar(); 方法判断是否应该加载导航栏,如果应该加载,则在第10行调用View.inflate()方法将布局加载出来,并赋值给NavigationBarView的引用mNavigationBarView,然后对mNavigationBarView进行各种初始化操作。

NavigationBarView的布局文件比较长,我就只贴一部分:

......

            <View
                android:layout_width="@dimen/navigation_side_padding"
                android:layout_height="match_parent"
                android:layout_weight="0"
                android:visibility="invisible"
                />
            <com.android.systemui.statusbar.policy.KeyButtonView android:id="@+id/back"
                android:layout_width="@dimen/navigation_key_width"
                android:layout_height="match_parent"
                android:src="@drawable/ic_sysbar_back"
                systemui:keyCode="4"
                android:layout_weight="0"
                android:scaleType="center"
                android:contentDescription="@string/accessibility_back"
                />
            <View
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1"
                android:visibility="invisible"
                />
            <com.android.systemui.statusbar.policy.KeyButtonView android:id="@+id/home"
                android:layout_width="@dimen/navigation_key_width"
                android:layout_height="match_parent"
                android:src="@drawable/ic_sysbar_home"
                systemui:keyCode="3"
                systemui:keyRepeat="false"
                android:layout_weight="0"
                android:scaleType="center"
                android:contentDescription="@string/accessibility_home"
                />
            <View
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1"
                android:visibility="invisible"
                />
            <com.android.systemui.statusbar.policy.KeyButtonView android:id="@+id/recent_apps"
                android:layout_width="@dimen/navigation_key_width"
                android:layout_height="match_parent"
                android:src="@drawable/ic_sysbar_recent"
                android:layout_weight="0"
                android:scaleType="center"
                android:contentDescription="@string/accessibility_recent"
                />  

            ......

我们主要关注在第10、25和41行中id分别为back、home、和recent_apps的3个KeyButtonView。如大家所料,这3个View分别对应着返回键,home键以及最近活动键(不是MENU键)。至于其它View的主要作用是为了布局的整齐,让三个按键平分导航栏的空间。

接下来我们看下KeyButtonView是个什么东东:

public class KeyButtonView extends ImageView {

    ......

}

可见KeyButtonView是继承自ImageView的。接下来我们去了解下它是如何处理点击事件的:

public boolean onTouchEvent(MotionEvent ev) {
        final int action = ev.getAction();
        int x, y;
        if (action == MotionEvent.ACTION_DOWN) {
            mGestureAborted = false;
        }
        if (mGestureAborted) {
            return false;
        }

        switch (action) {
            case MotionEvent.ACTION_DOWN:
                mDownTime = SystemClock.uptimeMillis();
                setPressed(true);
                if (mCode != 0) {
                    sendEvent(KeyEvent.ACTION_DOWN, 0, mDownTime);
                } else {
                    // Provide the same haptic feedback that the system offers for virtual keys.
                    performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
                }
                removeCallbacks(mCheckLongPress);
                postDelayed(mCheckLongPress, ViewConfiguration.getLongPressTimeout());
                break;
            case MotionEvent.ACTION_MOVE:
                x = (int)ev.getX();
                y = (int)ev.getY();
                setPressed(x >= -mTouchSlop
                        && x < getWidth() + mTouchSlop
                        && y >= -mTouchSlop
                        && y < getHeight() + mTouchSlop);
                break;
            case MotionEvent.ACTION_CANCEL:
                setPressed(false);
                if (mCode != 0) {
                    sendEvent(KeyEvent.ACTION_UP, KeyEvent.FLAG_CANCELED);
                }
                removeCallbacks(mCheckLongPress);
                break;
            case MotionEvent.ACTION_UP:
                final boolean doIt = isPressed();
                setPressed(false);
                if (mCode != 0) {
                    if (doIt) {
                        sendEvent(KeyEvent.ACTION_UP, 0);
                        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
                        playSoundEffect(SoundEffectConstants.CLICK);
                    } else {
                        sendEvent(KeyEvent.ACTION_UP, KeyEvent.FLAG_CANCELED);
                    }
                } else {
                    // no key code, just a regular ImageView
                    if (doIt) {
                        performClick();
                    }
                }
                removeCallbacks(mCheckLongPress);
                break;
        }

        return true;
    }

在ACTION_DOWN、ACTION_CANCEL以及ACTION_UP中都调用了一个很重要的方法sendEvent():

public void sendEvent(int action, int flags) {
        sendEvent(action, flags, SystemClock.uptimeMillis());
    }

    void sendEvent(int action, int flags, long when) {
        final int repeatCount = (flags & KeyEvent.FLAG_LONG_PRESS) != 0 ? 1 : 0;
        final KeyEvent ev = new KeyEvent(mDownTime, when, action, mCode, repeatCount,
                0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
                flags | KeyEvent.FLAG_FROM_SYSTEM | KeyEvent.FLAG_VIRTUAL_HARD_KEY,
                InputDevice.SOURCE_KEYBOARD);
        InputManager.getInstance().injectInputEvent(ev,
                InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
    }

KeyButtonView就是在sendEvent方法中通过构建出一个对应的keyCode的KeyEvent ev,然后调用InputManager的injectInputEvent模拟发送来实现与物理按键相同的功能。

其中第7行中KeyEvent的构建参数中的mCode的定义语句是这样的:

mCode = a.getInteger(R.styleable.KeyButtonView_keyCode, 0);

在上面贴出来的导航栏的布局文件中id为home以及id为back的KeyButtonView就分别通过systemui:keyCode属性对其进行了设置,设置的值分别是3和4。然后查看KeyEvent的KeyCode值:

/** Key code constant: Home key.
     * This key is handled by the framework and is never delivered to applications. */
    public static final int KEYCODE_HOME            = 3;
    /** Key code constant: Back key. */
    public static final int KEYCODE_BACK            = 4;

可以看到,id为home的KeyButtonView的mCode正好就是对应着物理键Home的KeyCode,id为back的KeyButtonView的mCode正好就是对应着物理键Back的KeyCode。基于以上的所有操作,导航栏的back和home就与物理按键中的back和home对应了起来。

这时候有人会问:还有一个按键呢?id为recent_apps的KeyButtonView又是对应着哪个按键呢?通过查看布局文件,我们会发现recent_apps并没有定义systemui:keyCode这个值,也就是说mCode会是一个默认值:0。查看onTouchEvent可知,当mCode为0的时候,KeyButtonView并不会调用sendEvent方法。

也就是说点击id为recent_apps的KeyButtonView时的操作并不是通过模拟物理按键实现的,接下来我们将逐渐讲到这一点。

在NavigationBar加载完成后,SystemUI会调用addNavigationBar方法,在这个方法里先是调用prepareNavigationBarView方法中完成NavigationBarView的准备工作,比如给各个按键设置点击事件和点击效果。然后通过WindowManager的addView方法将NavigationBar加载到系统窗口中。

private void addNavigationBar() {
        if (DEBUG) Log.v(TAG, "addNavigationBar: about to add " + mNavigationBarView);
        if (mNavigationBarView == null) return;

        prepareNavigationBarView();

        mWindowManager.addView(mNavigationBarView, getNavigationBarLayoutParams());

    ......

    private void prepareNavigationBarView() {
        mNavigationBarView.reorient();

        mNavigationBarView.getRecentsButton().setOnClickListener(mRecentsClickListener);
        mNavigationBarView.getRecentsButton().setOnTouchListener(mRecentsPreloadOnTouchListener);
        mNavigationBarView.getRecentsButton().setLongClickable(true);
        mNavigationBarView.getRecentsButton().setOnLongClickListener(mLongPressBackRecentsListener);
        mNavigationBarView.getBackButton().setLongClickable(true);
        mNavigationBarView.getBackButton().setOnLongClickListener(mLongPressBackRecentsListener);
        mNavigationBarView.getHomeButton().setOnTouchListener(mHomeActionListener);
        mNavigationBarView.getHomeButton().setOnLongClickListener(mLongPressHomeListener);
        mAssistManager.onConfigurationChanged();
        /// M: add for multi window @{
        if(MultiWindowProxy.isSupported()){
            mNavigationBarView.getFloatButton().setOnClickListener(mFloatClickListener);
            if(mIsSplitModeEnable){
                mNavigationBarView.getFloatModeButton().setOnClickListener(mFloatModeClickListener);
                mNavigationBarView.getSplitModeButton().setOnClickListener(mSplitModeClickListener);
            }
            MultiWindowProxy.getInstance().setSystemUiCallback(new MWSystemUiCallback());
        }
        /// @}

    }

在第14行:prepareNavigationBarView方法中通过
mNavigationBarView.getRecentsButton().setOnClickListener(mRecentsClickListener);
给recent_apps这个KeyButtonView设置了监听:

private View.OnClickListener mRecentsClickListener = new View.OnClickListener() {
        public void onClick(View v) {
            awakenDreams();
            toggleRecentApps();
        }
    };

逻辑非常简单清晰:当点击的时候就打开RecentApp的Activity。

到此NavigationBar的基本加载以及按键实现就分析完毕了