Android 80 只有全屏不透明活动可以请求方向问题

Android 8.0 只有全屏不透明活动可以请求方向问题

1 背景

  • Android 8.0,即 sdk26 时,Android 为了支持全面屏系统增加了一个限制,如果是透明的 Activity,则不能固定它的方向,因为它的方向其实是依赖其父 Activity 的(因为透明);
  • 因此产生了一个系统级别的 Bug,当以下四个条件同时满足时会发生的崩溃:
  • 1)使用的是 Android 8.0 操作系统的设备;
  • 2)targetSdkVersion 设置为 27 以上;
  • 3)将背景设置为透明主题;
  • 4)固定屏幕方向,screenOrientation 的值为 portrait 或者 landscape(代码或者清单文件);
  • 崩溃信息如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码Caused by: java.lang.IllegalStateException: Only fullscreen opaque activities can request orientation
at android.app.Activity.onCreate(Activity.java:1081)
at android.support.v4.app.SupportActivity.onCreate(SupportActivity.java:66)
at android.support.v4.app.FragmentActivity.onCreate(FragmentActivity.java:297)
at android.support.v7.app.AppCompatActivity.onCreate(AppCompatActivity.java:84)
at xxx.xxx.xxx.ui.XxxActivity.onCreate(XxxActivity.java:43)
at android.app.Activity.performCreate(Activity.java:7372)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1218)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3147)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3302)
at android.app.ActivityThread.-wrap12(Unknown Source:0)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1891)
at android.os.Handler.dispatchMessage(Handler.java:108)
at android.os.Looper.loop(Looper.java:166)

2 分析

  • 通过 Only fullscreen opaque activities can request orientation 报错信息可知,这是 Android 8.0 Activit 的源码所抛出的异常错误信息,源码如下所示:
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
42
java复制代码public class Activity {

protected void onCreate(@Nullable Bundle savedInstanceState) {
// ...
if (getApplicationInfo().targetSdkVersion >= O_MR1 && mActivityInfo.isFixedOrientation()) {
final TypedArray ta = obtainStyledAttributes(com.android.internal.R.styleable.Window);
final boolean isTranslucentOrFloating = ActivityInfo.isTranslucentOrFloating(ta);
ta.recycle();

if (isTranslucentOrFloating) {
throw new IllegalStateException(
"Only fullscreen opaque activities can request orientation");
}
}
// ...
}

}

public class ActivityInfo {

public boolean isFixedOrientation() {
return isFixedOrientationLandscape() || isFixedOrientationPortrait()
|| screenOrientation == SCREEN_ORIENTATION_LOCKED;
}

public static boolean isTranslucentOrFloating(TypedArray attributes) {
final boolean isTranslucent =
attributes.getBoolean(com.android.internal.R.styleable.Window_windowIsTranslucent,
false);
final boolean isSwipeToDismiss = !attributes.hasValue(
com.android.internal.R.styleable.Window_windowIsTranslucent)
&& attributes.getBoolean(
com.android.internal.R.styleable.Window_windowSwipeToDismiss, false);
final boolean isFloating =
attributes.getBoolean(com.android.internal.R.styleable.Window_windowIsFloating,
false);

return isFloating || isTranslucent || isSwipeToDismiss;
}

}
  • 通过阅读上述的源码可知,触发 IllegalStateException 异常的条件有:
  • 1)Activity 屏幕方向固定,windowIsTranslucent = true
  • 2)Activity 屏幕方向固定,windowIsTranslucent = falsewindowSwipeToDismiss = true
  • 3)Activity 屏幕方向固定,windowIsFloating = true

3 解决思路

  • 1)[不推荐] 暴力回退 sdk 版本,即 sdk <= 26
  • 2)[不推荐] 去除主题中的透明属性,需求允许的话:
1
xml复制代码<item name="android:windowIsTranslucent">false</item>
  • 3)[不推荐] 指定除 8.0 以外的系统固定屏幕方向,去掉清单文件中 screenOrientation 属性,activityonCreate 中执行屏幕方向固定的代码:
1
2
3
4
java复制代码if (android.os.Build.VERSION.SDK_INT != android.os.Build.VERSION_CODES.O) {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
// setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
}
  • 4)[不推荐] 如果你前一个页面和需要透明主题的界面屏幕方向一致,我们只需要在清单文件中配置 android:screenOrientation="behind"behind 的意思就是和之前页面的屏幕方向保持一致;
  • 5)[推荐] 通过反射,让系统绕过屏幕方向的检测,设置屏幕不固定:
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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
kotlin复制代码open class FixOreoOrientationActivity: AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
// 必须在 Activity#onCreate() 中 super 之前调用
fixOrientationBugForAndroidO()
super.onCreate(savedInstanceState)
}

/**
* 针对 Android 8.0 版本,如果 Activity 是透明或浮动的,则尝试修复屏幕方向设置的问题
* 异常日志如下:
* Caused by: java.lang.IllegalStateException: Only fullscreen opaque activities can request orientation
* at android.app.Activity.onCreate(Activity.java:1081)
* at android.support.v4.app.SupportActivity.onCreate(SupportActivity.java:66)
* at android.support.v4.app.FragmentActivity.onCreate(FragmentActivity.java:297)
* at android.support.v7.app.AppCompatActivity.onCreate(AppCompatActivity.java:84)
* at xxx.xxx.xxx.ui.XxxActivity.onCreate(XxxActivity.java:43)
* at android.app.Activity.performCreate(Activity.java:7372)
* at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1218)
* at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3147)
* at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3302)
* at android.app.ActivityThread.-wrap12(Unknown Source:0)
* at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1891)
* at android.os.Handler.dispatchMessage(Handler.java:108)
* at android.os.Looper.loop(Looper.java:166)
*/
private fun fixOrientationBugForAndroidO() {
if (Build.VERSION_CODES.O == Build.VERSION.SDK_INT && isTranslucentOrFloating()) {
fixOrientation()
}
}

/**
* 通过反射获取 ActivityInfo,并尝试修改 screenOrientation 属性以避免崩溃
* @return Boolean 是否成功修复 [true:成功 false:失败]
*/
@SuppressLint("DiscouragedPrivateApi")
private fun fixOrientation(): Boolean {
return try {
val field: Field = Activity::class.java.getDeclaredField("mActivityInfo")
field.isAccessible = true
val activityInfo: ActivityInfo = field.get(this) as ActivityInfo
// 设置屏幕方向不固定
activityInfo.screenOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
field.isAccessible = false
true
} catch (e: Exception) {
e.printStackTrace()
false
}
}

/**
* 通过反射检查当前 Activity 是否为透明或浮动
* @return Boolean 是否为透明或浮动 [true:是透明或浮动 false:不是透明或浮动]
*/
@SuppressLint("PrivateApi")
private fun isTranslucentOrFloating(): Boolean {
return try {
val styleableClass: Class<*> = Class.forName("com.android.internal.R\$styleable")
val windowField: Field = styleableClass.getField("Window")
val styleableAttrs: IntArray = windowField.get(null) as IntArray
val typedArray: TypedArray = obtainStyledAttributes(styleableAttrs)

val activityInfoClass: Class<*> = ActivityInfo::class.java
val method: Method = activityInfoClass.getMethod("isTranslucentOrFloating", TypedArray::class.java)
method.isAccessible = true
val isTranslucentOrFloating: Boolean = method.invoke(null, typedArray) as Boolean
method.isAccessible = false
typedArray.recycle()
isTranslucentOrFloating
} catch (e: Exception) {
e.printStackTrace()
false
}
}

/**
* 重写设置屏幕方向的方法
* 以便在 Android 8.0 且 Activity 为透明或浮动时,阻止设置屏幕方向
* @param orientation 屏幕方向
*/
override fun setRequestedOrientation(orientation: Int) {
if (Build.VERSION_CODES.O != Build.VERSION.SDK_INT || ! isTranslucentOrFloating()) {
super.setRequestedOrientation(orientation)
}
}
}
  • 注意
  • 上述的 FixOreoOrientationActivity 代码为反编译懂车帝 Android 代码中获取,由此可见在应用市场已得到有效验证;
  • 其次因为上代码涉及到反射,这些 API 在未来的 Android 版本中可能会改变或不再可访问,这可能导致你的应用在未来的 Android 版本上出现问题,所以需要关注 Android 版本升级相关变更信息,从而持续维护该方法。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%