Android 8.1 SystemUI虛擬導航鍵加載流程解析

需求

基於MTK 8.1平臺定製導航欄部分,在左邊增長音量減,右邊增長音量加java

思路

需求開始作以前,必定要研讀SystemUI Navigation模塊的代碼流程!!!不要直接去網上copy別人改的需求代碼,盲改的話很容易出現問題,然而無從解決。網上有老平臺(8.0-)的講解System UI的導航欄模塊的博客,自行搜索。8.0對System UI仍是作了很多細節上的改動,代碼改動體現上也比較多,可是整體基本流程並沒變。android

源碼閱讀能夠沿着一條線索去跟代碼,不要過度在意代碼細節!例如我客製化這個需求,能夠跟着導航欄的返回(back),桌面(home),最近任務(recent)中的一個功能跟代碼流程,大致知道好比recen這個view是哪一個方法調哪一個方法最終加載出來,加載的關鍵代碼在哪,點擊事件怎麼生成,而不在乎裏面的具體邏輯判斷等等。數組

代碼流程

1.SystemUI\src\com\android\systemui\statusbar\phone\StatusBar.java;app

從狀態欄入口開始看。ide

protected void makeStatusBarView() {
    final Context context = mContext;
    updateDisplaySize(); // populates mDisplayMetrics
    updateResources();
    updateTheme();

    ...
    ...

     try {
        boolean showNav = mWindowManagerService.hasNavigationBar();
        if (DEBUG) Log.v(TAG, "hasNavigationBar=" + showNav);
        if (showNav) {
            createNavigationBar();//建立導航欄
        }
    } catch (RemoteException ex) {

    }
}

2.進入 createNavigationBar 方法,發現主要是用 NavigationBarFragment 來管理.佈局

protected void createNavigationBar() {
    mNavigationBarView = NavigationBarFragment.create(mContext, (tag, fragment) -> {
        mNavigationBar = (NavigationBarFragment) fragment;
        if (mLightBarController != null) {
            mNavigationBar.setLightBarController(mLightBarController);
        }
        mNavigationBar.setCurrentSysuiVisibility(mSystemUiVisibility);
    });
}

3.看 NavigationBarFragment 的create方法,終於知道,是WindowManager去addView了導航欄的佈局,最終add了fragment的onCreateView加載的佈局。(其實SystemUI全部的模塊都是WindowManager來加載View)post

public static View create(Context context, FragmentListener listener) {
    WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
            LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT,
            WindowManager.LayoutParams.TYPE_NAVIGATION_BAR,
            WindowManager.LayoutParams.FLAG_TOUCHABLE_WHEN_WAKING
                    | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                    | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
                    | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
                    | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH
                    | WindowManager.LayoutParams.FLAG_SLIPPERY,
            PixelFormat.TRANSLUCENT);
    lp.token = new Binder();
    lp.setTitle("NavigationBar");
    lp.windowAnimations = 0;

    View navigationBarView = LayoutInflater.from(context).inflate(
            R.layout.navigation_bar_window, null);

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

    context.getSystemService(WindowManager.class).addView(navigationBarView, lp);
    FragmentHostManager fragmentHost = FragmentHostManager.get(navigationBarView);
    NavigationBarFragment fragment = new NavigationBarFragment();
    fragmentHost.getFragmentManager().beginTransaction()
            .replace(R.id.navigation_bar_frame, fragment, TAG) //注意!fragment裏onCreateView加載的佈局是add到這個Window屬性的view裏的。
            .commit();
    fragmentHost.addTagListener(TAG, listener);
    return navigationBarView;
    }
}

4.SystemUI\res\layout\navigation_bar_window.xmlui

來看WindowManager加載的這個view的佈局:navigation_bar_window.xml,發現根佈局是自定義的view類NavigationBarFrame.(其實SystemUI以及其餘系統應用如Launcher,都是這種自定義view的方式,好多邏輯處理也都是在自定義view裏,不能忽略)this

<com.android.systemui.statusbar.phone.NavigationBarFrame
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:systemui="http://schemas.android.com/apk/res-auto"
    android:id="@+id/navigation_bar_frame"
    android:layout_height="match_parent"
    android:layout_width="match_parent">    

</com.android.systemui.statusbar.phone.NavigationBarFrame>

5.SystemUI\src\com\android\systemui\statusbar\phone\NavigationBarFrame.javaspa

咱們進入NavigationBarFrame類。發現類裏並非咱們的預期,就是一個FrameLayout,對DeadZone功能下的touch事件作了手腳,無論了。

6.再回來看看NavigationBarFragment的生命週期呢。onCreateView()裏,導航欄的真正的rootView。

@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
        Bundle savedInstanceState) {
    return inflater.inflate(R.layout.navigation_bar, container, false);
}

進入導航欄的真正根佈局:navigation_bar.xml,好吧又是自定義view,NavigationBarView 和 NavigationBarInflaterView 都要仔細研讀。

<com.android.systemui.statusbar.phone.NavigationBarView
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:systemui="http://schemas.android.com/apk/res-auto"
    android:layout_height="match_parent"
    android:layout_width="match_parent"
    android:background="@drawable/system_bar_background">

    <com.android.systemui.statusbar.phone.NavigationBarInflaterView
        android:id="@+id/navigation_inflater"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</com.android.systemui.statusbar.phone.NavigationBarView>

7.SystemUI\src\com\android\systemui\statusbar\phone\NavigationBarInflaterView.java;繼承自FrameLayout

先看構造方法,由於加載xml佈局首先走的是初始化

public NavigationBarInflaterView(Context context, AttributeSet attrs) {
    super(context, attrs);
    createInflaters();//根據屏幕旋轉角度建立子view(單個back home or recent)的父佈局
    Display display = ((WindowManager)
            context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
    Mode displayMode = display.getMode();
    isRot0Landscape = displayMode.getPhysicalWidth() > displayMode.getPhysicalHeight();
}
private void inflateChildren() {
    removeAllViews();
    mRot0 = (FrameLayout) mLayoutInflater.inflate(R.layout.navigation_layout, this, false);
    mRot0.setId(R.id.rot0);
    addView(mRot0);
    mRot90 = (FrameLayout) mLayoutInflater.inflate(R.layout.navigation_layout_rot90, this, false);
    mRot90.setId(R.id.rot90);
    addView(mRot90);
    updateAlternativeOrder();
}

再看onFinishInflate()方法,這是view的生命週期,每一個view被inflate以後都會回調。

@Override
protected void onFinishInflate() {
    super.onFinishInflate();
    inflateChildren();//進去看可有可無 忽略
    clearViews();//進去看可有可無 忽略
    inflateLayout(getDefaultLayout());//關鍵方法:加載了 back.home.recent三個按鈕的layout
}

看inflateLayout():裏面的newLayout參數很重要!!!根據上一個方法看到getDefaultLayout(),他return了一個在xml寫死的字符串。再看inflateLayout方法,他解析分割了xml裏配置的字符串,並傳給了inflateButtons方法

protected void inflateLayout(String newLayout) {
    mCurrentLayout = newLayout;
    if (newLayout == null) {
        newLayout = getDefaultLayout();
    }
    String[] sets = newLayout.split(GRAVITY_SEPARATOR, 3);//根據「;」號分割成長度爲3的數組
    String[] start = sets[0].split(BUTTON_SEPARATOR);//根據「,」號分割,包含 left[.5W]和back[1WC]
    String[] center = sets[1].split(BUTTON_SEPARATOR);//包含home
    String[] end = sets[2].split(BUTTON_SEPARATOR);//包含recent[1WC]和right[.5W]
    // Inflate these in start to end order or accessibility traversal will be messed up.
    inflateButtons(start, mRot0.findViewById(R.id.ends_group), isRot0Landscape, true);
    inflateButtons(start, mRot90.findViewById(R.id.ends_group), !isRot0Landscape, true);

    inflateButtons(center, mRot0.findViewById(R.id.center_group), isRot0Landscape, false);
    inflateButtons(center, mRot90.findViewById(R.id.center_group), !isRot0Landscape, false);

    addGravitySpacer(mRot0.findViewById(R.id.ends_group));
    addGravitySpacer(mRot90.findViewById(R.id.ends_group));

    inflateButtons(end, mRot0.findViewById(R.id.ends_group), isRot0Landscape, false);
    inflateButtons(end, mRot90.findViewById(R.id.ends_group), !isRot0Landscape, false);
}

    protected String getDefaultLayout() {
    return mContext.getString(R.string.config_navBarLayout);
}

SystemUI\res\values\config.xml

<!-- Nav bar button default ordering/layout -->
<string name="config_navBarLayout" translatable="false">left[.5W],back[1WC];home;recent[1WC],right[.5W]</string>

再看inflateButtons()方法,遍歷加載inflateButton:

private void inflateButtons(String[] buttons, ViewGroup parent, boolean landscape,
        boolean start) {
    for (int i = 0; i < buttons.length; i++) {
        inflateButton(buttons[i], parent, landscape, start);
    }
}

@Nullable
protected View inflateButton(String buttonSpec, ViewGroup parent, boolean landscape,
        boolean start) {
    LayoutInflater inflater = landscape ? mLandscapeInflater : mLayoutInflater;
    View v = createView(buttonSpec, parent, inflater);//建立view
    if (v == null) return null;

    v = applySize(v, buttonSpec, landscape, start);
    parent.addView(v);//addView到父佈局
    addToDispatchers(v);
    View lastView = landscape ? mLastLandscape : mLastPortrait;
    View accessibilityView = v;
    if (v instanceof ReverseFrameLayout) {
        accessibilityView = ((ReverseFrameLayout) v).getChildAt(0);
    }
    if (lastView != null) {
        accessibilityView.setAccessibilityTraversalAfter(lastView.getId());
    }
    if (landscape) {
        mLastLandscape = accessibilityView;
    } else {
        mLastPortrait = accessibilityView;
    }
    return v;
}

咱們來看createView()方法:以home按鍵爲例,加載了home的button,實際上是加載了 R.layout.home 的layout佈局

private View createView(String buttonSpec, ViewGroup parent, LayoutInflater inflater) {
    View v = null;

    ...
    ...

    if (HOME.equals(button)) {
        v = inflater.inflate(R.layout.home, parent, false);
    } else if (BACK.equals(button)) {
        v = inflater.inflate(R.layout.back, parent, false);
    } else if (RECENT.equals(button)) {
        v = inflater.inflate(R.layout.recent_apps, parent, false);
    } else if (MENU_IME.equals(button)) {
        v = inflater.inflate(R.layout.menu_ime, parent, false);
    } else if (NAVSPACE.equals(button)) {
        v = inflater.inflate(R.layout.nav_key_space, parent, false);
    } else if (CLIPBOARD.equals(button)) {
        v = inflater.inflate(R.layout.clipboard, parent, false);
    } 

    ...
    ...

    return v;
}

//SystemUI\res\layout\home.xml 
//這裏佈局裏沒有src顯示home的icon,確定是在代碼裏設置了
//這裏也是自定義view:KeyButtonView
<com.android.systemui.statusbar.policy.KeyButtonView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:systemui="http://schemas.android.com/apk/res-auto"
android:id="@+id/home"
android:layout_width="@dimen/navigation_key_width"//引用了dimens.xml裏的navigation_key_width
android:layout_height="match_parent"
android:layout_weight="0"
systemui:keyCode="3"//systemui自定義的屬性
android:scaleType="fitCenter"
android:contentDescription="@string/accessibility_home"
android:paddingTop="@dimen/home_padding"
android:paddingBottom="@dimen/home_padding"
android:paddingStart="@dimen/navigation_key_padding"
android:paddingEnd="@dimen/navigation_key_padding"/>

8.SystemUI\src\com\android\systemui\statusbar\policy\KeyButtonView.java

先來看KeyButtonView的構造方法:咱們以前xml的systemui:keyCode=」3」方法在這裏獲取。再來看Touch事件,經過sendEvent()方法能夠看出,back等view的點擊touch事件不是本身處理的,而是交由系統以實體按鍵(keycode)的形式處理的.

固然KeyButtonView類還處理了支持長按的button,按鍵的響聲等,這裏忽略。

至此,導航欄按鍵事件咱們梳理完畢。

public KeyButtonView(Context context, AttributeSet attrs, int defStyle) {
    super(context, attrs);

    TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.KeyButtonView,
            defStyle, 0);

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

    mSupportsLongpress = a.getBoolean(R.styleable.KeyButtonView_keyRepeat, true);
    mPlaySounds = a.getBoolean(R.styleable.KeyButtonView_playSound, true);

    TypedValue value = new TypedValue();
    if (a.getValue(R.styleable.KeyButtonView_android_contentDescription, value)) {
        mContentDescriptionRes = value.resourceId;
    }

    a.recycle();

    setClickable(true);
    mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
    mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);

    mRipple = new KeyButtonRipple(context, this);
    setBackground(mRipple);
}

...
...

public boolean onTouchEvent(MotionEvent ev) {

   ...

    switch (action) {
        case MotionEvent.ACTION_DOWN:
            mDownTime = SystemClock.uptimeMillis();
            mLongClicked = false;
            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);
            }
            playSoundEffect(SoundEffectConstants.CLICK);
            removeCallbacks(mCheckLongPress);
            postDelayed(mCheckLongPress, ViewConfiguration.getLongPressTimeout());
            break;

        ...
        ...

    }

    return true;
}

void sendEvent(int action, int flags, long when) {
    mMetricsLogger.write(new LogMaker(MetricsEvent.ACTION_NAV_BUTTON_EVENT)
            .setType(MetricsEvent.TYPE_ACTION)
            .setSubtype(mCode)
            .addTaggedData(MetricsEvent.FIELD_NAV_ACTION, action)
            .addTaggedData(MetricsEvent.FIELD_FLAGS, flags));
    final int repeatCount = (flags & KeyEvent.FLAG_LONG_PRESS) != 0 ? 1 : 0;
    //這裏根據mCode new了一個KeyEvent事件,經過injectInputEvent使事件生效。
    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);
}

9.還遺留一個問題:設置圖片的icon到底在哪?咱們以前一直閱讀的是NavigationBarInflaterView,根據佈局咱們還有一個類沒有看,NavigationBarView.java

SystemUI\src\com\android\systemui\statusbar\phone\NavigationBarView.java

進入NavigationBarView類裏,找到構造方法。

public NavigationBarView(Context context, AttributeSet attrs) {
    super(context, attrs);

    mDisplay = ((WindowManager) context.getSystemService(
            Context.WINDOW_SERVICE)).getDefaultDisplay();


    ...
    ...

    updateIcons(context, Configuration.EMPTY, mConfiguration);//關鍵方法

    mBarTransitions = new NavigationBarTransitions(this);

    //mButtonDispatchers 是維護這些home back recent圖標view的管理類,會傳遞到他的child,NavigationBarInflaterView類中
    mButtonDispatchers.put(R.id.back, new ButtonDispatcher(R.id.back));
    mButtonDispatchers.put(R.id.home, new ButtonDispatcher(R.id.home));
    mButtonDispatchers.put(R.id.recent_apps, new ButtonDispatcher(R.id.recent_apps));
    mButtonDispatchers.put(R.id.menu, new ButtonDispatcher(R.id.menu));
    mButtonDispatchers.put(R.id.ime_switcher, new ButtonDispatcher(R.id.ime_switcher));
    mButtonDispatchers.put(R.id.accessibility_button,new ButtonDispatcher(R.id.accessibility_button));

}

 private void updateIcons(Context ctx, Configuration oldConfig, Configuration newConfig) {

       ...

        iconLight = mNavBarPlugin.getHomeImage(
                                    ctx.getDrawable(R.drawable.ic_sysbar_home));
        iconDark = mNavBarPlugin.getHomeImage(
                                    ctx.getDrawable(R.drawable.ic_sysbar_home_dark));
        //mHomeDefaultIcon = getDrawable(ctx,
        //        R.drawable.ic_sysbar_home, R.drawable.ic_sysbar_home_dark);
        mHomeDefaultIcon = getDrawable(iconLight,iconDark);

        //亮色的icon資源
        iconLight = mNavBarPlugin.getRecentImage(
                                    ctx.getDrawable(R.drawable.ic_sysbar_recent));
        //暗色的icon資源
        iconDark = mNavBarPlugin.getRecentImage(
                                    ctx.getDrawable(R.drawable.ic_sysbar_recent_dark));
        //mRecentIcon = getDrawable(ctx,
        //        R.drawable.ic_sysbar_recent, R.drawable.ic_sysbar_recent_dark);
        mRecentIcon = getDrawable(iconLight,iconDark);


        mMenuIcon = getDrawable(ctx, R.drawable.ic_sysbar_menu,
                                    R.drawable.ic_sysbar_menu_dark);

       ...
       ...

}

10.從第10能夠看到,以recent爲例,在初始化時獲得了mRecentIcon的資源,再看誰調用了了mRecentIcon就可知道,即反推看調用流程。

private void updateRecentsIcon() {
    getRecentsButton().setImageDrawable(mDockedStackExists ? mDockedIcon : mRecentIcon);
    mBarTransitions.reapplyDarkIntensity();
}

updateRecentsIcon這個方法設置了recent圖片的資源,再看誰調用了updateRecentsIcon方法:onConfigurationChanged屏幕旋轉會從新設置資源圖片

@Override
protected void onConfigurationChanged(Configuration newConfig) {
    super.onConfigurationChanged(newConfig);
    boolean uiCarModeChanged = updateCarMode(newConfig);
    updateTaskSwitchHelper();
    updateIcons(getContext(), mConfiguration, newConfig);
    updateRecentsIcon();
    if (uiCarModeChanged || mConfiguration.densityDpi != newConfig.densityDpi
            || mConfiguration.getLayoutDirection() != newConfig.getLayoutDirection()) {
        // If car mode or density changes, we need to reset the icons.
        setNavigationIconHints(mNavigationIconHints, true);
    }
    mConfiguration.updateFrom(newConfig);
}

public void setNavigationIconHints(int hints, boolean force) {

    ...
    ...

    mNavigationIconHints = hints;

    // We have to replace or restore the back and home button icons when exiting or entering
    // carmode, respectively. Recents are not available in CarMode in nav bar so change
    // to recent icon is not required.
    KeyButtonDrawable backIcon = (backAlt)
            ? getBackIconWithAlt(mUseCarModeUi, mVertical)
            : getBackIcon(mUseCarModeUi, mVertical);

    getBackButton().setImageDrawable(backIcon);

    updateRecentsIcon();

    ...
    ...

}

reorient()也調用了setNavigationIconHints()方法:

public void reorient() {
    updateCurrentView();

    ...

    setNavigationIconHints(mNavigationIconHints, true);

    getHomeButton().setVertical(mVertical);
}

再朝上推,最終追溯到NavigationBarFragment的onConfigurationChanged()方法 和 NavigationBarView的onAttachedToWindow()和onSizeChanged()方法。也就是說,在NavigationBarView導航欄這個佈局加載的時候就會設置圖片資源,和長度改變,屏幕旋轉都有可能引發從新設置

至此,SystemUI的虛擬導航欄模塊代碼流程結束。

總結

  1. 建立一個window屬性的父view
  2. 經過讀取解析xml裏config的配置,addView須要的icon,或者調換順序
  3. src圖片資源經過代碼設置亮色和暗色
  4. touch事件以keycode方式交由系統處理
相關文章
相關標籤/搜索