Android 8.0 只有全屏不透明活動可以請求方向問題

Android 8.0 只有全屏不透明活動可以請求方向問題

1 背景

  • Android 8.0,即 sdk26 時,Android 為了支持全面屏系統(tǒng)增加了一個限制,如果是透明的 Activity,則不能固定它的方向,因?yàn)樗姆较蚱鋵?shí)是依賴其父 Activity 的(因?yàn)橥该鳎?/li>
  • 因此產(chǎn)生了一個系統(tǒng)級別的 Bug,當(dāng)以下四個條件同時滿足時會發(fā)生的崩潰:
  • 1)使用的是 Android 8.0 操作系統(tǒng)的設(shè)備;
  • 2)targetSdkVersion 設(shè)置為 27 以上;
  • 3)將背景設(shè)置為透明主題;
  • 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;
  }

}

  • 通過閱讀上述的源碼可知,觸發(fā) 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 以外的系統(tǒng)固定屏幕方向,去掉清單文件中 screenOrientation 屬性,activityonCreate 中執(zhí)行屏幕方向固定的代碼:
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)[推薦] 通過反射,讓系統(tǒng)繞過屏幕方向的檢測,設(shè)置屏幕不固定:
open class FixOreoOrientationActivity: AppCompatActivity() {

  override fun onCreate(savedInstanceState: Bundle?) {
    // 必須在 Activity#onCreate() 中 super 之前調(diào)用
    fixOrientationBugForAndroidO()
    super.onCreate(savedInstanceState)
  }

  /**
   * 針對 Android 8.0 版本,如果 Activity 是透明或浮動的,則嘗試修復(fù)屏幕方向設(shè)置的問題
   * 異常日志如下:
   * 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 是否成功修復(fù) [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
      // 設(shè)置屏幕方向不固定
      activityInfo.screenOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
      field.isAccessible = false
      true
    } catch (e: Exception) {
      e.printStackTrace()
      false
    }
  }

  /**
   * 通過反射檢查當(dāng)前 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
    }
  }

  /**
   * 重寫設(shè)置屏幕方向的方法
   * 以便在 Android 8.0 且 Activity 為透明或浮動時,阻止設(shè)置屏幕方向
   * @param orientation 屏幕方向
   */
  override fun setRequestedOrientation(orientation: Int) {
    if (Build.VERSION_CODES.O != Build.VERSION.SDK_INT || ! isTranslucentOrFloating()) {
      super.setRequestedOrientation(orientation)
    }
  }
}

  • 注意
  • 上述的 FixOreoOrientationActivity 代碼為反編譯懂車帝 Android 代碼中獲取,由此可見在應(yīng)用市場已得到有效驗(yàn)證;
  • 其次因?yàn)樯洗a涉及到反射,這些 API 在未來的 Android 版本中可能會改變或不再可訪問,這可能導(dǎo)致你的應(yīng)用在未來的 Android 版本上出現(xiàn)問題,所以需要關(guān)注 Android 版本升級相關(guān)變更信息,從而持續(xù)維護(hù)該方法。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容