Fork me on GitHub

[Android] 关于系统工具栏和全屏沉浸模式

关于System Bars,之前写过几篇相关的文章:(链接等我把博客迁移好之后补上)

这三篇是按顺序写的,本来只是项目上的应用,其实并不需要深究的,查到方法并能用起来就好。随着应用程序的一些深入设计,大家总想要更好的界面和体验,所以有些东西并不能只是知道方法就结束了,是得要去深入研究研究的。通过这个过程我觉得,从应用层面来讲,想实现一个功能很简单,但若想实现的好,就要去了解设计者的设计思路以及提供的方法。而了解设计者想法最直接的途径就是查看文档。当然,了解文档以后还可以再进一步,看看 Android 的源码是怎么实现的,也就是从 Application 层进入到 Framework 层。熟悉 Framework 后就可以配合着 Linux 内核的知识了解 Android 底层的实现了。好了,先把注意放在应用层,毕竟这是最简单的。

System Bars 包括三条 bar:

  • status bar,也就是顶部的一条显示时间、电量、通知等信息的 bar
  • Navigation Bar,底部包含 back 键、home 键以及 recent 键的 bar
  • action bar,程序内顶部的可以添加诸如 search、menu 的 bar

  对 System Bar 的 操作也就是获取高度、状态以及设置显示/隐藏状态,前两者之前写过了,这次就把隐藏这些 bar 写详细点。


淡化系统工具栏

  淡化(dim—不知道这么译合适不)工具栏的效果就是 status bar 和 navigation bar 上的图标都变成一个淡灰色的圆点。这么做的意义就是可以让用户目光的焦点集中在程序要显示的内容上面,避免了屏幕上过多的东西分散用户注意力。

  可能这么说起来感觉这么做没有太大的意义,但实际上用户体验就是各方面一点点的细节积累起来的。有些时候用户在比较几款 APP 的时候都会有很明显的喜欢哪个不喜欢哪个,但让他具体列出来差距在哪里他却列不出来。这其中的原因大多就在这些小细节上,说不出但能感觉的到。而且既然有这个功能,它便有存在的意义,那么就来了解了解它怎么实现的。

  注意这个方法只在4.0版本及以上适用。使用时,应用内容显示的尺寸不会变化,只会把两条 bar 上的图标变淡,一旦触摸 bar 的区域,所有图标就会显现出来,不再消失。

  方法很简单,设置 system flag 为 SYSTEM_UI_FLAG_LOW_PROFILE 即可

getActivity().getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE);

  注意一旦触摸 bar 的位置,这个 flag 就会被清空,所以触摸结束后图标也不会淡化了。如果需要继续实现淡化效果,可以用 View.OnSystemUiVisibilityChangeListener 来监听状态变化再做处理。

  如果想通过代码在某些情况下主动清除当前 system ui flag ,可以用:

getActivity().getWindow().getDecorView().setSystemUiVisibility(0);

  看过我之前文章的可以知道,0 代表默认状态,也就是 bar 都正常显示的 flag,即:

public static final int SYSTEM_UI_FLAG_VISIBLE = 0;


隐藏 Status Bar

  其实淡化用的不是很多,而隐藏 Status Bar 倒是比较多。因为可以释放更多的显示空间,可以提供更好的用户体验。

  下面两张图可以看到隐藏 status bar 让程序更直观简洁,看起来更舒服。
显示ActionBar和Status Bar全屏
  注意,左边的图带有 action bar,如果你不显示 status bar 的时候也要把 action bar 隐藏掉,这是设计界面的建议。
设置方法:

4.0及以下版本:
: 1. 在 manifest 中设置:

<application
...
android:theme="@android:style/Theme.Holo.NoActionBar.Fullscreen" >
...
</application>

这样设置比较简单而且不易出错,因为系统在实例化 mainActivity 前已经拥有要渲染的界面,所以UI转换会比较平滑
: 2. 在代码中用 WindowManager flags 设置

public class MainActivity extends Activity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// If the Android version is lower than Jellybean, use this call to hide the status bar.
if (Build.VERSION.SDK_INT < 16) {
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
}
setContentView(R.layout.activity_main);
}
...
}

设置完 WindowManager flags 后这个 flag 会一直保留直到用代码清理掉他们。如果已经设定 FLAG_FULLSCREEN,就可以用 FLAG_LAYOUT_IN_SCREEN 设置 activity layout 使用当前可用的屏幕区域,这个 flag 可以防止显示/隐藏 status bar 时界面尺寸变化。

4.1及以上版本:
: 可以用前面提到的 setSystemUiVisibility() 在单独 view 层级上设置 UI 的标志,这些标志在窗口上生效。

View decorView = getWindow().getDecorView();
// Hide the status bar.
int uiOptions = View.SYSTEM_UI_FLAG_FULLSCREEN;
decorView.setSystemUiVisibility(uiOptions);
// Remember that you should never show the action bar if the
// status bar is hidden, so hide that too if necessary.
ActionBar actionBar = getActionBar();
actionBar.hide();

注意:

  1. 设置的 flag 一旦清空,应用程序需要重新设置 flag 才能隐藏 bar 。添加 listener 做处理即可
  2. 设置 flag 的代码写在不同的地方有不同的效果。比如你在 activity 的 onCreate() 方法里设置隐藏的标志,用户按下 Home 键, status bar 会再度显示,之后再打开应用程序,status bar 会保持显示的状态。如果需要其隐藏掉,需要在 onResume() 或者 onWindowFocusChanged() 方法里设置。
  3. setSystemUiVisibility() 方法只在可见的 view 中设置才有效,比如设置 View.gone 就没有效果
  4. 切换 view 会把当前 view 设置的 flag 清空

将程序内容显示在 Status Bar 的后面
之前的文章遇到过这个问题,还困扰了我半天,后来才发现程序是可以显示在 status bar 的后面的,这样的好处是程序的内容尺寸不会随着 status bar 的显示和隐藏而改变。
实现这个效果只用设置 SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN ,同时用 SYSTEM_UI_FLAG_LAYOUT_STABLE 来保证尺寸不变即可

When you use this approach, it becomes your responsibility to ensure that critical parts of your app’s UI (for example, the built-in controls in a Maps application) don’t end up getting covered by system bars. This could make your app unusable. In most cases you can handle this by adding the android:fitsSystemWindows attribute to your XML layout file, set to true. This adjusts the padding of the parent ViewGroup to leave space for the system windows. This is sufficient for most applications.

In some cases, however, you may need to modify the default padding to get the desired layout for your app. To directly manipulate how your content lays out relative to the system bars (which occupy a space known as the window’s “content insets”), override fitSystemWindows(Rect insets). The fitSystemWindows() method is called by the view hierarchy when the content insets for a window have changed, to allow the window to adjust its content accordingly. By overriding this method you can handle the insets (and hence your app’s layout) however you want.


隐藏 Navigation Bar

  作为设计上的建议,在隐藏掉导航栏的同时,也要把状态栏隐藏掉(当然状态栏隐藏了也要把动作栏也隐藏掉),当然隐藏掉还是保持随时可唤出的,这样可以利用整个屏幕空间,给用户更棒的体验。
  在4.0及以上版本使用 SYSTEM_UI_FLAG_HIDE_NAVIGATION 设置同时隐藏 status bar 和 navigation bar。  

View decorView = getWindow().getDecorView();
// Hide both the navigation bar and the status bar.
// SYSTEM_UI_FLAG_FULLSCREEN is only available on Android 4.1 and higher, but as a general rule, you should design your app to hide the status bar whenever you hide the navigation bar.
int uiOptions = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_FULLSCREEN;
decorView.setSystemUiVisibility(uiOptions);

注意事项参考隐藏 status bar 的注意。

当然,既然可以让程序内容显示在 status bar 的后面,那么相同效果也可以在 Navigation bar 上设置。使用 SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION 和 SYSTEM_UI_FLAG_LAYOUT_STABLE 即可。


使用全屏沉浸模式

  这是4.4版本新加的模式,设置标志为 SYSTEM_UI_FLAG_IMMERSIVE 和 SYSTEM_UI_FLAG_IMMERSIVE_STICKY两种。经常配合着 SYSTEM_UI_FLAG_HIDE_NAVIGATION 和 SYSTEM_UI_FLAG_FULLSCREEN 使用。

(补充:FLAG_IMMERSIVE 要和 FLAG_HIDE_NAVIGATION and FLAG_FULLSCREEN 两者其一一起使用才有效,与前者用为隐藏下方的 bar,与后者用为隐藏上方的 bar)

  这个模式的效果为隐藏掉上下两条 bar,同时你在 bar 的范围内点击事件也不会将其唤出,这为程序的操作提供了很大的便利。你会问,既然点击事件不会唤出 bar,那我要是想用 bar 上的功能怎么办?这个也很简单,手指放在 bar 的区域,如果是 status bar 的区域则手指向下滑动,反之则向上滑动,这样就可以把两条 bar 唤出了。这个操作实际上是把 SYSTEM_UI_FLAG_HIDE_NAVIGATION 和 SYSTEM_UI_FLAG_FULLSCREEN 清掉了,所以才会可见,同时会触发对应的 Listener 。

  前面说了有两种 IMMERSIVE 和 IMMERSIVE_STICKY ,前者是将 bar 唤出后不再消失,后者是将 bar 唤出后几秒就消失,后者不触发 Listener。

  还有一点,设置 FULLSCREEN 会让 status bar 显示的时候背景为半透明,正常状态下 status bar 的背景是黑色的。见下图:
  
example img

图1:正常状态。图2:第一次进入 immersive full-screen mode 时会有提示。

关于 IMMERSIVE 和 IMMERSIVE_STICKY 的选择:

  • 如果是做一个看书、杂志软件或者看新闻软件,建议使用 IMMERSIVE 标志,配合 SYSTEM_UI_FLAG_FULLSCREEN 和 SYSTEM_UI_FLAG_HIDE_NAVIGATION 。因为用户可能会频繁需要用到 UI 按钮,同时在浏览内容的时候不希望被打扰。
  • 如果希望用户体验沉浸模式,那就用 STICKY 标志
  • 如果像视频播放器那样用户交互就很少,就不要用 IMMERSIVE 了,之前写的内容就可以满足需求

  使用 IMMERSIVE 标志时,隐藏的 bar 会一直显示,那么就需要设置一些标志保持软件内容尺寸不变,如SYSTEM_UI_FLAG_HIDE_NAVIGATION 和 SYSTEM_UI_FLAG_LAYOUT_STABLE,同时也要注意 action bar 的隐藏。

// This snippet hides the system bars.
private void hideSystemUI() {
// Set the IMMERSIVE flag.
// Set the content to appear under the system bars so that the content
// doesn't resize when the system bars hide and show.
mDecorView.setSystemUiVisibility(
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION // hide nav bar
| View.SYSTEM_UI_FLAG_FULLSCREEN // hide status bar
| View.SYSTEM_UI_FLAG_IMMERSIVE);
}

// This snippet shows the system bars. It does this by removing all the flags
// except for the ones that make the content appear under the system bars.
private void showSystemUI() {
mDecorView.setSystemUiVisibility(
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
}

  下面是程序窗口收到焦点时设置 IMMERSIVE_STICKY 如下,其实结合前面提到的一些方法,自己组合可以实现很好的效果。

@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (hasFocus) {
decorView.setSystemUiVisibility(
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_FULLSCREEN
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);}
}


对系统工具栏显示变化的响应

  注册一个 View.OnSystemUiVisibilityChangeListener 来使界面同步变化,可以在 onCreate() 方法中添加以下代码:

View decorView = getWindow().getDecorView();
decorView.setOnSystemUiVisibilityChangeListener
(new View.OnSystemUiVisibilityChangeListener() {
@Override
public void onSystemUiVisibilityChange(int visibility) {
// Note that system bars will only be "visible" if none of the
// LOW_PROFILE, HIDE_NAVIGATION, or FULLSCREEN flags are set.
if ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) {
// TODO: The system bars are visible. Make any desired
// adjustments to your UI, such as showing the action bar or
// other navigational controls.
} else {
// TODO: The system bars are NOT visible. Make any desired
// adjustments to your UI, such as hiding the action bar or
// other navigational controls.
}
}
});

以上可能有理解上误差或者我测试中的没发现的错误,如果您看过后发现有哪些问题请留下反馈,谢谢。

Written with StackEdit.

坚持原创技术分享,您的支持将鼓励我继续创作!