1. 程式人生 > >一步步淺析Android系統導航欄(NavigationBar)

一步步淺析Android系統導航欄(NavigationBar)

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的基本載入以及按鍵實現就分析完畢了,歡迎提問或拍磚,謝謝。