Android上的System Bar

Posted on Jun 4, 2017


本文紧接着上一篇文章Android SystemUI 介绍 。在本文中,我们来详细了解一下Android上的System Bar

前言

上文中我们已经看到,Android系统上的System Bar由SystemBars这个类(SystemUI的子类)负责初始化,它会通过读取R.string.config_statusBarComponent这个字符串来确定当前平台上的StatusBar实现类,然后通过反射API创建对应的实例并进行初始化。

对于System Bar,不同平台上的外观和功能可能是不一样的。因此从内部实现上也需要有所区分。SystemUI中有一个名称为BaseStatusBar的父类,各个平台上的实现类都是这个类的子类。三种平台上的实现类继承关系如下:

PhoneStatusBar的初始化

考虑到手机设备是大家最为熟悉的设备,并且大部分的开发者都是手机平台的,因此这里以PhoneStatusBar为例,来讲解System Bar的初始化逻辑。

PhoneStatusBar的start方法主要逻辑如下所示:

// PhoneStatusBar.java
@Override
public void start() {
   mDisplay = ((WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE))
           .getDefaultDisplay();
   updateDisplaySize();
   mScrimSrcModeEnabled = mContext.getResources().getBoolean(
           R.bool.config_status_bar_scrim_behind_use_src);

   super.start(); 

   mMediaSessionManager
           = (MediaSessionManager) mContext.getSystemService(Context.MEDIA_SESSION_SERVICE);

   addNavigationBar(); 

   ...
   startKeyguard(); 

   ...
}

这段代码三个关键步骤说明如下:

  1. 调用父类(BaseStatusBar)的start方法。BaseStatusBar.start方法中处理了很多与平台无关的初始化逻辑,这个逻辑可以在不同的平台上进行复用。这个方法的代码我们马上会看到
  2. 完成Navigation Bar的初始化。系统外部虽然将Status Bar和Navigation Bar统称为System Bar。但在系统的内部实现中,认为Navigation Bar是Status Bar的一部分
  3. 启动Keyguard,即锁屏界面。

addNavigationBar方法将在系统上添加Navigation Bar并为Navigation Bar上的三个按钮(Back,Home,Recents)设置事件监听器(例如:长按近期任务按钮将切换到分屏模式,对于这一点在讲解多窗口的时候已经提到过),相关代码如下:

protected 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();

   ButtonDispatcher recentsButton = mNavigationBarView.getRecentsButton();
   recentsButton.setOnClickListener(mRecentsClickListener);
   recentsButton.setOnTouchListener(mRecentsPreloadOnTouchListener);
   recentsButton.setLongClickable(true);
   recentsButton.setOnLongClickListener(mRecentsLongClickListener);

   ButtonDispatcher backButton = mNavigationBarView.getBackButton();
   backButton.setLongClickable(true);
   backButton.setOnLongClickListener(mLongPressBackListener);

   ButtonDispatcher homeButton = mNavigationBarView.getHomeButton();
   homeButton.setOnTouchListener(mHomeActionListener);
   homeButton.setOnLongClickListener(mLongPressHomeListener);

   mAssistManager.onConfigurationChanged();
}

BaseStatusBar.start负责了平台无关的Status Bar的基本初始化工作,这个方法会在不同的平台上复用。

该方法的代码较长,这里我们摘取其中最主要的逻辑:

public void start() {
   mWindowManager = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
   mWindowManagerService = WindowManagerGlobal.getWindowManagerService(); 
   ...

   mNotificationData = new NotificationData(this); 

   ...

   mBarService = IStatusBarService.Stub.asInterface(
           ServiceManager.getService(Context.STATUS_BAR_SERVICE)); 

   mRecents = getComponent(Recents.class); 

   ...
   
   ArrayList<String> iconSlots = new ArrayList<>(); 
   ArrayList<StatusBarIcon> icons = new ArrayList<>();
   Rect fullscreenStackBounds = new Rect();
   Rect dockedStackBounds = new Rect();
   try {
       mBarService.registerStatusBar(mCommandQueue, iconSlots, icons, switches, binders,
               fullscreenStackBounds, dockedStackBounds);
   } catch (RemoteException ex) {
       // If the system process isn't there we're doomed anyway.
   }

   createAndAddWindows(); 

   ...

   // Set up the initial icon state
   int N = iconSlots.size();
   int viewIndex = 0;
   for (int i=0; i < N; i++) { 
       setIcon(iconSlots.get(i), icons.get(i));
   }
   
   ...

这段代码说明如下:

  1. 获取WindowManagerService。这是负责系统中所有窗口管理的服务。Status Bar可以利用这个服务进行窗口的控制
  2. 创建NotificationData,这里面包含了当前显示的所有通知,后面我们会专门讲解Notification功能
  3. 获取StatusBarService,这是一个Binder服务,位于system_server中。这个服务提供了Notification Panel的管理功能
  4. 获取近期任务组件,前面我们已经说过,近期任务界面也是SystemUI的一部分
  5. 创建通知栏上Icon的Slot
  6. 通过createAndAddWindows方法创建具体窗口。很显然,这里的窗口会是平台相关的:不同平台上的System Bar窗口可能是不一样的。因此这个方法是一个抽象方法,具体的逻辑留待子类实现。而在手机平台上,实现类即为PhoneStatusBar
  7. 设置系统的初始Icon

这里的createAndAddWindows方法由子类实现,于是又调用到了PhoneStatusBar.createAndAddWindows方法。该方法代码如下:

@Override
public void createAndAddWindows() {
   addStatusBarWindow();
}

private void addStatusBarWindow() {
   makeStatusBarView();
   mStatusBarWindowManager = new StatusBarWindowManager(mContext);
   mRemoteInputController = new RemoteInputController(mStatusBarWindowManager,
           mHeadsUpManager);
   mStatusBarWindowManager.add(mStatusBarWindow, getStatusBarHeight());
}

在makeStatusBarView方法会真正完整视图的构建和初始化。这个方法代码较长,这里就不列出了。

完整的System Bar初始化流程如下所示:

开发者API

System Bar虽然是系统的一部分。但是为了让应用能够提供更好的用户体验,系统提供了接口来进行控制。开发者可以根据需要来显示或者隐藏Status Bar以及Navigation Bar(它们中的两者之一或者全部)。

三种模式

对于System Bar的控制,Android系统定义了三种场景模式:

  • Lights Out 模式 这是Android 4.4之前版本上的模式,这种模式的行为是:当用户几秒钟内都没有操作的情况下,Action Bar和Status Bar会淡化成不可用状态。但是Navigation Bar是正常可用的,虽然它也会被dim。如果你在4.4之后的版本上开发,请考虑下面两种模式。
  • Lean Back 模式 在这种模式下,System Bar默认是隐藏的,每当用户轻触屏幕时,它们会重新显示出来变成可用。因此,这种模式适合于用户无需频繁交互的应用,例如播放视频。
  • Immersive 模式 在这种模式下,只有当用户从屏幕边缘滑向屏幕中间时,System Bar才会显示出来。因此这种模式适用于需要频繁交互但用户不太需要System Bar的应用。例如:全屏游戏或者的画图软件。

API与使用场景

开发者通过View.setSystemUiVisibility(int) API来控制System Bar。控制的内容就是这个方法的参数,这个方法支持的参数是下面这几个常量(它们都在View类中)的组合:

  • SYSTEM_UI_FLAG_FULLSCREEN 使应用进入全屏模式,对于这个flag的说明详见下文
  • SYSTEM_UI_FLAG_HIDE_NAVIGATION 临时隐藏Navigation Bar
  • SYSTEM_UI_FLAG_IMMERSIVE 见下文“沉浸式全屏”
  • SYSTEM_UI_FLAG_IMMERSIVE_STICKY 见下文“沉浸式全屏”
  • SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN 视图希望它的Window像被设置了SYSTEM_UI_FLAG_FULLSCREEN一样进行布局,即便它并没有设置
  • SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION 视图希望它的Window像被设置了SYSTEM_UI_FLAG_HIDE_NAVIGATION一样进行布局,即便它并没有设置
  • SYSTEM_UI_FLAG_LAYOUT_STABLE 让应用维持一个稳定的布局
  • SYSTEM_UI_FLAG_LOW_PROFILE 临时性的Dim System Bar,当用户触摸屏幕时,这个Flag将被清除。如果需要,应用要重新设置。
  • SYSTEM_UI_FLAG_VISIBLE 让Status Bar变成可见

单纯的看这些说明可能并不好理解,下面我们举一些实例来说明它们的用法。

Dim System Bar

// 这个例子使用了decorView来进行控制, 但实际上你可以使用任何可见的View
View decorView = getActivity().getWindow().getDecorView();
int uiOptions = View.SYSTEM_UI_FLAG_LOW_PROFILE;
decorView.setSystemUiVisibility(uiOptions);

这段代码将Dim System Bar,一旦用户触摸了屏幕,Dim就会被取消,同时Dim Flag将被清除。如果需要,应用要重新设置。

隐藏Status Bar

对于隐藏Status Bar来说,在Android 4.0及之前的版本中的做法与Android 4.1及之后的版本中的做法是不一样的。这里我们只会讲解后面一种。

在Android 4.1及更高版本上,使用SYSTEM_UI_FLAG_FULLSCREEN Flag来隐藏Status Bar:

View decorView = getWindow().getDecorView();
int uiOptions = View.SYSTEM_UI_FLAG_FULLSCREEN;
decorView.setSystemUiVisibility(uiOptions);

另外:

  • 对于应用来说,在隐藏Status Bar同时也应当同时隐藏应用自身的ActionBar。做法见下文“相应System UI的改变事件”
  • 如果希望应用内容位于Status Bar的背后,而不是隐藏Status Bar,可以使用SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN。并且,开发者也可以同时使用SYSTEM_UI_FLAG_LAYOUT_STABLE来保持布局的稳定

隐藏Navigation Bar

只有在在Android 4.0及之上的版本上,才可以隐藏Navigation Bar。另外,通常情况下,当你的应用隐藏Navigation Bar时,你也应当同时隐藏Status Bar。下面是对应的代码示例:

View decorView = getWindow().getDecorView();
int uiOptions = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
              | View.SYSTEM_UI_FLAG_FULLSCREEN;
decorView.setSystemUiVisibility(uiOptions);

需要说明的是:

  • 当使用这种方式时,用户的触摸将会导致System Bar重新显示出来。同时设置的Flag会被清除,如果需要,你需要重新设置。
  • 在Android 4.1及更高版本上,你可以设置SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION让应用在Navigation Bar的背后显示。并且,开发者也可以同时使用SYSTEM_UI_FLAG_LAYOUT_STABLE来保持布局的稳定

沉浸式全屏

Android 4.4引用了一个新的Flag:SYSTEM_UI_FLAG_IMMERSIVE。使用这个Flag可以使你的应用获得真正的全屏。当这个Flag与SYSTEM_UI_FLAG_HIDE_NAVIGATION和SYSTEM_UI_FLAG_FULLSCREEN组合起来使用时,会隐藏整个System Bar使得你的应用获取整个屏幕的触摸事件。

由于你的应用接受了全部的触摸事件,只有当用户从屏幕边缘往内部滑动时,System Bar才会显示出来。这样会清除SYSTEM_UI_FLAG_HIDE_NAVIGATION(如果设置了SYSTEM_UI_FLAG_FULLSCREEN也会被清除)。如果你希望System Bar在这之后再次自动隐藏起来,你同时设置SYSTEM_UI_FLAG_IMMERSIVE_STICKY。

下图展示了四种不同的状态:

  1. 非沉浸模式:这是应用程序在进入沉浸式模式之前出现的状态。除此之外,如果你使用了SYSTEM_UI_FLAG_IMMERSIVE标志,并且当用户从屏幕边缘往内部滑动时,此时会清除SYSTEM_UI_FLAG_HIDE_NAVIGATION和SYSTEM_UI_FLAG_FULLSCREEN标志。清除这些标志后,System Bar将重新出现并保持可见,此时也会是这样。请注意,最好的做法是将所有UI控件与系统栏保持同步,以最大限度地减少屏幕的状态数量,从而提供更加无缝的用户体验。所以这里所有的UI控件都与状态栏一起显示。一旦应用程序进入沉浸式模式,UI控件将与系统栏一起隐藏。为了确保您的UI可视性与系统栏可见性保持同步,请通过View.OnSystemUiVisibilityChangeListener来响应UI改变事件,接下来我们马上就会讲到。
  2. 提醒气泡: 当你的应用程序中首次进入沉浸式模式时,系统会显示提醒气泡。提醒气泡提醒用户如何显示系统栏。
  3. 沉浸式模式:这是沉浸式模式下的应用程序,系统栏和其他UI控件被隐藏。您可以使用Flag:SYSTEM_UI_FLAG_IMMERSIVE或SYSTEM_UI_FLAG_IMMERSIVE_STICKY来实现此状态。
  4. Sticky flag: 如果您使用SYSTEM_UI_FLAG_IMMERSIVE_STICKY标志,并且用户从屏幕边缘往内部滑动时,半透明条会暂时出现,然后再隐藏。滑动的行为不清除任何标志,也不会触发您的系统UI可见性更改监听事件,因为System Bar的暂时性外观改变不被视为UI可见性更改。

注意:SYSTEM_UI_FLAG_IMMERSIVE_STICKY仅在与SYSTEM_UI_FLAG_HIDE_NAVIGATION,SYSTEM_UI_FLAG_FULLSCREEN两者之一或一起使用时才起作用。但是想要实现“完全浸入”模式时,通常是同时隐藏状态和导航栏。

响应System UI的改变事件

在System Bar隐藏或者显示之后,应用自身的UI也可能需要做一些更改。并且,保持这两者的状态同步是一个很好的做法。

如果应用想要关心System UI的变更事件,只需要设置一个View.OnSystemUiVisibilityChangeListener即可。例如,你可以在Activity的onCreate方法中完成这个事件的监听,下面是一段代码示例:

View decorView = getWindow().getDecorView();
decorView.setOnSystemUiVisibilityChangeListener
        (new View.OnSystemUiVisibilityChangeListener() {
    @Override
    public void onSystemUiVisibilityChange(int visibility) {
        // 只有在LOW_PROFILE,HIDE_NAVIGATION和FULLSCREEN都没有设置的时候,System Bar才是可见的
        if ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) {
            // 当System Bar可见的时候,请调整应用的UI,例如显示Action Bar或者其他导航相关的控件。
        } else {
            // 当System Bar不可见的时候,请调整应用的UI,例如隐藏Action Bar或者其他导航相关的控件。
        }
    }
});

在这个状态变更的时候,应用通常需要更改自身Action Bar或者其他导航相关控件的显示隐藏状态。


 Contents