Jetpack ComposeでUIを実装しているアプリでフルスクリーン表示を実現する方法を説明します。ここでのフルスクリーンとは、画面上部のステータスバーと画面下部のナビゲーションバーを非表示にして、それらが表示されていた領域までコンテンツを広げて表示している状態のことです。
目次
やりたいこと
図のようにステータスバーとナビゲーションバーを非表示にして、画像をフルスクリーンで表示します。画像はカメラホールの周りの領域にも表示されるようにします。一方でテキストはカメラホールには重ならないようにします。
サンプル
ソースコードはGitHubにあります。
主な環境は以下の通りです。
- Kotlin 1.7210
- Compose Compiler 1.3.2
- Compose Libraries 1.2.1
- Material3 1.0.0-rc01
- Accompanist 0.25.1
ソースコードと説明
カメラホールの周囲に表示を拡大
はじめに、ウィンドウのlayoutInDisplayCutoutMode
にLAYOUT_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)
}
}
}
外側のBox
にImage
を配置することにより、全画面に画像を表示しています。
内側のBox
のmodifier
には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
を使う方が楽です。
注意点
基本的なフルスクリーンの実現方法は以上ですが、実際のアプリで使う場合にはいくつか工夫が必要かもしれません。
例えば上記サンプルではDisposableEffect
のonDispose
でdisableFullScreen()
を呼び出していますが、これが実行されるのはフルスクリーンの画面から他の画面に遷移した後になるので、遷移先の画面が一瞬フルスクリーンで表示された直後にフルスクリーンが解除されます。そのときに画面全体がカクっと動いてしまうのが不格好に見えるかもしれません。
これを防ぐには、例えば、ActivityのonCreate()
でアプリ全体に対してsetDecorFitsSystemWindows(false)
を指定しておき、フルスクリーンではない画面でもステータスバーやナビゲーションバーの下も含めて描画しておくという方法があります。ただ、すべての画面でstatusBarsPadding
やnavigationBarsPadding
などのパディングを適切に設定する必要があります。
また、アプリによっては画面遷移時ではなく画面やボタンのタップによってフルスクリーンの有効・無効を切り替えたい場合もあると思います。その場合は、フルスクリーン状態かどうかをremember
変数を使って保持しておくなどの工夫も必要になります。
コンテンツは随時追加していきます。