1. 始める前に
SociaLite は、ソーシャル ネットワーク アプリでよく見られる機能を実装するために Android プラットフォームの各種 API を使用する方法を示しています。さまざまな Jetpack API を使用して複雑な機能を実現することで、多くのデバイスで確実に動作し、必要なコードの量が少なくなるようにします。
この Codelab では、Android 15 でのエッジ ツー エッジの適用に対応した SociaLite アプリを作成するプロセスを説明します。また、アプリでのエッジ ツー エッジの適用を下位互換性がある形で実装します。エッジ ツー エッジの適用に対応した SociaLite は次のようになります。見た目はデバイスとナビゲーション モードによって変化します。
3 ボタン ナビゲーションの SociaLite | ジェスチャー ナビゲーションの SociaLite |
大画面デバイスでの SociaLite |
前提条件
- Kotlin の基礎知識があること。
- Android Studio の設定の Codelab を完了していること。または、Android Studio の利用と、Android 15 を実行するエミュレータまたは実機でのアプリのテストに慣れていること。
演習内容
- Android 15 でのエッジ ツー エッジに関する変更を処理する方法。
- 下位互換性がある形でアプリをエッジ ツー エッジに対応させる方法。
必要なもの
- Android Studio の最新バージョン。
- Android 15 ベータ 1 以降を実行するテストデバイスまたはエミュレータ。
- Android 15 ベータ 1 以降の SDK。
2. スターター コードを取得する
- GitHub からスターター コードをダウンロードします。
または、リポジトリをクローンして codelab_improve_android_experience_2024
ブランチをチェックアウトします。
$ git clone git@github.com:android/socialite.git
$ cd socialite
$ git checkout codelab_improve_android_experience_2024
- Android Studio で SociaLite を開き、Android 15 を実行するデバイスまたはエミュレータで SociaLite アプリを実行します。次のいずれかの画面に似たものが表示されます:
3 ボタン ナビゲーション | ジェスチャー ナビゲーション |
大画面 |
- [チャット] ページで、チャットの 1 つを選択します。たとえば犬とのチャットを選択します。
3 ボタン ナビゲーションの犬とのチャット メッセージ | ジェスチャー ナビゲーションの犬とのチャット メッセージ |
3. Android 15 でアプリをエッジ ツー エッジ対応にする
エッジ ツー エッジとは
アプリがシステムバーの背後に描画を行うことで、洗練されたユーザー エクスペリエンスを提供し、表示スペースを最大限に活用できます。これをエッジ ツー エッジと呼びます。
Android 15 でのエッジ ツー エッジに関する変更を処理する方法
Android 15 以前では、アプリのデフォルトの UI は、ステータスバーやナビゲーション バーなどのシステムバーの領域を避けてレイアウトされるようになっていました。アプリはオプトインしてエッジ ツー エッジになっていました。アプリによって、オプトインは簡単であったり煩雑であったりしました。
Android 15 以降では、アプリはデフォルトでエッジ ツー エッジになります。デフォルトで以下のようになります:
- 3 ボタン ナビゲーション バーは半透明です。
- ジェスチャー ナビゲーション バーは透明です。
- ステータスバーは透明です。
- コンテンツは、インセットかパディングが適用されない限り、ナビゲーション バー、ステータスバー、キャプション バーなどのシステムバーの背後に描画されます。
この変更により、アプリの品質を向上させる手段であるエッジ ツー エッジが見過ごされることがなくなります。また、アプリをエッジ ツー エッジに対応させるために必要な作業が少なくなります。ただし、この変更はアプリに悪影響を及ぼす可能性もあります。ターゲットの SDK を Android 15 にアップグレードすると、悪影響の例 2 つを SociaLite で確認できます。
ターゲットの SDK の値を Android 15 に変更する
- SociaLite アプリの build.gradle ファイルで、ターゲットの SDK とコンパイル SDK のバージョンを Android 15 または
VanillaIceCream
に変更します。
Android 15 の安定版がリリースされる前にこの Codelab を受講している場合、コードは次のようになります:
android {
namespace = "com.google.android.samples.socialite"
compileSdkPreview = "VanillaIceCream"
defaultConfig {
applicationId = "com.google.android.samples.socialite"
minSdk = 21
targetSdkPreview = "VanillaIceCream"
...
}
...
}
Android 15 の安定版がリリースされた後にこの Codelab を受講している場合、コードは次のようになります:
android {
namespace = "com.google.android.samples.socialite"
compileSdk = 35
defaultConfig {
applicationId = "com.google.android.samples.socialite"
minSdk = 21
targetSdk = 35
...
}
...
}
- SociaLite を再ビルドして、次の問題があることを確認します:
- 3 ボタン ナビゲーションの背景保護がナビゲーション バーに適合しません。ジェスチャー ナビゲーション用の [チャット] 画面は介入なしでエッジ ツー エッジで表示されます。ただし、3 ボタン ナビゲーションの背景保護があり、これを削除する必要があります。
3 ボタン ナビゲーションのチャット画面 | ジェスチャー ナビゲーションのチャット画面 |
- UI が遮られます。チャットの下部の UI 要素がナビゲーション バーによって遮られます。これは 3 ボタン ナビゲーションで最も明らかです。
3 ボタン ナビゲーションの犬とのチャット メッセージ | ジェスチャー ナビゲーションの犬とのチャット メッセージ |
SociaLite を修正する
デフォルトの 3 ボタン ナビゲーションの背景保護を削除する手順は次のとおりです:
MainActivity.kt
ファイルで、window.isNavigationBarContrastEnforced プロパティを false に設定して、デフォルトの背景保護を削除します。
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen()
super.onCreate(savedInstanceState)
setContent {
// Add this block:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
window.isNavigationBarContrastEnforced = false
}
}
}
...
}
window.isNavigationBarContrastEnforced
は、完全に透明な背景がリクエストされたときに、ナビゲーション バーに十分なコントラストがあることを保証します。この属性を false に設定すると、実質的に 3 ボタン ナビゲーションの背景を透明に設定することになります。window.isNavigationBarContrastEnforced
は 3 ボタン ナビゲーションにのみ影響します。ジェスチャー ナビゲーションには影響しません。
- アプリを再実行し、Android 15 デバイスでチャットの 1 つを表示します。[タイムライン]、[チャット]、[設定] の画面はいずれもエッジ ツー エッジで表示されます。アプリの [タイムライン]、[チャット]、[設定] ボタンから
NavigationBar
を表示すると、システムの透明な 3 ボタン ナビゲーション バーの背後に描画されます。
縞模様が削除された [チャット] 画面 | ジェスチャー ナビゲーションには変化なし |
ただし、チャットの InputBar
は依然としてシステムバーによって遮られています。こんの問題を解決するには、インセットを適切に処理する必要があります。
3 ボタン ナビゲーションでの犬のチャット。下部の入力フィールドがシステムのナビゲーション バーによって遮られています。 | ジェスチャー ナビゲーションでの犬のチャット。下部の入力フィールドがシステムのナビゲーション バーによって遮られています。 |
SociaLite では InputBar
が隠れています。実際には、回転して横表示に切り替えた場合や、大画面デバイスを使用している場合に、上下左右の要素が隠れる場合があります。これらのユースケースすべてでインセットをどのように処理するか、考慮する必要があります。SociaLite では、InputBar
のタップ可能なコンテンツを上に移動させるために、パディングを適用します。
UI が遮られる問題を解決するためにインセットを適用する手順は次のとおりです:
ui/chat/ChatScreen.kt
ファイルに移動して、178 行目あたりにあるChatContent
コンポーザブルを探します。ここにはチャット画面の UI が含まれています。ChatContent
はScaffold
を利用して UI を簡単に構築しています。デフォルトでは、システムバーの重なり順や、Scaffold
のパディングの値(innerPadding
パラメータ)を使用して消費できるインセットなど、Scaffold
はシステム UI についての情報を提供します。Scaffold
のinnerPadding
を使用して、InputBar
にパディングを追加します。ChatContent
の 214 行目あたりでInputBar
を探します。これは、ユーザーがメッセージを書くための UI を作成する、カスタムのコンポーザブルです。プレビューは次のようになります。
InputBar
は contentPadding
を受け取り、それを Row コンポーザブルのパディングとして適用します。このコンポーザブルには残りの UI が含まれています。パディングは Row コンポーザブルのすべての辺に適用されます。この動作に関する記述は 432 行目あたりにあります。参考に、InputBar
コンポーザブルのコードを挙げます(このコードを追加しないでください):
// Don't add this code because it's only for reference.
@Composable
private fun InputBar(
contentPadding: PaddingValues,
...,
) {
Surface(...) {
Row(
modifier = Modifier
.padding(contentPadding)
...
) {
IconButton(...) { ... } // take picture
IconButton(...) { ... } // attach picture
TextField(...) // write message
FilledIconButton(...){ ... } // send message
}
}
}
}
ChatContent
内のInputBar
に戻り、contentPadding
を変更して、システムバーのインセットを消費するようにします。これは 220 行目あたりにあります。
InputBar(
...
contentPadding = innerPadding, //Add this line.
// contentPadding = PaddingValues(0.dp), // Remove this line.
...
)
- Android 15 デバイスでアプリを再実行します。
インセットが正しく適用されていない 3 ボタン ナビゲーションでの犬のチャット。 | インセットが正しく適用されていないジェスチャー ナビゲーションでの犬のチャット。 |
下部のパディングが適用され、ボタンがシステムバーによって遮られなくなりました。しかし、上にもパディングが適用されています。上のパディングは TopAppBar
とシステムバーの重なり順にもおよびます。Scaffold はその内容にパディングの値を渡すので、上部のアプリバーとシステムバーを避けることができます。
- 上のパディングを修正するために、
innerPadding
PaddingValues
のコピーを作成して、上のパディングを0.dp
に設定し、変更したコピーをcontentPadding
に渡します。
InputBar(
...
contentPadding = innerPadding.copy(layoutDirection, top = 0.dp), //Add this line.
// contentPadding = innerPadding, // Remove this line.
...
)
- Android 15 デバイスでアプリを再実行します。
インセットが正しく適用された 3 ボタン ナビゲーションでの犬のチャット。 | インセットが正しく適用されたジェスチャー ナビゲーションでの犬のチャット。 |
お疲れさまでした。SociaLite を Android 15 プラットフォームでのエッジ ツー エッジに関する変更に対応させることができました。次に、SociaLite を下位互換性がある形でエッジ ツー エッジに対応させる方法を学びます。
4. SociaLite を下位互換性がある形でエッジ ツー エッジに対応させる
SociaLite は Android 15 ではエッジ ツー エッジに対応しましたが、古い Android デバイスではまだエッジ ツー エッジに対応していません。古い Android デバイスで SociaLite をエッジ ツー エッジに対応させるために、MainActivity.kt
ファイルでコンテンツを設定する前に enableEdgeToEdge
を呼び出します。
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen()
enableEdgeToEdge() // Add this line.
window.isNavigationBarContrastEnforced = false
super.onCreate(savedInstanceState)
setContent {... }
}
}
enableEdgeToEdge
のインポートは import androidx.activity.enableEdgeToEdge
です。依存関係は AndroidX Activity 1.8.0 以降です。
下位互換性がある形でアプリをエッジ ツー エッジに対応させる方法とインセットを処理する方法について詳しくは、以下のガイドをご覧ください。
- Compose でのウィンドウ インセット
- アプリでコンテンツをエッジ ツー エッジで表示する
このパスウェイでのエッジ ツー エッジの適用に関する説明は以上です。次のセクションはオプションです。そちらでは、アプリによっては該当する可能性がある、エッジ ツー エッジに関する考慮事項について説明します。
5. オプション: エッジ ツー エッジに関する追加の考慮事項
アーキテクチャ間でのインセットの処理
コンポーネント
SociaLite のコンポーネントの多くは、ターゲットの SDK の値を変更した後に変化しませんでした。SociaLite はベスト プラクティスに沿って構築されているため、このプラットフォームの変更を簡単に行うことができます。ベスト プラクティスの一部を次に示します:
- マテリアル デザイン 3 のコンポーネントを使用する(
androidx.compose.material3
)。TopAppBar
、BottomAppBar
、NavigationBar
などがこれに該当します。これらはインセットを自動的に適用します。 - アプリが Compose でマテリアル デザイン 2 のコンポーネント(
androidx.compose.material
)を使用している場合、コンポーネントはインセットを自動的には処理しません。ただし、インセットにアクセスして手動で適用することはできます。androidx.compose.material 1.6.0
以降では、windowInsets
パラメータを使用して、BottomAppBar
、TopAppBar
、BottomNavigation
、NavigationRail
にインセットを手動で適用します。同様に、Scaffold
にはcontentWindowInsets
パラメータを使用します。別の方法としては、インセットをパディングとして手動で設定することもできます。 - アプリでビューとマテリアル コンポーネント(
com.google.android.material
)を使用する場合、ビューベースのマテリアル コンポーネントの多く(BottomNavigationView
、BottomAppBar
、NavigationRailView
、NavigationView
など)はインセットを処理します。追加の作業は不要です。ただし、AppBarLayout
を使用する場合はandroid:fitsSystemWindows="true"
を追加する必要があります。 - アプリがビューと
BottomSheet
、SideSheet
、またはカスタム コンテナを使用する場合、ViewCompat.setOnApplyWindowInsetsListener
を使用してパディングを適用します。RecyclerView
については、このリスナーを使用してパディングを適用して、さらにclipToPadding="false"
を追加します。 - 複雑な UI に対しては、
Surface
ではなくScaffold
(またはNavigationSuiteScaffold
あるいはListDetailPaneScaffold
)を使用します。Scaffold
を使用すると、TopAppBar
、BottomAppBar
、NavigationBar
、NavigationRail
を簡単に配置できます。
コンテンツをスクロールする
アプリにリストがある場合、Android 15 での変更に伴い、リストの最後のアイテムがシステムのナビゲーション バーによって遮られることがあります。
3 ボタン ナビゲーションでリストの最後のアイテムが遮られている例です。
Compose でコンテンツをスクロールする
TextField
を使っている場合を除き、Compose で LazyColumn
の contentPadding を使用して、最後のアイテムにスペースを追加します:
Scaffold { innerPadding ->
LazyColumn(
contentPadding = innerPadding
) {
// Content that does not contain TextField
}
}
3 ボタン ナビゲーションでリストの最後のアイテムが遮られていない例です。
TextField
については、LazyColumn
で Spacer
を使用して最後の TextField
を描画します。詳しくは、インセットの消費をご覧ください。
LazyColumn(
Modifier.imePadding()
) {
// Content with TextField
item {
Spacer(
Modifier.windowInsetsBottomHeight(
WindowInsets.systemBars
)
)
}
}
ビューでコンテンツをスクロールする
RecyclerView
または NestedScrollView
には、android:clipToPadding="false"
を追加します。
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
app:layoutManager="LinearLayoutManager" />
setOnApplyWindowInsetsListener
を使用して、ウィンドウ インセットから左、右、下のパディングを提供します:
ViewCompat.setOnApplyWindowInsetsListener(binding.recycler) { v, insets ->
val i = insets.getInsets(
WindowInsetsCompat.Type.systemBars() + WindowInsetsCompat.Type.displayCutout()
)
v.updatePadding(
left = i.left,
right = i.right,
bottom = i.bottom + bottomPadding,
)
WindowInsetsCompat.CONSUMED
}
LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS を使用する
ターゲットの SDK が 35 以前の場合、SociaLite は横向きではこのように表示されました。左端には、カメラ カットアウトを考慮した大きな白いボックスが表示されました。3 ボタン ナビゲーションでは、ボタンは右側にあります。
ターゲットの SDK が 35 以降の場合、SociaLite はこのように表示されます。左端のカメラ カットアウトを考慮した大きな白いボックスはなくなりました。この効果を実現するために、Android は LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS を自動的に設定します。
アプリによっては、インセットをここで処理できます。
SociaLite の場合は、次の手順を行います:
ui/ContactRow.kt
ファイルで Row コンポーザブルを見つけます。- ディスプレイ カットアウトを考慮してパディングを変更します。
@Composable
fun ChatRow(
chat: ChatDetail,
onClick: (() -> Unit)?,
modifier: Modifier = Modifier,
) {
// Add layoutDirection, displayCutout, startPadding, and endPadding.
val layoutDirection = LocalLayoutDirection.current
val displayCutout = WindowInsets.displayCutout.asPaddingValues()
val startPadding = displayCutout.calculateStartPadding(layoutDirection)
val endPadding = displayCutout.calculateEndPadding(layoutDirection)
Row(
modifier = modifier
...
// .padding(16.dp) // Remove this line.
// Add this block:
.padding(
PaddingValues(
top = 16.dp,
bottom = 16.dp,
// Ensure content is not occluded by display cutouts
// when rotating the device.
start = startPadding.coerceAtLeast(16.dp),
end = endPadding.coerceAtLeast(16.dp)
)
),
...
) { ... }
ディスプレイ カットアウトを処理した SociaLite はこのように表示されます:
[ディスプレイ カットアウト] の下の [開発者向けオプション] で、ディスプレイ カットアウトのさまざまな設定をテストできます。
アプリに非フローティング ウィンドウ(アクティビティなど)があり、それが LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT
、LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER
、または LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
を使用している場合、Android 15 ベータ 2 以降では、Android はこれらのカットアウト モードが LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
であると解釈します。以前の Android 15 ベータ 1 では、アプリはクラッシュしました。
システムバーの一種であるキャプション バー
キャプション バーもシステムバーの一種です。キャプション バーは、上部のタイトルバーなど、フリーフォーム ウィンドウのシステム UI ウィンドウの装飾を説明します。Android Studio のデスクトップ エミュレータ内でキャプション バーを表示できます。以降のスクリーンショットでは、キャプション バーはアプリの上部にあります。
Compose では、Scaffold の PaddingValues
、safeContent
、safeDrawing
、または組み込みの WindowInsets.systemBars
を使用している場合、アプリは想定どおりに表示されます。ただし、statusBar
でインセットを処理している場合、アプリのコンテンツが想定どおりに表示されない可能性があります。これは、ステータスバーがキャプション バーを考慮しないためです。
ビューでは、WindowInsetsCompat.systemBars
を使用してインセットを手動で処理している場合、アプリは想定どおりに表示されます。WindowInsetsCompat.statusBars
を使用してインセットを手動で処理している場合、アプリは想定どおりに表示されない可能性があります。これは、ステータスバーはキャプション バーではないためです。
イマーシブ モードのアプリ
イマーシブなアプリはすでにエッジ ツー エッジになっているため、イマーシブ モードの画面は、Android 15 でのエッジ ツー エッジの適用からほとんど影響を受けません。
システムバーを保護する
アプリのジェスチャー ナビゲーションでは透明なバー、3 ボタン ナビゲーションでは半透明または不透明のバーを使いたい場合があるかもしれません。
Android 15 では、半透明の 3 ボタン ナビゲーションがデフォルトです。これは、プラットフォームで window.isNavigationBarContrastEnforced
プロパティが true
に設定されるためです。ジェスチャー ナビゲーションは透明なままです。
3 ボタン ナビゲーションはデフォルトで半透明です。 |
一般には半透明の 3 ボタン ナビゲーションで十分です。ただし、場合によってはアプリに不透明の 3 ボタン ナビゲーションが必要です。まず、window.isNavigationBarContrastEnforced
プロパティを false
に設定します。次に、ビューには WindowInsetsCompat.tappableElement
、Compose には WindowInsets.tappableElement
を使用します。これらが 0 の場合、ユーザーはジェスチャー ナビゲーションを使用しています。0 でない場合は、ユーザーは 3 ボタン ナビゲーションを使用しています。ユーザーが 3 ボタン ナビゲーションを使用している場合は、ナビゲーション バーの背後にビューまたはボックスを描画します。Compose の例はこのようになります:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
window.isNavigationBarContrastEnforced = false
MyTheme {
Surface(...) {
MyContent(...)
ProtectNavigationBar()
}
}
}
}
}
// Use only if required.
@Composable
fun ProtectNavigationBar(modifier: Modifier = Modifier) {
val density = LocalDensity.current
val tappableElement = WindowInsets.tappableElement
val bottomPixels = tappableElement.getBottom(density)
val usingTappableBars = remember(bottomPixels) {
bottomPixels != 0
}
val barHeight = remember(bottomPixels) {
tappableElement.asPaddingValues(density).calculateBottomPadding()
}
Column(
modifier = modifier.fillMaxSize(),
verticalArrangement = Arrangement.Bottom
) {
if (usingTappableBars) {
Box(
modifier = Modifier
.background(MaterialTheme.colorScheme.background)
.fillMaxWidth()
.height(barHeight)
)
}
}
}
不透明な 3 ボタン ナビゲーション |
6. 解答コードを確認する
MainActivity.kt
ファイルの onCreate
メソッドはこのようになります:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen()
enableEdgeToEdge()
window.isNavigationBarContrastEnforced = false
super.onCreate(savedInstanceState)
setContent {
Main(
shortcutParams = extractShortcutParams(intent),
)
}
}
}
ChatScreen.kt
ファイル内の ChatContent
コンポーザブルはインセットを処理する必要があります:
private fun ChatContent(...) {
...
Scaffold(...) { innerPadding ->
Column {
...
InputBar(
input = input,
onInputChanged = onInputChanged,
onSendClick = onSendClick,
onCameraClick = onCameraClick,
onPhotoPickerClick = onPhotoPickerClick,
contentPadding = innerPadding.copy(
layoutDirection, top = 0.dp
),
sendEnabled = sendEnabled,
modifier = Modifier
.fillMaxWidth()
.windowInsetsPadding(
WindowInsets.ime.exclude(WindowInsets.navigationBars)
),
)
}
}
}
解答コードは main ブランチから取得できます。SociaLite をダウンロード済みの場合:
git checkout main
そうでない場合、コードを再度ダウンロードして、直接または git を介して main ブランチを表示できます:
git clone git@github.com:android/socialite.git