Android沉浸式浏览图片

Android沉浸式浏览图片

一、目标

沉浸式浏览图片

二、体验地址

神马笔记最新版本:【神马笔记Version1.1.0_beta.apk

三、需求分析

图片浏览分为2个层次

  1. 浏览层

  2. 操作层

  • 浏览层

图片浏览层用于查看图片,用户通过双击,多点操作,移动查看图片。浏览层为全屏模式,图片的顶部和底部可能被操作层所遮挡。

  • 操作层

操作层提供了图片相关或其他操作,目前需要实现的是返回前一界面,查看图片列表。可以隐藏操作层从而提供用户沉浸式查看图片的体验。

四、准备工作

1. 实现全屏

有2种方式可以实现全屏。

  1. windowFullscreen
  2. SystemUI

windowFullscreen将Window设置为全屏,从而实现全屏模式。

Android提供了一种更好的方式来实现全屏,使用SystemUI。

对于图片查看,相关的SystemUI有2个,顶部的状态栏Status,底部的导航栏Navigation。SystemUI提供了使用Status及Navigation背后控件的功能,及将应用的控件区域延伸到系统控件之下。

SystemUI提供了3个Flag来实现这个功能。

  • SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN

    使控件区域延伸到Status之下。

  • SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION

    使控件区域延伸到Navigation之下。

  • SYSTEM_UI_FLAG_LAYOUT_STABLE

    使界面保持稳定,不会因切换SystemUI可见性或者Activity切换导致布局变化。

设置这3个Flag只是延伸了控件的空间,Status和Navigation依然是可见的。

2. 控制可见性

SystemUI提供了另外2个Flag用户控制Status和Navigation的可见性。

  • SYSTEM_UI_FLAG_FULLSCREEN

隐藏Status,默认为显示

  • SYSTEM_UI_FLAG_HIDE_NAVIGATION

隐藏Navigation,默认为显示

3. 设置样式风格

SystemUI提供了2个Flag用于设置Status和Navigation的风格。

  • SYSTEM_UI_FLAG_LIGHT_STATUS_BAR

将Status设置为明亮风格,通常显示为白底黑色按钮。默认为深色,黑底白色按钮。

  • SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR

将Navigation设置为明亮风格,通常显示为白底黑色按钮。默认为深色,黑底白色按钮。

注意,这2个Flag控制的是Status和Navigation按钮的样式,背景可以独立设置。

4. 设置背景颜色

在styles.xml资源中可以设置以下的样式风格。

1
2
3
4
5
6
7
<item name="android:windowDrawsSystemBarBackgrounds">true</item>

<item name="android:windowTranslucentStatus">false</item>
<item name="android:statusBarColor">@android:color/black</item>

<item name="android:windowTranslucentNavigation">false</item>
<item name="android:navigationBarColor">@android:color/black</item>
  • windowDrawsSystemBarBackgrounds

绘制Status及Navigation背景

  • windowTranslucentStatus

设置Status为半透明状态,默认为true

  • statusBarColor

设置Status的背景颜色

  • windowTranslucentNavigation

设置Navigation为半透明状态,默认为true

  • navigationBarColor

设置Navigation的背景颜色

5. 实现沉浸式全屏

SystemUI提供另外3个Flag来完成沉浸模式。

  • SYSTEM_UI_FLAG_IMMERSIVE

设置为沉浸模式,用户从底部或底部向屏幕内滑动,或者Window焦点发生变化时,会显示Status及Navigation,并且一直显示,直到重新被隐藏。显示Status及Navigation不会影响布局。

默认状态下,会进行重新布局

  • SYSTEM_UI_FLAG_IMMERSIVE_STICKY

设置为粘性沉浸模式,与SYSTEM_UI_FLAG_IMMERSIVE的区别在于,Status或Navigation在一段时间后会自动隐藏。

  • SYSTEM_UI_FLAG_LOW_PROFILE

降低Status的明亮度,弱化Status显示。具体机型实现有差异,视具体机型而定。

6. SystemUI FLAG

  1. 默认为可见
  • SYSTEM_UI_FLAG_VISIBLE
  1. 控制布局,能否延伸到SystemUI的空间
  • SYSTEM_UI_FLAG_LAYOUT_STABLE
  • SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
  • SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
  1. SystemUI的样式
  • SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
  • SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR
  1. 控制SystemUI可见性
  • SYSTEM_UI_FLAG_HIDE_NAVIGATION
  • SYSTEM_UI_FLAG_FULLSCREEN
  1. 实现沉浸式全屏
  • SYSTEM_UI_FLAG_IMMERSIVE
  • SYSTEM_UI_FLAG_IMMERSIVE_STICKY
  • SYSTEM_UI_FLAG_LOW_PROFILE

7. 处理SystemUI空间

当我们设置SYSTEM_UI_FLAG_LAYOUT_FULLSCREENSYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATIONSYSTEM_UI_FLAG_LAYOUT_STABLE将控件延伸到Status及Navigation的区域后,控件中的内容会被Status及Navigation遮挡,如同使用FrameLayout一样。

View提供了2个方法来处理SystemUI控件问题。

1
2
public WindowInsets dispatchApplyWindowInsets(WindowInsets insets); 
public WindowInsets onApplyWindowInsets(WindowInsets insets);

并且提供了默认处理方式,也可以在布局文件中设置fitSystemWindows属性。

1
2
3
public void setFitsSystemWindows(boolean fitSystemWindows) {
setFlags(fitSystemWindows ? FITS_SYSTEM_WINDOWS : 0, FITS_SYSTEM_WINDOWS);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private boolean fitSystemWindowsInt(Rect insets) {
if ((mViewFlags & FITS_SYSTEM_WINDOWS) == FITS_SYSTEM_WINDOWS) {
mUserPaddingStart = UNDEFINED_PADDING;
mUserPaddingEnd = UNDEFINED_PADDING;
Rect localInsets = sThreadLocal.get();
if (localInsets == null) {
localInsets = new Rect();
sThreadLocal.set(localInsets);
}
boolean res = computeFitSystemWindows(insets, localInsets);
mUserPaddingLeftInitial = localInsets.left;
mUserPaddingRightInitial = localInsets.right;
internalSetPadding(localInsets.left, localInsets.top,
localInsets.right, localInsets.bottom);
return res;
}
return false;
}

ViewGroup重载了其中一个方法。

一旦WindowInsets被标志为Consumed,将不再传递给下一个控件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
public WindowInsets dispatchApplyWindowInsets(WindowInsets insets) {
insets = super.dispatchApplyWindowInsets(insets);
if (!insets.isConsumed()) {
final int count = getChildCount();
for (int i = 0; i < count; i++) {
insets = getChildAt(i).dispatchApplyWindowInsets(insets);
if (insets.isConsumed()) {
break;
}
}
}
return insets;
}

五、组合起来

1. 设置初始状态

1
2
3
4
5
6
7
int flags = View.SYSTEM_UI_FLAG_VISIBLE;
flags |= View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
flags |= View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION;
flags |= View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
flags |= View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;

view.setSystemUiVisibility(flags);

2. 处理SystemUI控件

1
2
3
4
5
6
getView().setOnApplyWindowInsetsListener((v, insets) -> {
titleBar.setPadding(0, insets.getSystemWindowInsetTop(), 0, 0);
bottomBar.setPadding(0, 0, 0, insets.getSystemWindowInsetBottom());

return insets.consumeSystemWindowInsets();
});

3. 过渡效果

使用TransitionDrawable来切换全屏/非全屏下的背景。

1
2
3
4
5
6
7
8
9
<transition xmlns:android="http://schemas.android.com/apk/res/android">

<item android:drawable="@drawable/anc_shape_texture_paper"></item>

<item>
<color android:color="@android:color/black"/>
</item>

</transition>

默认为非全屏状态,显示为纸张纹理的背景。

全屏状态下,切换到黑色背景。

4. 切换SystemUI可见性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
void setActionVisible(boolean value) {

if (!(isActionVisible() ^ value)) {
return;
}

this.actionVisible = value;

{
if (value) {
titleBar.animate().translationY(0);
bottomBar.animate().translationY(0);

TransitionDrawable d = (TransitionDrawable) (getView().getBackground());
d.reverseTransition(getResources().getInteger(android.R.integer.config_shortAnimTime));

} else {
titleBar.animate().translationY(-titleBar.getHeight());
bottomBar.animate().translationY(bottomBar.getHeight());

TransitionDrawable d = (TransitionDrawable) (getView().getBackground());
d.startTransition(getResources().getInteger(android.R.integer.config_shortAnimTime));
}
}

{
View view = this.getView();
int flags = view.getSystemUiVisibility();

if (value) {
flags &= (~View.SYSTEM_UI_FLAG_FULLSCREEN);
flags &= (~View.SYSTEM_UI_FLAG_HIDE_NAVIGATION);
} else {
flags |= (View.SYSTEM_UI_FLAG_FULLSCREEN);
flags |= (View.SYSTEM_UI_FLAG_HIDE_NAVIGATION);
}

view.setSystemUiVisibility(flags);
}

}

六、Final

完成以上所有步骤之后,完美的图片沉浸式查看功能终于大功告成。

BUT,Android碎片化问题总能给我们带来问题。

Android 9.0开始支持刘海屏,但国产手机厂商如小米在Android 8.1已经开始出现刘海屏。

我手上的测试机红米6 Pro就是一台有着刘海屏的Android 8.1手机。

在红米6 Pro上切换全屏/非全屏时会出现图片跳跃的问题,除小米自带的图片应用外,快图浏览、相册管家都会出现跳跃的问题。

临时的处理办法,关闭MIUI的刘海屏功能。

1
2
3
<meta-data
android:name="notch.config"
android:value="none"/>