1.简介
- 导航栏就是屏幕下方或者两侧几个按键,以前是物理按键,现在都虚拟的。
- 常见的就是后退键,home键,recents键。
- 当然了,如果有输入法的显示的话,可能会有个箭头按钮用来隐藏输入法。
- 现在还支持手势操作,就是滑屏关闭页面,这时候导航栏可能就显示一个按钮线条。
- 手机模式用的是SystemUi下的NavigationBar,本篇学习的就是这个
- 如果是平板的话,那么导航按钮是在launcher3工程里,有个quickstep目录,里边有个taskbar目录下就是相关的导航功能了。
1.1.创建
>1.createNavigationBar
还是在CentralSurfacesImpl.java里边,通过Controller来创建的
protected void createNavigationBar(@Nullable RegisterStatusBarResult result) {
//参考2.1
mNavigationBarController.createNavigationBars(true /* includeDefaultDisplay */, result);
}
2.NavigationBarController
2.1.createNavigationBars
首先判断是否需要显示导航栏,需要的话再创建
public void createNavigationBars(final boolean includeDefaultDisplay,
RegisterStatusBarResult result) {
updateAccessibilityButtonModeIfNeeded();
// Don't need to create nav bar on the default display if we initialize TaskBar.
//taskbar和navitaionbar是二选一的,如果已经初始化了taskbar,这里就不创建navigationBar了
final boolean shouldCreateDefaultNavbar = includeDefaultDisplay
&& !initializeTaskbarIfNecessary();//参考补充1
Display[] displays = mDisplayManager.getDisplays();
for (Display display : displays) {
if (shouldCreateDefaultNavbar || display.getDisplayId() != DEFAULT_DISPLAY) {
//参考补充2
createNavigationBar(display, null /* savedState */, result);
}
}
}
>1.initializeTaskbarIfNecessary
- 是否有必要初始化taskbar,也就是launcher3里的,我们这里的是navigationbar
private boolean initializeTaskbarIfNecessary() {
//大屏的话用taskbar
boolean taskbarEnabled = mIsLargeScreen || mFeatureFlags.isEnabled(
Flags.HIDE_NAVBAR_WINDOW);
if (taskbarEnabled) {
final int displayId = mContext.getDisplayId();
// Hint to NavBarHelper if we are replacing an existing bar to skip extra work
mNavBarHelper.setTogglingNavbarTaskbar(mNavigationBars.contains(displayId));
//移除NavigationBar,如果存在的话
removeNavigationBar(displayId);
//初始化taskbar
mTaskbarDelegate.init(displayId);
mNavBarHelper.setTogglingNavbarTaskbar(false);
} else {
mTaskbarDelegate.destroy();
}
return taskbarEnabled;
}
>2.createNavigationBar
void createNavigationBar(Display display, Bundle savedState, RegisterStatusBarResult result) {
//...
final int displayId = display.getDisplayId();
final boolean isOnDefaultDisplay = displayId == DEFAULT_DISPLAY;
//默认设备,已经创建了taskbar的,就不要创建navigationBar了
if (isOnDefaultDisplay && initializeTaskbarIfNecessary()) {
return;
}
final IWindowManager wms = WindowManagerGlobal.getWindowManagerService();
try {
if (!wms.hasNavigationBar(displayId)) {
return;
}
} catch (RemoteException e) {
return;
}
//..
NavigationBarComponent component = mNavigationBarComponentFactory.create(
context, savedState);
//navBar是通过注解生成的
NavigationBar navBar = component.getNavigationBar();
navBar.init();
mNavigationBars.put(displayId, navBar);
navBar.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(View v) {
if (result != null) {
navBar.setImeWindowStatus(display.getDisplayId(), result.mImeToken,
result.mImeWindowVis, result.mImeBackDisposition,
result.mShowImeSwitcher);
}
}
@Override
public void onViewDetachedFromWindow(View v) {
v.removeOnAttachStateChangeListener(this);
}
});
}
2.2NavigationBar实例化
>1.构造方法
@Inject
NavigationBar(
NavigationBarView navigationBarView,
NavigationBarFrame navigationBarFrame,
@Nullable Bundle savedState,
//...
super(navigationBarView);
mFrame = navigationBarFrame;
注解生成的,如下,
>2.NavigationBarComponent
@Subcomponent(modules = { NavigationBarModule.class })
@NavigationBarComponent.NavigationBarScope
public interface NavigationBarComponent {
/** Factory for {@link NavigationBarComponent}. */
@Subcomponent.Factory
interface Factory {
NavigationBarComponent create(
@BindsInstance @DisplayId Context context,
@BindsInstance @Nullable Bundle savedState);
}
/** */
NavigationBar getNavigationBar();
>3.NavigationBarModule
这里有NavigationBar构造方法里用到的一些view
/** Module for {@link com.android.systemui.navigationbar.NavigationBarComponent}. */
@Module
public interface NavigationBarModule {
/** A Layout inflater specific to the display's context. */
@Provides
@NavigationBarScope
@DisplayId
static LayoutInflater provideLayoutInflater(@DisplayId Context context) {
return LayoutInflater.from(context);
}
/** */
@Provides
@NavigationBarScope
static NavigationBarFrame provideNavigationBarFrame(@DisplayId LayoutInflater layoutInflater) {
return (NavigationBarFrame) layoutInflater.inflate(R.layout.navigation_bar_window, null);
}
/** */
@Provides
@NavigationBarScope
static NavigationBarView provideNavigationBarview(
@DisplayId LayoutInflater layoutInflater, NavigationBarFrame frame) {
View barView = layoutInflater.inflate(R.layout.navigation_bar, frame);
return barView.findViewById(R.id.navigation_bar_view);
}
/** */
@Provides
@NavigationBarScope
static EdgeBackGestureHandler provideEdgeBackGestureHandler(
EdgeBackGestureHandler.Factory factory, @DisplayId Context context) {
return factory.create(context);
}
/** A WindowManager specific to the display's context. */
@Provides
@NavigationBarScope
@DisplayId
static WindowManager provideWindowManager(@DisplayId Context context) {
return context.getSystemService(WindowManager.class);
}
}
相关的两个布局如下
>4.navigation_bar_window.xml
<com.android.systemui.navigationbar.NavigationBarFrame
android:id="@+id/navigation_bar_frame"
android:theme="@style/Theme.SystemUI"
android:layout_height="match_parent"
android:layout_width="match_parent">
</com.android.systemui.navigationbar.NavigationBarFrame>
>5.navigation_bar.xml
<com.android.systemui.navigationbar.NavigationBarView
android:id="@+id/navigation_bar_view"
android:layout_height="match_parent"
android:layout_width="match_parent"
android:clipChildren="false"
android:clipToPadding="false"
android:background="@drawable/system_bar_background">
<com.android.systemui.navigationbar.NavigationBarInflaterView
android:id="@+id/navigation_inflater"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false"
android:clipToPadding="false" />
</com.android.systemui.navigationbar.NavigationBarView>
2.3.onInit()
- mView(NavigationBarView)是mFrame(NavigationBarFrame)的child,具体看上边Module里的加载过程以及相关的布局。
- 导航栏里边是加载了两种线性布局的,一种是横向的,一种是竖的,根据旋转角度显示不同的布局
@Override
public void onInit() {
mView.setBarTransitions(mNavigationBarTransitions);
mView.setTouchHandler(mTouchHandler);
setNavBarMode(mNavBarMode);//把mode传递给mView,mode是啥以及咋来的后边讲
mEdgeBackGestureHandler.setStateChangeCallback(mView::updateStates);
mNavigationBarTransitions.addListener(this::onBarTransition);
mView.updateRotationButton();
mView.setVisibility(
mStatusBarKeyguardViewManager.isNavBarVisible() ? View.VISIBLE : View.INVISIBLE);
//添加导航栏的控件mFrame(NavigationBarFrame)
mWindowManager.addView(mFrame,//根据旋转角度获取布局参数
getBarLayoutParams(mContext.getResources().getConfiguration().windowConfiguration
.getRotation()));//参考补充1
mDisplayId = mContext.getDisplayId();
mIsOnDefaultDisplay = mDisplayId == DEFAULT_DISPLAY;
// Ensure we try to get currentSysuiState from navBarHelper before command queue callbacks
// start firing, since the latter is source of truth
parseCurrentSysuiState();
mCommandQueue.addCallback(this);
mLongPressHomeEnabled = mNavBarHelper.getLongPressHomeEnabled();
mNavBarHelper.init();
mHomeButtonLongPressDurationMs = Optional.of(mDeviceConfigProxy.getLong(
DeviceConfig.NAMESPACE_SYSTEMUI,
HOME_BUTTON_LONG_PRESS_DURATION_MS,
/* defaultValue = */ 0
)).filter(duration -> duration != 0);
mDeviceConfigProxy.addOnPropertiesChangedListener(
DeviceConfig.NAMESPACE_SYSTEMUI, mHandler::post, mOnPropertiesChangedListener);
//...
// Respect the latest disabled-flags.
mCommandQueue.recomputeDisableFlags(mDisplayId, false);
mNotificationShadeDepthController.addListener(mDepthListener);
}
>1.getBarLayoutParams
private WindowManager.LayoutParams getBarLayoutParams(int rotation) {
//参考补充2
WindowManager.LayoutParams lp = getBarLayoutParamsForRotation(rotation);
//顺道保存了4种旋转角度对应的layoutParam
lp.paramsForRotation = new WindowManager.LayoutParams[4];
for (int rot = Surface.ROTATION_0; rot <= Surface.ROTATION_270; rot++) {
lp.paramsForRotation[rot] = getBarLayoutParamsForRotation(rot);
}
return lp;
}
>2.getBarLayoutParamsForRotation
- config_navBarCanMove,values-sw600dp下是false,values下是true,所以对于手机来说应该是true
- android 13里横屏的时候导航键是在左右显示的,不像以前在底部
private WindowManager.LayoutParams getBarLayoutParamsForRotation(int rotation) {
int width = WindowManager.LayoutParams.MATCH_PARENT;
int height = WindowManager.LayoutParams.MATCH_PARENT;
int insetsHeight = -1;
int gravity = Gravity.BOTTOM;
boolean navBarCanMove = true;
final Context userContext = mUserContextProvider.createCurrentUserContext(mContext);
if (mWindowManager != null && mWindowManager.getCurrentWindowMetrics() != null) {
Rect displaySize = mWindowManager.getCurrentWindowMetrics().getBounds();
//读取配置
navBarCanMove = displaySize.width() != displaySize.height()
&& userContext.getResources().getBoolean(
com.android.internal.R.bool.config_navBarCanMove);
}
if (!navBarCanMove) {
//读取配置里写好的高度,手机平板高度不一样
height = userContext.getResources().getDimensionPixelSize(
com.android.internal.R.dimen.navigation_bar_frame_height);
insetsHeight = userContext.getResources().getDimensionPixelSize(
com.android.internal.R.dimen.navigation_bar_height);
} else {
switch (rotation) {
case ROTATION_UNDEFINED:
case Surface.ROTATION_0:
case Surface.ROTATION_180://默认的竖屏,导航栏在底部,所以限制的是高度
height = userContext.getResources().getDimensionPixelSize(
com.android.internal.R.dimen.navigation_bar_frame_height);
insetsHeight = userContext.getResources().getDimensionPixelSize(
com.android.internal.R.dimen.navigation_bar_height);
break;
case Surface.ROTATION_90://90/270导航栏在左右两侧,所以限制的是宽度
gravity = Gravity.RIGHT;
width = userContext.getResources().getDimensionPixelSize(
com.android.internal.R.dimen.navigation_bar_width);
break;
case Surface.ROTATION_270:
gravity = Gravity.LEFT;
width = userContext.getResources().getDimensionPixelSize(
com.android.internal.R.dimen.navigation_bar_width);
break;
}
}
WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
width,
height,
WindowManager.LayoutParams.TYPE_NAVIGATION_BAR,
//...
PixelFormat.TRANSLUCENT);
//...
return lp;
}
2.4.init方法
父类ViewController里的
@NavigationBarScope
public class NavigationBar extends ViewController<NavigationBarView> implements Callbacks {
-
给view添加attach,detach监听.
-
根据子类构造方法里"super(navigationBarView);"可知,这里的view是navigationBarView
protected ViewController(T view) { mView = view; } public void init() { if (mInited) { return; } onInit();//子类2.3实现 mInited = true; if (isAttachedToWindow()) { mOnAttachStateListener.onViewAttachedToWindow(mView); } addOnAttachStateChangeListener(mOnAttachStateListener); }
3.NavigationBarView
public class NavigationBarView extends FrameLayout {
public void onFinishInflate() {
super.onFinishInflate();
mNavigationInflaterView = findViewById(R.id.navigation_inflater);
mNavigationInflaterView.setButtonDispatchers(mButtonDispatchers);
updateOrientationViews();
reloadNavIcons();
}
3.1.updateNavButtonIcons
public void updateNavButtonIcons() {
// 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.
final boolean useAltBack =
(mNavigationIconHints & StatusBarManager.NAVIGATION_HINT_BACK_ALT) != 0;
KeyButtonDrawable backIcon = mBackIcon;
orientBackButton(backIcon);
KeyButtonDrawable homeIcon = mHomeDefaultIcon;
if (!mUseCarModeUi) {
orientHomeButton(homeIcon);
}
//设置图标
getHomeButton().setImageDrawable(homeIcon);
getBackButton().setImageDrawable(backIcon);
updateRecentsIcon();
// Update IME button visibility, a11y and rotate button always overrides the appearance
boolean disableImeSwitcher =
(mNavigationIconHints & StatusBarManager.NAVIGATION_HINT_IME_SWITCHER_SHOWN) == 0
|| isImeRenderingNavButtons();
mContextualButtonGroup.setButtonVisibility(R.id.ime_switcher, !disableImeSwitcher);
mBarTransitions.reapplyDarkIntensity();
boolean disableHome = isGesturalMode(mNavBarMode)
|| ((mDisabledFlags & View.STATUS_BAR_DISABLE_HOME) != 0);
// Always disable recents when alternate car mode UI is active and for secondary displays.
boolean disableRecent = isRecentsButtonDisabled();
// Disable the home handle if both hone and recents are disabled
boolean disableHomeHandle = disableRecent
&& ((mDisabledFlags & View.STATUS_BAR_DISABLE_HOME) != 0);
boolean disableBack = !useAltBack && (mEdgeBackGestureHandler.isHandlingGestures()
|| ((mDisabledFlags & View.STATUS_BAR_DISABLE_BACK) != 0))
|| isImeRenderingNavButtons();
// When screen pinning, don't hide back and home when connected service or back and
// recents buttons when disconnected from launcher service in screen pinning mode,
// as they are used for exiting.
final boolean pinningActive = ActivityManagerWrapper.getInstance().isScreenPinningActive();
if (mOverviewProxyEnabled) {
// Force disable recents when not in legacy mode
disableRecent |= !QuickStepContract.isLegacyMode(mNavBarMode);
if (pinningActive && !QuickStepContract.isGesturalMode(mNavBarMode)) {
disableBack = disableHome = false;
}
} else if (pinningActive) {
disableBack = disableRecent = false;
}
ViewGroup navButtons = getCurrentView().findViewById(R.id.nav_buttons);
if (navButtons != null) {
LayoutTransition lt = navButtons.getLayoutTransition();
if (lt != null) {
if (!lt.getTransitionListeners().contains(mTransitionListener)) {
lt.addTransitionListener(mTransitionListener);
}
}
}
getBackButton().setVisibility(disableBack ? View.INVISIBLE : View.VISIBLE);
getHomeButton().setVisibility(disableHome ? View.INVISIBLE : View.VISIBLE);
getRecentsButton().setVisibility(disableRecent ? View.INVISIBLE : View.VISIBLE);
getHomeHandle().setVisibility(disableHomeHandle ? View.INVISIBLE : View.VISIBLE);
notifyActiveTouchRegions();
}
4.NavigationBarInflaterView
public class NavigationBarInflaterView extends FrameLayout
implements NavigationModeController.ModeChangedListener {
4.1.onFinishInflate
protected void onFinishInflate() {
super.onFinishInflate();
inflateChildren();//补充1
clearViews();
//配置数据参考4.2,布局加载参考4.3
inflateLayout(getDefaultLayout());
}
>1.inflateChildren
- 添加了 横屏和竖屏两种布局,根据实际情况显示
private void inflateChildren() {
removeAllViews();
mHorizontal = (FrameLayout) mLayoutInflater.inflate(R.layout.navigation_layout,
this /* root */, false /* attachToRoot */);
addView(mHorizontal);
mVertical = (FrameLayout) mLayoutInflater.inflate(R.layout.navigation_layout_vertical,
this /* root */, false /* attachToRoot */);
addView(mVertical);
updateAlternativeOrder();
}
>2.navigation_layout.xml
- 里边是2个horizontal的线性布局,显示在底部的导航栏
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="@dimen/rounded_corner_content_padding"
android:layout_marginEnd="@dimen/rounded_corner_content_padding"
android:paddingStart="@dimen/nav_content_padding"
android:paddingEnd="@dimen/nav_content_padding"
android:clipChildren="false"
android:clipToPadding="false"
android:id="@+id/horizontal">
<com.android.systemui.navigationbar.buttons.NearestTouchFrame
android:id="@+id/nav_buttons"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false"
android:clipToPadding="false"
systemui:isVertical="false">
<LinearLayout
android:id="@+id/ends_group"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
android:clipToPadding="false"
android:clipChildren="false" />
<LinearLayout
android:id="@+id/center_group"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center"
android:gravity="center"
android:orientation="horizontal"
android:clipToPadding="false"
android:clipChildren="false" />
</com.android.systemui.navigationbar.buttons.NearestTouchFrame>
</FrameLayout>
>3.navigation_layout_vertical.xml
- 里边是两个vertical的线性布局,显示在两侧的导航栏,横屏的时候
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="@dimen/rounded_corner_content_padding"
android:layout_marginBottom="@dimen/rounded_corner_content_padding"
android:paddingTop="@dimen/nav_content_padding"
android:paddingBottom="@dimen/nav_content_padding"
android:id="@+id/vertical">
<!!--帧布局-->
<com.android.systemui.navigationbar.buttons.NearestTouchFrame
android:id="@+id/nav_buttons"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false"
android:clipToPadding="false"
systemui:isVertical="true">
<com.android.systemui.navigationbar.buttons.ReverseLinearLayout
android:id="@+id/ends_group"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:clipToPadding="false"
android:clipChildren="false" />
<com.android.systemui.navigationbar.buttons.ReverseLinearLayout
android:id="@+id/center_group"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:clipToPadding="false"
android:clipChildren="false" />
</com.android.systemui.navigationbar.buttons.NearestTouchFrame>
</FrameLayout>
4.2.getDefaultLayout
-
导航栏都显示哪些按钮?读取配置string
protected String getDefaultLayout() { final int defaultResource = QuickStepContract.isGesturalMode(mNavBarMode) ? R.string.config_navBarLayoutHandle : mOverviewProxyService.shouldShowSwipeUpUI() ? R.string.config_navBarLayoutQuickstep : R.string.config_navBarLayout; return getContext().getString(defaultResource); }
>1.config_navBarLayout
分号隔开左中右,然后逗号隔开,w表示比重,A表示绝对值,C表示居中,具体的的逻辑看代码如何解析这些字符串
<!-- Nav bar button default ordering/layout -->
<string name="config_navBarLayout" translatable="false">left[.5W],back[1WC];home;recent[1WC],right[.5W]</string>
<string name="config_navBarLayoutQuickstep" translatable="false">back[1.7WC];home;contextual[1.7WC]</string>
<string name="config_navBarLayoutHandle" translatable="false">back[70AC];home_handle;ime_switcher[70AC]</string>
4.3.inflateLayout
-
解析配置里的字符串,加载相关的按钮。
-
start ,end相关的按钮记载在ends_group容器里,中间又额外加了个spacer(比重为1)
-
home 按钮是加载在center_group容器里的,这个容器是居中显示的。
protected void inflateLayout(String newLayout) { mCurrentLayout = newLayout; if (newLayout == null) { newLayout = getDefaultLayout(); } String[] sets = newLayout.split(GRAVITY_SEPARATOR, 3); if (sets.length != 3) { newLayout = getDefaultLayout(); sets = newLayout.split(GRAVITY_SEPARATOR, 3); } String[] start = sets[0].split(BUTTON_SEPARATOR); String[] center = sets[1].split(BUTTON_SEPARATOR); String[] end = sets[2].split(BUTTON_SEPARATOR); // Inflate these in start to end order or accessibility traversal will be messed up. inflateButtons(start, mHorizontal.findViewById(R.id.ends_group), false /* landscape */, true /* start */); inflateButtons(start, mVertical.findViewById(R.id.ends_group), true /* landscape */, true /* start */); inflateButtons(center, mHorizontal.findViewById(R.id.center_group), false /* landscape */, false /* start */); inflateButtons(center, mVertical.findViewById(R.id.center_group), true /* landscape */, false /* start */); addGravitySpacer(mHorizontal.findViewById(R.id.ends_group)); addGravitySpacer(mVertical.findViewById(R.id.ends_group)); inflateButtons(end, mHorizontal.findViewById(R.id.ends_group), false /* landscape */, false /* start */); inflateButtons(end, mVertical.findViewById(R.id.ends_group), true /* landscape */, false /* start */); updateButtonDispatchersCurrentView(); }
>1.inflateButton
protected View inflateButton(String buttonSpec, ViewGroup parent, boolean landscape,
boolean start) {
LayoutInflater inflater = landscape ? mLandscapeInflater : mLayoutInflater;
View v = createView(buttonSpec, parent, inflater);
if (v == null) return null;
//通过解析配置里的字符串,设置比重,宽度等尺寸
v = applySize(v, buttonSpec, landscape, start);
parent.addView(v);//添加到容器里
addToDispatchers(v);//添加到dispachers里
>2.createView
-
根据配置里的字段名字加载对应的按钮布局
-
left和right,最终是被替换成space 和 menu_ime了
View createView(String buttonSpec, ViewGroup parent, LayoutInflater inflater) { View v = null; String button = extractButton(buttonSpec); if (LEFT.equals(button)) { button = extractButton(NAVSPACE); } else if (RIGHT.equals(button)) { button = extractButton(MENU_IME_ROTATE); } 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_ROTATE.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); } else if (CONTEXTUAL.equals(button)) { v = inflater.inflate(R.layout.contextual, parent, false); } else if (HOME_HANDLE.equals(button)) { v = inflater.inflate(R.layout.home_handle, parent, false); } else if (IME_SWITCHER.equals(button)) { v = inflater.inflate(R.layout.ime_switcher, parent, false); } else if (button.startsWith(KEY)) { String uri = extractImage(button); int code = extractKeycode(button); v = inflater.inflate(R.layout.custom_key, parent, false); ((KeyButtonView) v).setCode(code); if (uri != null) { if (uri.contains(":")) { ((KeyButtonView) v).loadAsync(Icon.createWithContentUri(uri)); } else if (uri.contains("/")) { int index = uri.indexOf('/'); String pkg = uri.substring(0, index); int id = Integer.parseInt(uri.substring(index + 1)); ((KeyButtonView) v).loadAsync(Icon.createWithResource(pkg, id)); } } } return v; }
4.5.back,home,recents布局
>1.back.xml
<com.android.systemui.navigationbar.buttons.KeyButtonView
android:id="@+id/back"
android:layout_width="@dimen/navigation_key_width"
android:layout_height="match_parent"
android:layout_weight="0"
systemui:keyCode="4"
android:scaleType="center"
android:contentDescription="@string/accessibility_back"
android:paddingStart="@dimen/navigation_key_padding"
android:paddingEnd="@dimen/navigation_key_padding"
/>
>2.home.xml
<com.android.systemui.navigationbar.buttons.KeyButtonView
android:id="@+id/home"
android:layout_width="@dimen/navigation_key_width"
android:layout_height="match_parent"
android:layout_weight="0"
systemui:keyCode="3"
android:scaleType="center"
android:contentDescription="@string/accessibility_home"
android:paddingStart="@dimen/navigation_key_padding"
android:paddingEnd="@dimen/navigation_key_padding"
/>
>3.home_handle.xml
<com.android.systemui.navigationbar.gestural.NavigationHandle
android:id="@+id/home_handle"
android:layout_width="@dimen/navigation_home_handle_width"
android:layout_height="match_parent"
android:layout_weight="0"
android:contentDescription="@string/accessibility_home"
android:paddingStart="@dimen/navigation_key_padding"
android:paddingEnd="@dimen/navigation_key_padding"
/>
>4.recent_apps.xml
<com.android.systemui.navigationbar.buttons.KeyButtonView
android:id="@+id/recent_apps"
android:layout_width="@dimen/navigation_key_width"
android:layout_height="match_parent"
android:layout_weight="0"
android:scaleType="center"
android:contentDescription="@string/accessibility_recent"
android:paddingStart="@dimen/navigation_key_padding"
android:paddingEnd="@dimen/navigation_key_padding"
/>
4.6.KeyButtonView
public class KeyButtonView extends ImageView implements ButtonInterface {
//...
public KeyButtonView(Context context, AttributeSet attrs, int defStyle, InputManager manager,
UiEventLogger uiEventLogger) {
super(context, attrs);
//... 这里有个code的属性
mCode = a.getInteger(R.styleable.KeyButtonView_keyCode, KEYCODE_UNKNOWN);
//...
setClickable(true);
//...
}
>1.onTouchEvent
从上边的布局可以知道,back,home是有keyCode值的
public boolean onTouchEvent(MotionEvent ev) {
final int action = ev.getAction();
int x, y;
//...
switch (action) {
case MotionEvent.ACTION_DOWN:
mDownTime = SystemClock.uptimeMillis();
mLongClicked = false;
setPressed(true);
mTouchDownX = (int) ev.getX();
mTouchDownY = (int) ev.getY();
//有code的view会把keyEvent发送出去
if (mCode != KEYCODE_UNKNOWN) {
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:
//...我们只关心点击事件,所以这个就不看了
break;
case MotionEvent.ACTION_CANCEL:
setPressed(false);
//同样发送keyEvent
if (mCode != KEYCODE_UNKNOWN) {
sendEvent(KeyEvent.ACTION_UP, KeyEvent.FLAG_CANCELED);
}
removeCallbacks(mCheckLongPress);
break;
case MotionEvent.ACTION_UP:
final boolean doIt = isPressed() && !mLongClicked;
setPressed(false);
final boolean doHapticFeedback = (SystemClock.uptimeMillis() - mDownTime) > 150;
//...继续发送keyEvent
if (mCode != KEYCODE_UNKNOWN) {
if (doIt) {
sendEvent(KeyEvent.ACTION_UP, 0);
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
} else {
sendEvent(KeyEvent.ACTION_UP, KeyEvent.FLAG_CANCELED);
}
} else {
// 没有设置code的view,看下有没有设置clickListener,有的话对应处理
if (doIt && mOnClickListener != null) {
mOnClickListener.onClick(this);
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
}
}
removeCallbacks(mCheckLongPress);
break;
}
return true;
}
>2.sendEvent
private 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);
int displayId = INVALID_DISPLAY;
// Make KeyEvent work on multi-display environment
if (getDisplay() != null) {
displayId = getDisplay().getDisplayId();
}
if (displayId != INVALID_DISPLAY) {
ev.setDisplayId(displayId);
}
上边实例化一个keyEvent,这里发送出去,交给系统处理(PhoneWindowManager)
mInputManager.injectInputEvent(ev, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
}
4.7.按钮的点击事件
>1.PhoneWindowManager.java
back和home的点击事件交给系统处理了,具体见上边keyButtonView的touch事件,下边简单看下系统key的处理逻辑
public int interceptKeyBeforeQueueing(KeyEvent event, int policyFlags) {
final int keyCode = event.getKeyCode();
final boolean down = event.getAction() == KeyEvent.ACTION_DOWN;
//...
// Handle special keys.
switch (keyCode) {
case KeyEvent.KEYCODE_BACK: {
if (down) {
mBackKeyHandled = false;
} else {
if (!hasLongPressOnBackBehavior()) {
mBackKeyHandled |= backKeyPress();//处理back事件
}
// Don't pass back press to app if we've already handled it via long press
if (mBackKeyHandled) {
result &= ~ACTION_PASS_TO_USER;
}
}
break;
}
case KeyEvent.KEYCODE_VOLUME_DOWN:
case KeyEvent.KEYCODE_VOLUME_UP:
//如果你打算拦击某种keyCode,这里的result返回0即可
return result;
}
public long interceptKeyBeforeDispatching(IBinder focusedToken, KeyEvent event,
int policyFlags) {
final boolean keyguardOn = keyguardOn();
final int keyCode = event.getKeyCode();
//...
switch(keyCode) {
case KeyEvent.KEYCODE_HOME:
DisplayHomeButtonHandler handler = mDisplayHomeButtonHandlers.get(displayId);
if (handler == null) {
handler = new DisplayHomeButtonHandler(displayId);
mDisplayHomeButtonHandlers.put(displayId, handler);
}
return handler.handleHomeButton(focusedToken, event);
//...这个是recents
case KeyEvent.KEYCODE_APP_SWITCH:
if (!keyguardOn) {
if (down && repeatCount == 0) {
preloadRecentApps();
} else if (!down) {
toggleRecentApps();
}
}
return key_consumed;
toggleRecentApps
private void toggleRecentApps() {
mPreloadedRecentApps = false; // preloading no longer needs to be canceled
StatusBarManagerInternal statusbar = getStatusBarManagerInternal();
if (statusbar != null) {
statusbar.toggleRecentApps();
}
}
>2.adb shell
- keycode的值: back【4】,home【3】,recents【187】,可以利用adb命令模拟按键操作
adb shell input keyevent 4
>3.NavigationBar.java
recents的点击事件是下边添加的。
private void prepareNavigationBarView() {
mView.reorient();
ButtonDispatcher recentsButton = mView.getRecentsButton();
recentsButton.setOnClickListener(this::onRecentsClick); //recent这里有设置点击事件
recentsButton.setOnTouchListener(this::onRecentsTouch);
ButtonDispatcher homeButton = mView.getHomeButton();
homeButton.setOnTouchListener(this::onHomeTouch);
reconfigureHomeLongClick();
ButtonDispatcher accessibilityButton = mView.getAccessibilityButton();
accessibilityButton.setOnClickListener(this::onAccessibilityClick);
accessibilityButton.setOnLongClickListener(this::onAccessibilityLongClick);
updateAccessibilityStateFlags();
ButtonDispatcher imeSwitcherButton = mView.getImeSwitchButton();
imeSwitcherButton.setOnClickListener(this::onImeSwitcherClick);
updateScreenPinningGestures();
}
onRecentsClick
private void onRecentsClick(View v) {
if (LatencyTracker.isEnabled(mContext)) {
LatencyTracker.getInstance(mContext).onActionStart(
LatencyTracker.ACTION_TOGGLE_RECENTS);
}
mCentralSurfacesOptionalLazy.get().ifPresent(CentralSurfaces::awakenDreams);
mCommandQueue.toggleRecentApps();
}
>4.Recents.java
上边的recent事件最终走的都是CommandQueue.toggleRecentApps()方法,而这个方法里调用的又是一堆callback,具体的recent相关的类是Recents.java
public Recents(Context context, RecentsImplementation impl, CommandQueue commandQueue) {
mContext = context;
mImpl = impl;//交给它处理了
mCommandQueue = commandQueue;
}
@Override
public void start() {
mCommandQueue.addCallback(this);// 添加到CommandQueue的callbacks里了
mImpl.onStart(mContext);
}
public void preloadRecentApps() {
//...
mImpl.preloadRecentApps();
}
RecentsImplementation的实现类
public class OverviewProxyRecentsImpl implements RecentsImplementation {
//...
public void toggleRecentApps() {
// If connected to launcher service, let it handle the toggle logic
IOverviewProxy overviewProxy = mOverviewProxyService.getProxy();
if (overviewProxy != null) {
final Runnable toggleRecents = () -> {
try {
if (mOverviewProxyService.getProxy() != null) {
//其实是这行代码调用才会显示recents内容的
mOverviewProxyService.getProxy().onOverviewToggle();
//我开始以为交给它处理了,后来发现不是这个。这个是平板模式taskbar用到的
mOverviewProxyService.notifyToggleRecentApps();
}
};
继续看OverviewProxyService.java,构造方法或者user变化的时候会调用下边的方法绑定服务
private static final String ACTION_QUICKSTEP = "android.intent.action.QUICKSTEP_SERVICE";
private void internalConnectToCurrentUser() {
//...
Intent launcherServiceIntent = new Intent(ACTION_QUICKSTEP)
.setPackage(mRecentsComponentName.getPackageName());
try {
mBound = mContext.bindServiceAsUser(launcherServiceIntent,
mOverviewServiceConnection,
Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE_WHILE_AWAKE,
UserHandle.of(mUserTracker.getUserId()));
} catch (SecurityException e) {
//...
}
看下要bind的服务是啥
com.android.systemui/.recents.RecentsActivity -->
<string name="config_recentsComponentName" translatable="false"
>com.android.launcher3/com.android.quickstep.RecentsActivity</string>
我们需要的mOverviewProxy如下
mOverviewProxy = IOverviewProxy.Stub.asInterface(service);
这个服务在模拟器里是系统自带的桌面,具体的逻辑可以看后边launcher3相关的文章。
5.NavigationModeController.java
5.1.addListener
public int addListener(ModeChangedListener listener) {
mListeners.add(listener);
return getCurrentInteractionMode(mCurrentUserContext);
}
>1.getCurrentInteractionMode
private int getCurrentInteractionMode(Context context) {
int mode = context.getResources().getInteger(
com.android.internal.R.integer.config_navBarInteractionMode);
return mode;
}
>2.mode
-
如下三种,有3个按钮,2个按钮,以及1个按钮
<!-- Controls the navigation bar interaction mode: 0: 3 button mode (back, home, overview buttons) 1: 2 button mode (back, home buttons + swipe up for overview) 2: gestures only for back, home and overview --> <integer name="config_navBarInteractionMode">0</integer>
6.总结
这里简单整理了下导航栏的创建过程。
- 是由NavigationBarController实例化NavigationBar
- NavigationBar里又通过注解获取要用到的View,然后在onInit方法里通过WindowManager把View添加到窗口上
- 显示几个按钮是配置里决定的,具体看mode的说明
- 具体的按钮的添加逻辑都是在NavigationBarView,NavigationBarinflaterView里,至于按钮的显示大小啥的可以看getDefaultLayout里的配置string