Gunosy Tech Blog

Gunosy Tech Blogは株式会社Gunosyのエンジニアが知見を共有する技術ブログです。

Android Jetpack Compose 横向きフルスクリーン表示

こんにちは。Android アプリ開発担当の nagayama(@nagayan_dev)です。
今回は Jetpack Compose で横向きフルスクリーン表示の対応をしたため、その内容をお伝えします。




対応方針

今回は WebView を、横画面 かつ フルスクリーン で表示をしたいと思います。WebView の実装は SamplePage の Composable で実装しており、詳細は割愛します。
また一時的な表示とするため、ダイアログで横画面・フルスクリーンの表示にします。そのダイアログが終了すると、画面は元に戻る仕様にします。

実装

ダイアログを表示する

Dialog を用いて、ダイアログを実装します。fullScreen フラグを remember で保持し、表示切り替えができるようにします。

var fullScreen by remember { mutableStateOf(true) }
if (fullScreen) {
  Dialog(
    onDismissRequest = {
      // ダイアログがキャンセルになった時の処理
      fullScreen = false
    },
    content = {
      SamplePage(
        modifier = Modifier
          .fillMaxSize()
        )
      }
    )
}



縦横切り替え

続けて画面を縦から横に切り替える処理を実装します。下記のように、画面の縦横を切り替える Composable を作成します。

Fragment 等で縦横切り替えを行う処理と同様に、Activity のメソッドで切り替えを行います。 Activity を参照するため、 Context から探すメソッドを作成します。

今回は一時的な横画面表示であるため、DisposableEffect の onDispose で Composable が破棄された時に元に戻すようにします。

@Composable
private fun ChangeOrientationLandscape() {
  val context = LocalContext.current
  if (context.resources.configuration.orientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) return
  DisposableEffect(context) {
    val activity = context.findActivity() ?: return@DisposableEffect onDispose {}
    activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
    onDispose {
      activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
    }
  }
}

private fun Context.findActivity(): Activity? {
  return when (this) {
    is Activity       -> this
    is ContextWrapper -> baseContext.findActivity()
    else              -> null
  }
}
  Dialog(
    〜省略〜
    content = {
      // 横画面切り替え
      ChangeOrientationLandscape()
      SamplePage(
        modifier = Modifier
          .fillMaxSize()
      )
    }
  )



フルスクリーン

フルスクリーン表示の実装を行います。

まず、Dialog の properties パラメータに DialogProperties のインスタンスを指定し、decorFitsSystemWindows を false に設定します。これにより Dialog の横幅を変更できるようになります。

次に、ダイアログで表示している Window サイズをリサイズし、画面全体で表示するよう LayoutParams を操作します。先ほどと同じ処理から Activity を参照し、Dialog の window と親 View にコピーします。

ここまで行うと、ナビゲーションバーにダイアログが被って表示されてしまいます。これを回避するため、 WindowInsets からナビゲーションバーの高さを取得し、その分の padding を設定します。ジェスチャーナビゲーションを設定している場合はこの高さが 0 になるため、問題なく表示されます。

  Dialog(
    〜省略〜
    properties = DialogProperties(
          decorFitsSystemWindows = false
    ),
    content = {
      ChangeOrientationLandscape()
      // フルスクリーン
      val activityWindow = LocalView.current.context.findActivity()?.window
      val dialogWindow = (LocalView.current.parent as? DialogWindowProvider)?.window
      val parentView = LocalView.current.parent as View
      SideEffect {
        if (activityWindow != null && dialogWindow != null) {
          val attributes = WindowManager.LayoutParams().also {
            it.copyFrom(activityWindow.attributes)
            it.type = dialogWindow.attributes.type
          }
          dialogWindow.attributes = attributes
          parentView.layoutParams = FrameLayout.LayoutParams(activityWindow.decorView.width, activityWindow.decorView.height)
        }
      }
      // 全画面にした時 Navigation Bar に被って表示されてしまうため、そのサイズ分 padding を設定します
      val navigationPadding = WindowInsets.navigationBars.asPaddingValues().calculateEndPadding(LayoutDirection.Ltr)
      SamplePage(
        modifier = Modifier
          .fillMaxSize()
          .padding(end = navigationPadding)
      )
    }
  )



ステータスバー表示切り替え

最後の仕上げです。フルスクリーンで表示された WebView の上にステータスバーのアイコン等が表示されます。フルスクリーン時にステータスバーを非表示にするため、再度 Activity を取得して Window クラスに処理を行います。

API レベル 30 以上の場合は、WindowInsetsController を取得し、show / hide でステータスバーを指定して表示を切り替えます。 30 未満の場合は、Window クラスの setFlags / clearFlags でステータスバーの Flag を追加・削除をして切り替えを行います。

@Composable
fun ChangeStatusBarVisibility(isShowStatusBar: Boolean) {
  val activityWindow = LocalView.current.context.findActivity()?.window ?: return
  if (isShowStatusBar) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
      activityWindow.decorView.windowInsetsController?.show(android.view.WindowInsets.Type.statusBars())
    } else {
      activityWindow.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
    }
  } else {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
      activityWindow.decorView.windowInsetsController?.hide(android.view.WindowInsets.Type.statusBars())
    } else {
      activityWindow.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN)
    }
  }
}
@Composable
fun FullScreenSample() {
  var fullScreen by remember { mutableStateOf(false) }
  if (fullScreen) {
    // フルスクリーン表示
    Dialog(〜省略〜)
  } else {
    // 通常表示
    〜省略〜
  }
  ChangeStatusBarVisibility(!fullScreen)
}



以上で横画面フルスクリーン表示ができました。

まとめ

Jetpack Compose の横向きフルスクリーン表示についてまとめました。やることが多く複雑な処理が多いですが、処理を分割して整理しながら実装できるのは Compose の大きなメリットであると思います。これからも Jetpack Compose ライフを楽しみましょう。