Jetpack Composeでフルスクリーン表示したい

この記事をシェア
JetpackCompose一問一答

Jetpack ComposeでUIを実装しているアプリでフルスクリーン表示を実現する方法を説明します。ここでのフルスクリーンとは、画面上部のステータスバーと画面下部のナビゲーションバーを非表示にして、それらが表示されていた領域までコンテンツを広げて表示している状態のことです。

やりたいこと

図のようにステータスバーとナビゲーションバーを非表示にして、画像をフルスクリーンで表示します。画像はカメラホールの周りの領域にも表示されるようにします。一方でテキストはカメラホールには重ならないようにします。

サンプル

ソースコードはGitHubにあります。

主な環境は以下の通りです。

  • Kotlin 1.7.10
  • Compose Compiler 1.3.0
  • Compose Libraries 1.2.1
  • Material3 1.0.0 alpha15
  • Accompanist 0.25.1

ソースコードと説明

カメラホールの周囲に表示を拡大

はじめに、ウィンドウのlayoutInDisplayCutoutModeLAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGESを指定してコンテンツ描画領域を拡大します。この設定はコンポーザブル関数内から呼び出すとうまく反映されないようでしたので、ActivityのonCreate内で実行しています。またこの設定はAPI Level 28以降で使用可能なので、Build.VERSION.SDK_INTで条件分岐が必要です。

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        if (Build.VERSION.SDK_INT >= 28) {
            window.attributes.layoutInDisplayCutoutMode =
                WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
        }

        setContent {
            ...
        }
    }
}

ちなみにlayoutInDisplayCutoutModeのデフォルト値はLAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULTです。この値がセットされている状態では、カメラホールやノッチの周辺エリアにはコンテンツが描画されず、下図のような表示になってしまいます。

ディスプレイカットアウトの公式ガイドはこちらです。このガイドではXMLでモードを指定していますが、せっかくJetpack Composeで脱XMLを進めているので、上記のようにKotlin側から設定してみました。

コンポーザブル

サンプルのコンポーザブル関数は下記のようになります。

@Composable
fun FullScreenSample() {
    Box(modifier = Modifier.fillMaxSize()) {
        Image(
            painter = painterResource(id = R.drawable.bgimage),
            contentDescription = null,
            contentScale = ContentScale.Crop,
            modifier = Modifier.fillMaxSize()
        )
        Box(modifier = Modifier.fillMaxSize().displayCutoutPadding()) {
            Text(
                text = "FullScreenSample",
                fontSize = 30.sp,
                modifier = Modifier.align(Alignment.Center)
            )
            Text(text = "TopCenter", modifier = Modifier.align(Alignment.TopCenter))
            Text(text = "TopStart", modifier = Modifier.align(Alignment.TopStart))
            Text(text = "TopEnd", modifier = Modifier.align(Alignment.TopEnd))
            Text(text = "CenterStart", modifier = Modifier.align(Alignment.CenterStart))
            Text(text = "CenterEnd", modifier = Modifier.align(Alignment.CenterEnd))
            Text(text = "BottomStart", modifier = Modifier.align(Alignment.BottomStart))
            Text(text = "BottomCenter", modifier = Modifier.align(Alignment.BottomCenter))
            Text(text = "BottomEnd", modifier = Modifier.align(Alignment.BottomEnd))
        }
    }

    val context = LocalContext.current
    DisposableEffect(true) {
        enableFullScreen(context)
        onDispose {
            disableFullScreen(context)
        }
    }
}

外側のBoxImageを配置することにより、全画面に画像を表示しています。

内側のBoxmodifierにはdisplayCutoutPadding()を指定しています。これにより、内側のBoxがカメラホールに重なることを防いでいます。テキストやボタンなどはカメラホールに重なると操作に支障が出るので、このようにして回避する必要があります。

ちなみにdisplayCutoutPadding()を指定しない場合は下図のようになります。テキストがカメラホールに重なってしまっています。

フルスクリーンの有効・無効を切り替える処理はコンポーザブル関数ではない普通の関数なので、DisposableEffect()内部から呼び出しています。DisposableEffectの引数(key)にtrueを指定することによって、FullScreenSample()コンポーザブルが表示されたときに一度だけenableFullScreen()を呼び出し、コンポーザブルが消えるときに一度だけdisableFullScreen()が呼び出されます。enableFullScreen, disableFullScreenの実装は次に紹介します。

全画面表示処理

全画面表示を有効・無効にする処理は下記のようになります。

fun enableFullScreen(context: Context) {
    val window = context.findActivity().window
    WindowCompat.setDecorFitsSystemWindows(window, false)
    WindowInsetsControllerCompat(window, window.decorView).apply {
        systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
        hide(WindowInsetsCompat.Type.systemBars())
    }
}

fun disableFullScreen(context: Context) {
    val window = context.findActivity().window
    WindowCompat.setDecorFitsSystemWindows(window, true)
    WindowInsetsControllerCompat(window, window.decorView).apply {
        show(WindowInsetsCompat.Type.systemBars())
        systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_BARS_BY_TOUCH
    }
}

fun Context.findActivity(): Activity {
    var context = this
    while (context is ContextWrapper) {
        if (context is Activity) return context
        context = context.baseContext
    }
    throw IllegalStateException("no activity")
}

enableFullScreen(), disableFullScreen()とも、やっていることは下記の3つです。

Windowオブジェクト取得

はじめに、ContextからWindowオブジェクトを取得します。Contextはコンポーザブル関数内のLocalContextから取得したものを関数の引数で渡しています。Context.findActivity()Contextの拡張関数で、Contextを順に走査してActivityを取得します。この関数はこちらで紹介されていたもので、Accompanistでも使われているようなので信頼できると思います。Activityが取得できたら、そのプロパティからWindowを取得できます。このWindowオブジェクトを使って以降の処理を行います。

表示領域設定

次に、WindowCompat#setDecorFitsSystemWindows()を呼び出して、表示領域を設定します。falseを指定するとコンポーザブルUIが全画面に描画され、trueを指定するとステータスバーの下端からナビゲーションバーの上端までの範囲にUIが描画されます。ちなみに、Windowクラスにも同じsetDecorFitsSystemWindwos()メソッドがありますが、こちらはAPI Level 30以降でしか使えないので、AndroidXのWindowCompatのほうを使うと楽です。

バーの設定

最後に、WindowInsetsControllerCompatクラスを使ってステータスバーとナビゲーションバー(これらを合わせて「システムバー」と呼びます)の動作を設定します。show()またはhide()でシステムバーを表示・非表示にしています。非表示にしている間のシステムバーの挙動はsystemBarsBehaviorで設定します。BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPEを指定すると、画面端からスワイプしたときにシステムバーが表示され、一定時間たつとまた非表示になるような動作になります。こちらも、WindowInsetsControllerクラスで同じことができますが、やはりAPI Level 30以降でしか使えないので、WindowInsetsControllerCompatを使う方が楽です。

注意点

基本的なフルスクリーンの実現方法は以上ですが、実際のアプリで使う場合にはいくつか工夫が必要かもしれません。

例えば上記サンプルではDisposableEffectonDisposedisableFullScreen()を呼び出していますが、これが実行されるのはフルスクリーンの画面から他の画面に遷移した後になるので、遷移先の画面が一瞬フルスクリーンで表示された直後にフルスクリーンが解除されます。そのときに画面全体がカクっと動いてしまうのが不格好に見えるかもしれません。

これを防ぐには、例えば、ActivityのonCreate()でアプリ全体に対してsetDecorFitsSystemWindows(false)を指定しておき、フルスクリーンではない画面でもステータスバーやナビゲーションバーの下も含めて描画しておくという方法があります。ただ、すべての画面でstatusBarsPaddingnavigationBarsPaddingなどのパディングを適切に設定する必要があります。

また、アプリによっては画面遷移時ではなく画面やボタンのタップによってフルスクリーンの有効・無効を切り替えたい場合もあると思います。その場合は、フルスクリーン状態かどうかをremember変数を使って保持しておくなどの工夫も必要になります。

JetpackCompose一問一答

Jetpack Compose一問一答

コンテンツは随時追加していきます。

この記事をシェア