Android 8.0 只有全屏不透明活动可以请求方向问题
1 背景
Android 8.0
,即 sdk
为 26
时,Android
为了支持全面屏系统增加了一个限制,如果是透明的 Activity
,则不能固定它的方向,因为它的方向其实是依赖其父 Activity
的(因为透明);
- 因此产生了一个系统级别的
Bug
,当以下四个条件同时满足时会发生的崩溃:
- 1)使用的是
Android 8.0
操作系统的设备;
- 2)
targetSdkVersion
设置为 27
以上;
- 3)将背景设置为透明主题;
- 4)固定屏幕方向,
screenOrientation
的值为 portrait
或者 landscape
(代码或者清单文件);
- 崩溃信息如下所示:
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
的源码所抛出的异常错误信息,源码如下所示:
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 = false
, windowSwipeToDismiss = true
;
- 3)
Activity
屏幕方向固定,windowIsFloating = true
。
3 解决思路
- 1)[不推荐] 暴力回退
sdk
版本,即 sdk <= 26
;
- 2)[不推荐] 去除主题中的透明属性,需求允许的话:
<item name="android:windowIsTranslucent">false</item>
- 3)[不推荐] 指定除
8.0
以外的系统固定屏幕方向,去掉清单文件中 screenOrientation
属性,activity
中 onCreate
中执行屏幕方向固定的代码:
if (android.os.Build.VERSION.SDK_INT != android.os.Build.VERSION_CODES.O) {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
}
- 4)[不推荐] 如果你前一个页面和需要透明主题的界面屏幕方向一致,我们只需要在清单文件中配置
android:screenOrientation="behind"
,behind
的意思就是和之前页面的屏幕方向保持一致;
- 5)[推荐] 通过反射,让系统绕过屏幕方向的检测,设置屏幕不固定:
open class FixOreoOrientationActivity: AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
fixOrientationBugForAndroidO()
super.onCreate(savedInstanceState)
}
private fun fixOrientationBugForAndroidO() {
if (Build.VERSION_CODES.O == Build.VERSION.SDK_INT && isTranslucentOrFloating()) {
fixOrientation()
}
}
@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
}
}
@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
}
}
override fun setRequestedOrientation(orientation: Int) {
if (Build.VERSION_CODES.O != Build.VERSION.SDK_INT || ! isTranslucentOrFloating()) {
super.setRequestedOrientation(orientation)
}
}
}
- 注意:
- 上述的
FixOreoOrientationActivity
代码为反编译懂车帝 Android
代码中获取,由此可见在应用市场已得到有效验证;
- 其次因为上代码涉及到反射,这些
API
在未来的 Android
版本中可能会改变或不再可访问,这可能导致你的应用在未来的 Android
版本上出现问题,所以需要关注 Android
版本升级相关变更信息,从而持续维护该方法。