溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

Android8.1平臺SystemUI導航欄加載的示例分析

發(fā)布時間:2021-06-28 10:12:52 來源:億速云 閱讀:120 作者:小新 欄目:移動開發(fā)

小編給大家分享一下Android8.1平臺SystemUI導航欄加載的示例分析,希望大家閱讀完這篇文章之后都有所收獲,下面讓我們一起去探討吧!

需求

基于MTK8163 8.1平臺定制導航欄部分,在左邊增加音量減,右邊增加音量加

思路

需求開始做之前,一定要研讀SystemUI Navigation模塊的代碼流程?。。〔灰苯尤ゾW(wǎng)上copy別人改的需求代碼,盲改的話很容易出現(xiàn)問題,然而無從解決。網(wǎng)上有老平臺(8.0-)的講解System UI的導航欄模塊的博客,自行搜索。8.0對System UI還是做了不少細節(jié)上的改動,代碼改動體現(xiàn)上也比較多,但是總體基本流程并沒變。

源碼閱讀可以沿著一條線索去跟代碼,不要過分在乎代碼細節(jié)!例如我客制化這個需求,可以跟著導航欄的返回(back),桌面(home),最近任務(wù)(recent)中的一個功能跟代碼流程,大體知道比如recen這個view是哪個方法調(diào)哪個方法最終加載出來,加載的關(guān)鍵代碼在哪,點擊事件怎么生成,而不在意里面的具體邏輯判斷等等。

代碼流程

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

從狀態(tài)欄入口開始看。

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();//創(chuàng)建導航欄
    }
  } catch (RemoteException ex) {
  }
}

2.進入 createNavigationBar 方法,發(fā)現(xiàn)主要是用 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)

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.xml;

來看WindowManager加載的這個view的布局:navigation_bar_window.xml,發(fā)現(xiàn)根布局是自定義的view類NavigationBarFrame.(其實SystemUI以及其他系統(tǒng)應(yīng)用如Launcher,都是這種自定義view的方式,好多邏輯處理也都是在自定義view里,不能忽略)

<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.java;

我們進入NavigationBarFrame類。發(fā)現(xiàn)類里并不是我們的預(yù)期,就是一個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

先看構(gòu)造方法,因為加載xml布局首先走的是初始化

public NavigationBarInflaterView(Context context, AttributeSet attrs) {
  super(context, attrs);
  createInflaters();//根據(jù)屏幕旋轉(zhuǎn)角度創(chuàng)建子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之后都會回調(diào)。

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

看inflateLayout():里面的newLayout參數(shù)很重要!?。「鶕?jù)上一個方法看到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);//根據(jù)“;”號分割成長度為3的數(shù)組
  String[] start = sets[0].split(BUTTON_SEPARATOR);//根據(jù)“,”號分割,包含 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);//創(chuàng)建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,肯定是在代碼里設(shè)置了
//這里也是自定義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的構(gòu)造方法:我們之前xml的systemui:keyCode=”3”方法在這里獲取。再來看Touch事件,通過sendEvent()方法可以看出,back等view的點擊touch事件不是自己處理的,而是交由系統(tǒng)以實體按鍵(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);//關(guān)鍵方法
      } 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;
  //這里根據(jù)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.還遺留一個問題:設(shè)置圖片的icon到底在哪?我們之前一直閱讀的是NavigationBarInflaterView,根據(jù)布局我們還有一個類沒有看,NavigationBarView.java

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

進入NavigationBarView類里,找到構(gòu)造方法。

public NavigationBarView(Context context, AttributeSet attrs) {
  super(context, attrs);
  mDisplay = ((WindowManager) context.getSystemService(
      Context.WINDOW_SERVICE)).getDefaultDisplay();
  ...
  ...
  updateIcons(context, Configuration.EMPTY, mConfiguration);//關(guān)鍵方法
  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的資源,再看誰調(diào)用了了mRecentIcon就可知道,即反推看調(diào)用流程。

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

updateRecentsIcon這個方法設(shè)置了recent圖片的資源,再看誰調(diào)用了updateRecentsIcon方法:onConfigurationChanged屏幕旋轉(zhuǎn)會重新設(shè)置資源圖片

@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()也調(diào)用了setNavigationIconHints()方法:

public void reorient() {
  updateCurrentView();
  ...
  setNavigationIconHints(mNavigationIconHints, true);
  getHomeButton().setVertical(mVertical);
}

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

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

看完了這篇文章,相信你對“Android8.1平臺SystemUI導航欄加載的示例分析”有了一定的了解,如果想了解更多相關(guān)知識,歡迎關(guān)注億速云行業(yè)資訊頻道,感謝各位的閱讀!

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI