您好,登錄后才能下訂單哦!
今天小編給大家分享一下Android怎么實現SystemUI導航欄加載的相關知識點,內容詳細,邏輯清晰,相信大部分人都還太了解這方面的知識,所以分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后有所收獲,下面我們一起來了解一下吧。
需求
基于MTK8163 8.1平臺定制導航欄部分,在左邊增加音量減,右邊增加音量加
思路
需求開始做之前,一定要研讀SystemUI Navigation模塊的代碼流程!!!不要直接去網上copy別人改的需求代碼,盲改的話很容易出現問題,然而無從解決。網上有老平臺(8.0-)的講解System UI的導航欄模塊的博客,自行搜索。8.0對System UI還是做了不少細節上的改動,代碼改動體現上也比較多,但是總體基本流程并沒變。
源碼閱讀可以沿著一條線索去跟代碼,不要過分在乎代碼細節!例如我客制化這個需求,可以跟著導航欄的返回(back),桌面(home),最近任務(recent)中的一個功能跟代碼流程,大體知道比如recen這個view是哪個方法調哪個方法最終加載出來,加載的關鍵代碼在哪,點擊事件怎么生成,而不在意里面的具體邏輯判斷等等。
代碼流程
1.SystemUI\src\com\android\systemui\statusbar\phone\StatusBar.java;
從狀態欄入口開始看。
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)
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,發現根布局是自定義的view類NavigationBarFrame.(其實SystemUI以及其他系統應用如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類。發現類里并不是我們的預期,就是一個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導航欄這個布局加載的時候就會設置圖片資源,和長度改變,屏幕旋轉都有可能引起重新設置
以上就是“Android怎么實現SystemUI導航欄加載”這篇文章的所有內容,感謝各位的閱讀!相信大家閱讀完這篇文章都有很大的收獲,小編每天都會為大家更新不同的知識,如果還想學習更多的知識,請關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。