Membuat dan menguji aplikasi parkir untuk Android Automotive OS

1. Sebelum memulai

Tidak termasuk dalam cakupan

  • Panduan tentang cara membuat aplikasi media (audio - misalnya musik, radio, podcast) untuk Android Auto dan Android Automotive OS. Lihat Membangun aplikasi media untuk mobil guna mengetahui detail tentang cara membangun aplikasi serupa.

Yang akan Anda butuhkan

Yang akan Anda bangun

Dalam codelab ini, Anda akan mempelajari cara memigrasikan aplikasi seluler streaming video yang ada, Road Reels, ke Android Automotive OS.

Versi titik awal aplikasi yang berjalan di ponsel

Versi lengkap aplikasi yang berjalan di emulator Android Automotive OS dengan potongan layar.

Versi titik awal aplikasi yang berjalan di ponsel

Versi lengkap aplikasi yang berjalan di emulator Android Automotive OS dengan potongan layar.

Yang akan Anda pelajari

  • Cara menggunakan emulator Android Automotive OS.
  • Cara membuat perubahan yang diperlukan untuk membuat build Android Automotive OS.
  • Asumsi umum yang dibuat ketika mengembangkan aplikasi untuk perangkat seluler yang mungkin rusak saat aplikasi berjalan di Android Automotive OS.
  • Berbagai tingkat kualitas untuk aplikasi di mobil.
  • Cara menggunakan sesi media untuk memungkinkan aplikasi lain mengontrol pemutaran aplikasi Anda.
  • Perbedaan UI sistem dan inset jendela di perangkat Android Automotive OS dibandingkan dengan perangkat seluler.

2. Memulai persiapan

Mendapatkan kode

  1. Kode untuk codelab ini dapat ditemukan di direktori build-a-parked-app dalam repositori GitHub car-codelabs. Untuk membuat clone kode ini, jalankan perintah berikut:
git clone https://github.com/android/car-codelabs.git
  1. Atau, Anda dapat mendownload repositori sebagai file ZIP:

Membuka project

  • Setelah memulai Android Studio, impor project dengan memilih direktori build-a-parked-app/start saja. Direktori build-a-parked-app/end berisi kode solusi yang dapat Anda rujuk kapan saja jika Anda mengalami kebuntuan atau hanya ingin melihat project lengkap.

Memahami kode

  • Setelah membuka project di Android Studio, luangkan waktu untuk memeriksa kode awal.

3. Mempelajari aplikasi parkir untuk Android Automotive OS

Aplikasi parkir merupakan subkumpulan kategori aplikasi yang didukung oleh Android Automotive OS. Pada saat penulisan codelab ini, aplikasi tersebut terdiri dari aplikasi streaming video, browser web, dan game. Aplikasi ini sangat cocok untuk mobil, mengingat hardware yang ada di kendaraan yang dilengkapi Google dan meningkatnya prevalensi kendaraan listrik. Waktu pengisian daya memberikan peluang besar bagi pengemudi dan penumpang untuk berinteraksi dengan jenis aplikasi ini.

Dalam banyak hal, mobil mirip dengan perangkat layar besar lainnya seperti tablet dan perangkat foldable. Mobil memiliki layar sentuh dengan ukuran, resolusi, dan rasio aspek yang serupa, dan bisa saja dalam orientasi potret atau lanskap (meskipun, tidak seperti tablet, orientasi layar mobil tetap). Mobil juga merupakan perangkat terhubung yang dapat masuk dan keluar dari koneksi jaringan. Dengan semua hal itu, tidak mengherankan jika aplikasi yang sudah adaptif sering kali memerlukan sedikit saja upaya untuk menghadirkan pengalaman pengguna yang sangat baik di mobil.

Serupa dengan perangkat layar besar, ada juga tingkat kualitas aplikasi untuk aplikasi di mobil:

  • Tingkat 3 - Siap digunakan di mobil: Aplikasi Anda kompatibel dengan perangkat layar besar dan dapat digunakan saat mobil diparkir. Meskipun aplikasi ini mungkin tidak memiliki fitur yang dioptimalkan untuk mobil, pengguna dapat menikmati aplikasi ini seperti yang mereka lakukan di perangkat Android layar besar lainnya. Aplikasi seluler yang memenuhi persyaratan ini layak didistribusikan ke mobil secara apa adanya melalui program aplikasi seluler siap digunakan untuk mobil.
  • Tingkat 2 - Dioptimalkan untuk mobil: Aplikasi Anda memberikan pengalaman yang sangat baik pada tampilan stack tengah mobil. Agar dapat digunakan di mobil, aplikasi Anda harus melalui beberapa rekayasa khusus mobil untuk menyertakan kemampuan yang dapat digunakan di seluruh mode mengemudi atau parkir, bergantung pada kategori aplikasi Anda.
  • Tingkat 1 - Dirancang khusus untuk mobil: Aplikasi Anda dibuat agar berfungsi di berbagai hardware pada mobil dan dapat menyesuaikan pengalamannya di seluruh mode mengemudi dan parkir. Aplikasi ini pada tingkat ini memberikan pengalaman pengguna terbaik yang didesain untuk berbagai layar di mobil, seperti konsol tengah, cluster instrumen, dan layar tambahan - seperti tampilan panorama yang terlihat di banyak mobil premium.

4. Menjalankan aplikasi di emulator Android Automotive OS

Menginstal Image Sistem Automotive with Play Store

  1. Pertama, buka SDK Manager di Android Studio, lalu pilih tab SDK Platforms jika belum dipilih. Di pojok kanan bawah jendela SDK Manager, pastikan kotak Show package details dicentang.
  2. Instal image emulator Android Automotive dengan Google API menggunakan API 33 yang tercantum di Menambahkan Generic System Image (GSI). Image hanya dapat berjalan di komputer dengan arsitektur yang sama (x86/ARM) dengan image itu sendiri.

Membuat Perangkat Virtual Android untuk Android Automotive OS

  1. Setelah membuka Pengelola Perangkat, pilih Automotive di kolom Category di sisi kiri jendela. Kemudian, pilih profil hardware gabungan Automotive (1408p landscape) dari daftar, lalu klik Next.
  2. Di halaman berikutnya, pilih image sistem dari langkah sebelumnya. Klik Next dan pilih opsi lanjutan yang Anda inginkan sebelum akhirnya membuat AVD dengan mengklik Finish. Catatan: jika Anda memilih image API 30, image tersebut mungkin berada di tab selain tab Recommended.

Menjalankan aplikasi

Jalankan aplikasi di emulator yang baru saja Anda buat menggunakan konfigurasi run app yang ada. Coba gunakan aplikasi di berbagai layar dan bandingkan perilakunya dengan ketika dijalankan di emulator ponsel atau tablet.

599922cd078f2589.png

5. Memperbarui manifes untuk mendeklarasikan dukungan Android Automotive OS

Meskipun aplikasi "berfungsi", ada beberapa perubahan kecil yang perlu dilakukan agar aplikasi berfungsi dengan baik di Android Automotive OS dan memenuhi persyaratan agar dapat dipublikasikan di Play Store. Perubahan ini dapat dilakukan sedemikian rupa sehingga APK atau App Bundle yang sama dapat mendukung perangkat seluler dan Android Automotive OS. Kumpulan perubahan pertama adalah memperbarui file AndroidManifest.xml untuk menunjukkan bahwa aplikasi mendukung perangkat Android Automotive OS dan merupakan aplikasi video.

Mendeklarasikan fitur hardware otomotif

Untuk menunjukkan bahwa aplikasi Anda mendukung perangkat Android Automotive OS, tambahkan elemen <uses-feature> berikut dalam file AndroidManifest.xml:

AndroidManifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    ...
    <uses-feature
        android:name="android.hardware.type.automotive"
        android:required="false" />
    ...
</manifest>

Menggunakan nilai false untuk atribut android:required memungkinkan APK atau App Bundle yang dihasilkan didistribusikan ke perangkat Android Automotive OS dan perangkat seluler. Lihat Memilih jenis jalur untuk Android Automotive OS guna mengetahui informasi selengkapnya.

Menandai aplikasi sebagai aplikasi video

Bagian terakhir metadata yang perlu ditambahkan adalah file automotive_app_desc.xml. File ini digunakan untuk mendeklarasikan kategori aplikasi Anda dalam konteks Android untuk Mobil, dan tidak bergantung pada kategori yang Anda pilih untuk aplikasi di Konsol Play.

  1. Klik kanan modul app dan pilih opsi New > Android Resource File, lalu masukkan nilai berikut sebelum mengklik OK:
  • Nama file: automotive_app_desc.xml
  • Jenis resource: XML
  • Elemen root: automotiveApp
  • Set sumber: main
  • Nama direktori: xml

9fc697aec93d9d09.png

  1. Dalam file tersebut, tambahkan elemen <uses> berikut untuk mendeklarasikan bahwa aplikasi Anda adalah aplikasi video.

automotive_app_desc.xml

<?xml version="1.0" encoding="utf-8"?>
<automotiveApp xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <uses
        name="video"
        tools:ignore="InvalidUsesTagAttribute" />
</automotiveApp>
  1. Dalam elemen <application> yang ada, tambahkan elemen <meta-data> berikut yang mereferensikan file automotive_app_desc.xml yang baru saja Anda buat.

AndroidManifest.xml

<application ...>
    <meta-data
        android:name="com.android.automotive"
        android:resource="@xml/automotive_app_desc" />
</application>

Dengan demikian, Anda telah membuat semua perubahan yang diperlukan untuk mendeklarasikan dukungan Android Automotive OS.

6. Memenuhi persyaratan kualitas Android Automotive OS: Kemampuan Navigasi

Meskipun mendeklarasikan dukungan Android Automotive OS adalah salah satu bagian dari menghadirkan aplikasi Anda di mobil, Anda tetap perlu memastikan aplikasi dapat digunakan dan aman digunakan.

Menambahkan kemampuan navigasi

Saat menjalankan aplikasi di emulator Android Automotive OS, Anda mungkin melihat bahwa kembali dari layar detail ke layar utama atau dari layar pemutar ke layar detail tidak mungkin dilakukan. Tidak seperti faktor bentuk lainnya, yang mungkin memerlukan tombol kembali atau gestur sentuh untuk mengaktifkan navigasi kembali, perangkat Android Automotive OS tidak memiliki persyaratan semacam itu. Dengan demikian, aplikasi harus menyediakan kemampuan navigasi di UI-nya untuk memastikan pengguna dapat melakukan navigasi tanpa terhenti di layar dalam aplikasi. Persyaratan ini dikodifikasi sebagai pedoman kualitas AN-1.

Untuk mendukung navigasi kembali dari layar detail ke layar utama, tambahkan parameter navigationIcon tambahan untuk CenterAlignedTopAppBar layar detail sebagai berikut:

RoadReelsApp.kt

import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton

...

} else if (route?.startsWith(Screen.Detail.name) == true) {
    CenterAlignedTopAppBar(
        title = { Text(stringResource(R.string.bbb_title)) },
        navigationIcon = {
            IconButton(onClick = { navController.popBackStack() }) {
                Icon(
                    Icons.AutoMirrored.Filled.ArrowBack,
                    contentDescription = null
                )
            }
        }
    )
}

Untuk mendukung navigasi kembali dari layar pemutar ke layar utama:

  1. Perbarui composable TopControls untuk mengambil parameter callback bernama onClose dan tambahkan IconButton yang memanggilnya saat diklik.

PlayerControls.kt

import androidx.compose.material.icons.twotone.Close

...

@Composable
fun TopControls(
    title: String?,
    onClose: () -> Unit,
    modifier: Modifier = Modifier
) {
    Box(modifier) {
        IconButton(
            modifier = Modifier
                .align(Alignment.TopStart),
            onClick = onClose
        ) {
            Icon(
                Icons.TwoTone.Close,
                contentDescription = "Close player",
                tint = Color.White
            )
        }

        if (title != null) { ... }
    }
}
  1. Perbarui composable PlayerControls untuk mengambil juga parameter callback onClose dan meneruskannya ke TopControls

PlayerControls.kt

fun PlayerControls(
    uiState: PlayerUiState,
    onClose: () -> Unit,
    onPlayPause: () -> Unit,
    onSeek: (seekToMillis: Long) -> Unit,
    modifier: Modifier = Modifier,
) {
    AnimatedVisibility(
        visible = uiState.isShowingControls,
        enter = fadeIn(),
        exit = fadeOut()
    ) {
        Box(modifier = modifier.background(Color.Black.copy(alpha = .5f))) {
            TopControls(
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(dimensionResource(R.dimen.screen_edge_padding))
                    .align(Alignment.TopCenter),
                title = uiState.mediaMetadata.title?.toString(),
                onClose = onClose
            )
            ...
        }
    }
}
  1. Selanjutnya, perbarui composable PlayerScreen untuk mengambil parameter yang sama, dan teruskan ke PlayerControls-nya.

PlayerScreen.kt

@Composable
fun PlayerScreen(
    onClose: () -> Unit,
    modifier: Modifier = Modifier,
    viewModel: PlayerViewModel = viewModel()
) {
    ...

    PlayerControls(
        modifier = Modifier
            .fillMaxSize(),
        uiState = playerUiState,
        onClose = onClose,
        onPlayPause = { if (playerUiState.isPlaying) viewModel.pause() else viewModel.play() },
        onSeek = viewModel::seekTo
    )
}
  1. Terakhir, di RoadReelsNavHost, berikan implementasi yang akan diteruskan ke PlayerScreen:

RoadReelsNavHost.kt

composable(route = Screen.Player.name, ...) {
    PlayerScreen(onClose = { navController.popBackStack() })
}

Sekarang pengguna dapat berpindah antarlayar tanpa terhenti. Selain itu, pengalaman pengguna mungkin juga lebih baik pada faktor bentuk lainnya – misalnya, pada ponsel yang tinggi ketika tangan pengguna sudah berada di dekat bagian atas layar, mereka dapat dengan lebih mudah menavigasi aplikasi tanpa perlu memindahkan perangkat di tangan mereka.

46cf7ec051b32ddf.gif

Menyesuaikan dengan dukungan orientasi layar

Tidak seperti kebanyakan perangkat seluler, sebagian besar mobil memiliki orientasi tetap. Artinya, mobil tersebut mendukung orientasi lanskap atau potret, tetapi tidak keduanya, karena layarnya tidak dapat diputar. Oleh karena itu, aplikasi harus menghindari asumsi bahwa kedua orientasi didukung.

Dalam Create an Android Automotive OS manifest, Anda menambahkan dua elemen <uses-feature> untuk fitur android.hardware.screen.portrait dan android.hardware.screen.landscape dengan atribut required yang ditetapkan ke false. Tindakan tersebut memastikan bahwa tidak ada dependensi fitur yang implisit pada salah satu orientasi layar yang dapat mencegah aplikasi didistribusikan ke mobil. Akan tetapi, elemen manifes tersebut tidak mengubah perilaku aplikasi, hanya cara pendistribusiannya.

Saat ini, aplikasi memiliki fitur berguna yang otomatis menetapkan orientasi aktivitas ke lanskap saat pemutar video terbuka, sehingga pengguna ponsel tidak perlu mengutak-atik perangkat mereka untuk mengubah orientasinya jika belum berupa lanskap.

Sayangnya, perilaku yang sama dapat menyebabkan loop berkedip atau tampilan lebar di perangkat dengan orientasi potret tetap, seperti yang dimiliki banyak mobil laik jalan saat ini.

Untuk memperbaikinya, Anda dapat menambahkan pemeriksaan berdasarkan orientasi layar yang didukung perangkat saat ini.

  1. Untuk menyederhanakan implementasinya, tambahkan terlebih dahulu kode berikut di Extensions.kt:

Extensions.kt

import android.content.Context
import android.content.pm.PackageManager

...

enum class SupportedOrientation {
    Landscape,
    Portrait,
}

fun Context.supportedOrientations(): List<SupportedOrientation> {
    return when (Pair(
        packageManager.hasSystemFeature(PackageManager.FEATURE_SCREEN_LANDSCAPE),
        packageManager.hasSystemFeature(PackageManager.FEATURE_SCREEN_PORTRAIT)
    )) {
        Pair(true, false) -> listOf(SupportedOrientation.Landscape)
        Pair(false, true) -> listOf(SupportedOrientation.Portrait)
        // For backwards compat, if neither feature is declared, both can be assumed to be supported
        // 
        else -> listOf(SupportedOrientation.Landscape, SupportedOrientation.Portrait)
    }
}
  1. Kemudian, simpan panggilan untuk menetapkan orientasi yang diminta. Karena aplikasi dapat mengalami masalah serupa dalam mode multi-aplikasi di perangkat seluler, Anda juga dapat menyertakan pemeriksaan agar tidak menyetel orientasi secara dinamis dalam kasus tersebut.

PlayerScreen.kt

import com.example.android.cars.roadreels.SupportedOrientation
import com.example.android.cars.roadreels.supportedOrientations

...

DisposableEffect(Unit) {
    ...

    // Only automatically set the orientation to landscape if the device supports landscape.
    // On devices that are portrait only, the activity may enter a compat mode and won't get to
    // use the full window available if so. The same applies if the app's window is portrait
    // in multi-window mode.
    if (activity.supportedOrientations().contains(SupportedOrientation.Landscape)
        && !activity.isInMultiWindowMode
    ) {
        activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
    }

    ...
}

Layar pemutar memasuki loop berkedip pada emulator Polestar 2 sebelum menambahkan pemeriksaan (saat aktivitas tidak menangani perubahan konfigurasi orientasi)

Layar pemutar memiliki tampilan lebar pada emulator Polestar 2 sebelum menambahkan pemeriksaan (saat aktivitas menangani perubahan konfigurasi orientasi)

Layar pemutar tidak memiliki tampilan lebar pada emulator Polestar 2 setelah menambahkan pemeriksaan.

Layar pemutar memasuki loop berkedip pada emulator Polestar 2 sebelum menambahkan pemeriksaan (saat aktivitas tidak menangani perubahan konfigurasi orientation)

Layar pemutar memiliki tampilan lebar pada emulator Polestar 2 sebelum menambahkan pemeriksaan (saat aktivitas menangani perubahan konfigurasi orientation)

Layar pemutar tidak memiliki tampilan lebar pada emulator Polestar 2 setelah menambahkan pemeriksaan

Karena tempat ini adalah satu-satunya lokasi di aplikasi yang menetapkan orientasi layar, aplikasi kini menghindari tampilan lebar. Di aplikasi Anda sendiri, periksa apakah ada atribut screenOrientation atau panggilan setRequestedOrientation yang hanya ditujukan untuk orientasi lanskap atau potret (termasuk sensor, reverse, dan varian user), lalu hapus atau simpan jika diperlukan untuk membatasi tampilan lebar. Untuk mengetahui detail selengkapnya, lihat Mode kompatibilitas perangkat.

Menyesuaikan dengan kemampuan kontrol kolom sistem

Sayangnya, meskipun perubahan sebelumnya memastikan aplikasi tidak memasuki loop berkedip atau membuat tampilan lebar, perubahan ini juga mengekspos asumsi lain yang rusak – yaitu bahwa kolom sistem selalu dapat disembunyikan. Karena pengguna memiliki kebutuhan yang berbeda saat menggunakan mobil (dibandingkan saat menggunakan ponsel atau tablet), OEM memiliki opsi untuk mencegah aplikasi menyembunyikan kolom sistem guna memastikan bahwa kontrol kendaraan, seperti pengontrol kondisi udara, selalu dapat diakses di layar.

Akibatnya, ada potensi bagi aplikasi untuk merender di belakang kolom sistem saat aplikasi tersebut merender dalam mode imersif dan menganggap bahwa kolom tersebut dapat disembunyikan. Anda dapat melihatnya di langkah sebelumnya, karena kontrol pemutar atas dan bawah tidak lagi terlihat saat aplikasi tidak dijadikan tampilan lebar. Dalam contoh khusus ini, aplikasi tidak lagi dapat dijelajahi karena tombol untuk menutup pemutar terhalang dan fungsinya terhambat karena bilah geser tidak dapat digunakan.

Perbaikan yang termudah adalah menerapkan padding inset jendela systemBars ke pemutar seperti berikut:

PlayerScreen.kt

import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.windowInsetsPadding

...

Box(
    modifier = Modifier
        .fillMaxSize()
        .windowInsetsPadding(WindowInsets.systemBars)
) {
    PlayerView(...)
    PlayerControls(...)
}

Namun, solusi ini tidak ideal karena menyebabkan elemen UI berpindah-pindah ketika kolom sistem bergerak.

9fa1de6d2518340a.gif

Untuk meningkatkan pengalaman pengguna, Anda dapat mengupdate aplikasi guna melacak inset mana yang dapat dikontrol dan menerapkan padding hanya untuk inset yang tidak dapat dikontrol.

  1. Karena layar lain dalam aplikasi mungkin ingin mengontrol inset jendela, sebaiknya teruskan inset yang dapat dikontrol sebagai CompositionLocal. Buat file baru, LocalControllableInsets.kt, di paket com.example.android.cars.roadreels dan tambahkan kode berikut:

LocalControllableInsets.kt

import androidx.compose.runtime.compositionLocalOf

// Assume that no insets can be controlled by default
const val DEFAULT_CONTROLLABLE_INSETS = 0
val LocalControllableInsets = compositionLocalOf { DEFAULT_CONTROLLABLE_INSETS }
  1. Siapkan OnControllableInsetsChangedListener untuk memproses perubahan.

MainActivity.kt

import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsControllerCompat.OnControllableInsetsChangedListener

...

class MainActivity : ComponentActivity() {
    private lateinit var onControllableInsetsChangedListener: OnControllableInsetsChangedListener

    @OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        enableEdgeToEdge()

        setContent {
            var controllableInsetsTypeMask by remember { mutableIntStateOf(DEFAULT_CONTROLLABLE_INSETS) }

            onControllableInsetsChangedListener =
                OnControllableInsetsChangedListener { _, typeMask ->
                    if (controllableInsetsTypeMask != typeMask) {
                        controllableInsetsTypeMask = typeMask
                    }
                }

            WindowCompat.getInsetsController(window, window.decorView)
                .addOnControllableInsetsChangedListener(onControllableInsetsChangedListener)

            RoadReelsTheme {
                RoadReelsApp(calculateWindowSizeClass(this))
            }
        }
    }

    override fun onDestroy() {
        super.onDestroy()

        WindowCompat.getInsetsController(window, window.decorView)
            .removeOnControllableInsetsChangedListener(onControllableInsetsChangedListener)
    }
}
  1. Tambahkan CompositionLocalProvider tingkat teratas yang berisi composable tema dan aplikasi, juga yang mengikat nilai ke LocalControllableInsets.

MainActivity.kt

import androidx.compose.runtime.CompositionLocalProvider

...

CompositionLocalProvider(LocalControllableInsets provides controllableInsetsTypeMask) {
    RoadReelsTheme {
        RoadReelsApp(calculateWindowSizeClass(this))
    }
}
  1. Pada pemutar, baca nilai saat ini dan gunakan untuk menentukan inset untuk disembunyikan dan yang akan digunakan untuk padding.

PlayerScreen.kt

import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.union
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.unit.dp
import com.example.android.cars.roadreels.LocalControllableInsets

...

val controllableInsetsTypeMask by rememberUpdatedState(LocalControllableInsets.current)

DisposableEffect(Unit) {
    ...
    windowInsetsController.hide(WindowInsetsCompat.Type.systemBars().and(controllableInsetsTypeMask))
    ...
}

...

// When the system bars can be hidden, ignore them when applying padding to the player and
// controls so they don't jump around as the system bars disappear. If they can't be hidden
// include them so nothing renders behind the system bars
var windowInsetsForPadding = WindowInsets(0.dp)
if (controllableInsetsTypeMask.and(WindowInsetsCompat.Type.statusBars()) == 0) {
    windowInsetsForPadding = windowInsetsForPadding.union(WindowInsets.statusBars)
}
if (controllableInsetsTypeMask.and(WindowInsetsCompat.Type.navigationBars()) == 0) {
    windowInsetsForPadding = windowInsetsForPadding.union(WindowInsets.navigationBars)
}

Box(
    modifier = Modifier
        .fillMaxSize()
        .windowInsetsPadding(windowInsetsForPadding)
) {
    PlayerView(...)
    PlayerControls(...)
}

Konten tetap terlihat ketika kolom sistem tidak dapat disembunyikan

Konten tidak berpindah-pindah ketika kolom sistem dapat disembunyikan

Konten tetap terlihat ketika kolom sistem tidak dapat disembunyikan

Konten tampak lebih bagus dan tidak akan berpindah-pindah. Selain itu, kontrol sepenuhnya terlihat, bahkan di mobil yang kolom sistemnya tidak dapat dikontrol.

7. Memenuhi persyaratan kualitas Android Automotive OS: Gangguan bagi pengemudi

Terakhir, ada satu perbedaan utama antara mobil dan faktor bentuk lainnya, yaitu bahwa mobil digunakan untuk mengemudi. Oleh karena itu, membatasi gangguan saat berkendara sangatlah penting. Semua aplikasi parkir untuk Android Automotive OS harus menjeda pemutaran saat batasan pengalaman pengguna aktif dan mencegah kelanjutan pemutaran saat batasan pengalaman pengguna aktif. Overlay sistem muncul saat batasan pengalaman pengguna aktif, dan pada gilirannya, peristiwa siklus proses onPause dipanggil untuk aplikasi yang ditempatkan. Selama panggilan ini, aplikasi harus menjeda pemutaran.

Simulasi mengemudi

Buka tampilan pemutar di emulator dan mulai memutar konten. Kemudian, ikuti langkah-langkah untuk simulasi mengemudi dan perhatikan bahwa saat UI aplikasi terhalang oleh sistem, pemutaran tidak dijeda. Hal ini melanggar pedoman kualitas aplikasi mobil DD-2.

c2eda16df688c102.png

Menjeda pemutaran ketika mulai mengemudi

  1. Tambahkan dependensi pada artefak androidx.lifecycle:lifecycle-runtime-compose, yang berisi LifecycleEventEffect yang membantu menjalankan kode pada peristiwa siklus proses.

libs.version.toml

androidx-lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycle" }

build.gradle.kts (Module :app)

implementation(libs.androidx.lifecycle.runtime.compose)
  1. Setelah menyinkronkan project untuk mendownload dependensi, tambahkan LifecycleEventEffect yang berjalan di peristiwa ON_PAUSE untuk menjeda pemutaran (dan secara opsional di peristiwa ON_RESUME untuk melanjutkan pemutaran).

PlayerScreen.kt

import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.LifecycleEventEffect

...

@Composable
fun PlayerScreen(...) {
    ...
    LifecycleEventEffect(Lifecycle.Event.ON_PAUSE) {
        viewModel.pause()
    }

    LifecycleEventEffect(Lifecycle.Event.ON_RESUME) {
        viewModel.play()
    }
    ...
}

Setelah perbaikan diterapkan, ikuti langkah yang sama seperti yang Anda lakukan sebelumnya untuk simulasi mengemudi selama pemutaran aktif, dan perhatikan bahwa pemutaran berhenti, yang memenuhi persyaratan DD-2.

8. Menguji aplikasi di emulator distant display

Konfigurasi baru yang mulai muncul di mobil adalah penyiapan dua layar dengan layar utama di konsol tengah dan layar sekunder tinggi di dasbor, dekat kaca depan. Aplikasi dapat dipindahkan dari layar tengah ke layar sekunder, lalu kembali lagi untuk memberikan lebih banyak opsi kepada pengemudi dan penumpang.

Menginstal image Automotive Distant Display

  1. Pertama, buka SDK Manager di Pratinjau Android Studio, lalu pilih tab SDK Platforms jika belum dipilih. Di pojok kanan bawah jendela SDK Manager, pastikan kotak Show package details dicentang.
  2. Instal image emulator Automotive Distant Display with Google Play menggunakan API 33 untuk arsitektur komputer Anda (x86/ARM).

Membuat Perangkat Virtual Android untuk Android Automotive OS

  1. Setelah membuka Pengelola Perangkat, pilih Automotive di kolom Category di sisi kiri jendela. Kemudian, pilih profil hardware paket Automotive Distant Display with Google Play dari daftar, lalu klik Next.
  2. Di halaman berikutnya, pilih image sistem dari langkah sebelumnya. Klik Next dan pilih opsi lanjutan yang Anda inginkan sebelum akhirnya membuat AVD dengan mengklik Finish.

Menjalankan aplikasi

Jalankan aplikasi di emulator yang baru saja Anda buat menggunakan konfigurasi run app yang ada. Ikuti petunjuk di Menggunakan emulator distant display untuk memindahkan aplikasi ke dan dari distant display. Uji pemindahan aplikasi saat berada di layar utama/detail dan saat berada di layar pemutar, serta saat mencoba berinteraksi dengan aplikasi di kedua layar.

b277bd18a94e9c1b.png

9. Meningkatkan pengalaman aplikasi di distant display

Saat menggunakan aplikasi di distant display, Anda mungkin memperhatikan dua hal:

  1. Pemutaran terganggu saat aplikasi dipindahkan ke dan dari distant display.
  2. Anda tidak dapat berinteraksi dengan aplikasi saat berada di distant display, termasuk mengubah status pemutaran.

Meningkatkan kontinuitas aplikasi

Gangguan pemutaran disebabkan oleh aktivitas yang dibuat ulang karena perubahan konfigurasi. Karena aplikasi ditulis menggunakan Compose dan konfigurasi yang berubah terkait dengan ukuran, Compose dapat menangani perubahan konfigurasi untuk Anda dengan membatasi pembuatan ulang aktivitas untuk perubahan konfigurasi berbasis ukuran. Hal ini membuat transisi antarlayar berjalan lancar, tanpa penghentian dalam pemutaran atau pemuatan ulang karena pembuatan ulang aktivitas.

AndroidManifest.xml

<activity
    android:name="com.example.android.cars.roadreels.MainActivity"
    ...
    android:configChanges="screenSize|smallestScreenSize|orientation|screenLayout|density">
        ...
</activity>

Menerapkan kontrol pemutaran

Untuk memperbaiki masalah ketika aplikasi tidak dapat dikontrol saat berada di distant display, Anda dapat menerapkan MediaSession. Sesi media menyediakan cara universal untuk berinteraksi dengan pemutar audio atau video. Untuk informasi selengkapnya, lihat Mengontrol dan memberitahukan pemutaran menggunakan MediaSession.

  1. Menambahkan dependensi pada artefak androidx.media3:media3-session

libs.version.toml

androidx-media3-mediasession = { group = "androidx.media3", name = "media3-session", version.ref = "media3" }

build.gradle.kts (Module :app)

implementation(libs.androidx.media3.mediasession)
  1. Di PlayerViewModel, tambahkan variabel untuk menyimpan sesi media dan buat MediaSession menggunakan builder-nya.

PlayerViewModel.kt

import androidx.media3.session.MediaSession
...

class PlayerViewModel(...) {
    ...

    private var mediaSession: MediaSession? = null
    
    init {
        viewModelScope.launch {
            _player.onEach { player ->
                playerUiStateUpdateJob?.cancel()
                mediaSession?.release()

                if (player != null) {
                    initializePlayer(player)
                    mediaSession = MediaSession.Builder(application, player).build()
                    playerUiStateUpdateJob = viewModelScope.launch {... }
                }
            }.collect()
        }
    }
}
  1. Kemudian, tambahkan baris tambahan di metode onCleared untuk merilis MediaSession saat PlayerViewModel tidak lagi diperlukan.

PlayerViewModel.kt

override fun onCleared() {
    super.onCleared()
    mediaSession?.release()
    _player.value?.release()
}
  1. Terakhir, saat berada di layar pemutar (dengan aplikasi di layar utama atau distant display), Anda dapat menguji kontrol media menggunakan perintah adb shell cmd media_session dispatch
# To play content
adb shell cmd media_session dispatch play

# To pause content
adb shell cmd media_session dispatch pause

# To toggle the playing state
adb shell cmd media_session dispatch play-pause

Membatasi kelanjutan pemutaran

Meskipun mendukung MediaSession memungkinkan kontrol pemutaran saat aplikasi berada dalam distant display, hal ini menimbulkan satu masalah baru. Yaitu, pemutaran dapat dilanjutkan saat batasan pengalaman pengguna diterapkan, yang melanggar batasan pedoman kualitas DD-2 (lagi!). Untuk mengujinya sendiri:

  1. Mulai pemutaran
  2. Simulasikan mengemudi
  3. Gunakan perintah media_session dispatch. Perhatikan bahwa pemutaran dilanjutkan meskipun aplikasi disamarkan.

Untuk memperbaikinya, Anda dapat memproses batasan pengalaman pengguna perangkat dan hanya mengizinkan kelanjutan pemutaran saat aplikasi aktif. Hal ini bahkan dapat dilakukan dengan logika yang sama yang digunakan untuk Android Automotive OS dan perangkat seluler.

  1. Dalam file build.gradle modul app, tambahkan kode berikut untuk menyertakan Library Android Automotive dan lakukan sinkronisasi Gradle setelahnya:

build.gradle.kts

android {
    ...
    useLibrary("android.car")
}
  1. Klik kanan paket com.example.android.cars.roadreels, lalu pilih New > Kotlin Class/File. Masukkan RoadReelsPlayer sebagai nama dan klik jenis Class.
  2. Dalam file yang baru saja Anda buat, tambahkan implementasi awal class berikut. Dengan memperluas ForwardingSimpleBasePlayer, Anda dapat dengan mudah mengubah perintah dan interaksi yang didukung untuk pemutar yang digabungkan dengan mengganti metode getState().

RoadReelsPlayer.kt

import android.content.Context
import androidx.media3.common.ForwardingSimpleBasePlayer
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.ExoPlayer

@UnstableApi
class RoadReelsPlayer(context: Context) :
    ForwardingSimpleBasePlayer(ExoPlayer.Builder(context).build()) {
    private var shouldPreventPlay = false

    override fun getState(): State {
        val state = super.getState()

        return state.buildUpon()
            .setAvailableCommands(
                state.availableCommands.buildUpon().removeIf(COMMAND_PLAY_PAUSE, shouldPreventPlay)
                    .build()
            ).build()
    }
}
  1. Di PlayerViewModel.kt, perbarui deklarasi variabel pemain untuk menggunakan instance RoadReelsPlayer, bukan ExoPlayer. Saat ini, perilakunya akan sama persis seperti sebelumnya karena shouldPreventPlay tidak pernah diperbarui dari nilai defaultnya, yaitu false.

PlayerViewModel.kt

init {
    ...
    _player.update { RoadReelsPlayer(application) }
}
  1. Untuk mulai melacak batasan pengalaman pengguna, tambahkan blok init dan implementasi handleRelease berikut:

RoadReelsPlayer.kt

import android.car.Car
import android.car.drivingstate.CarUxRestrictions
import android.car.drivingstate.CarUxRestrictionsManager
import android.content.pm.PackageManager
import com.google.common.util.concurrent.ListenableFuture

...

@UnstableApi
class RoadReelsPlayer(context: Context) :
    ForwardingSimpleBasePlayer(ExoPlayer.Builder(context).build()) {
    ...
    private var pausedByUxRestrictions = false
    private lateinit var carUxRestrictionsManager: CarUxRestrictionsManager

   init {
        with(context) {
            // Only listen to UX restrictions if the device is running Android Automotive OS
            if (packageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)) {
                val car = Car.createCar(context)
                carUxRestrictionsManager =
                    car.getCarManager(Car.CAR_UX_RESTRICTION_SERVICE) as CarUxRestrictionsManager

                // Get the initial UX restrictions and update the player state
                shouldPreventPlay =
                    carUxRestrictionsManager.currentCarUxRestrictions.isRequiresDistractionOptimization
                invalidateState()

                // Register a listener to update the player state as the UX restrictions change
                carUxRestrictionsManager.registerListener { carUxRestrictions: CarUxRestrictions ->
                    shouldPreventPlay = carUxRestrictions.isRequiresDistractionOptimization

                    if (!shouldPreventPlay && pausedByUxRestrictions) {
                        handleSetPlayWhenReady(true)
                        invalidateState()
                    } else if (shouldPreventPlay && isPlaying) {
                        pausedByUxRestrictions = true
                        handleSetPlayWhenReady(false)
                        invalidateState()
                    }
                }
            }

            addListener(object : Player.Listener {
                override fun onEvents(player: Player, events: Player.Events) {
                    if (events.contains(EVENT_IS_PLAYING_CHANGED) && isPlaying) {
                        pausedByUxRestrictions = false
                    }
                }
            })
        }
    }

    ...

    override fun handleRelease(): ListenableFuture<*> {
        if (::carUxRestrictionsManager.isInitialized) {
            carUxRestrictionsManager.unregisterListener()
        }
        return super.handleRelease()
    }
}

Ada beberapa hal yang perlu diperhatikan di sini:

  • CarUxRestrictionsManager disimpan sebagai variabel lateinit karena tidak dibuat instance-nya atau digunakan di perangkat non-Android Automotive OS, tetapi pemrosesnya harus dihapus saat pemutar dirilis.
  • Hanya nilai isRequiresDistractionOptimization yang dirujuk saat menentukan status batasan UX. Meskipun class CarUxRestrictions berisi detail tambahan tentang pembatasan yang aktif, Anda tidak perlu mereferensikannya karena hanya ditujukan untuk digunakan oleh aplikasi distraksi dioptimalkan (seperti aplikasi navigasi), karena aplikasi tersebut akan terus terlihat saat batasan aktif.
  • Setelah pembaruan pada variabel shouldPreventPlay, invalidateState() dipanggil untuk memberi tahu konsumen tentang status perubahan pemutar.
  • Di pemroses itu sendiri, pemutaran otomatis dijeda atau dilanjutkan dengan memanggil handleSetPlayWhenReady dengan nilai yang sesuai.
  1. Sekarang, uji lanjutkan pemutaran saat menyimulasikan mengemudi seperti yang dijelaskan di awal bagian ini dan perhatikan bahwa pemutaran tidak dilanjutkan.
  2. Terakhir, karena menjeda pemutaran saat batasan pengalaman pengguna aktif ditangani oleh RoadReelsPlayer, LifecycleEventEffect tidak perlu menjeda pemutar selama ON_PAUSE. Sebagai gantinya, hal ini dapat diubah menjadi ON_STOP, sehingga pemutaran berhenti saat pengguna keluar dari aplikasi untuk membuka peluncur atau membuka aplikasi lain.

PlayerScreen.kt

LifecycleEventEffect(Lifecycle.Event.ON_START) {
    viewModel.play()
}
LifecycleEventEffect(Lifecycle.Event.ON_STOP) {
    viewModel.pause()
}

Rangkuman

Dengan demikian, aplikasi ini berfungsi jauh lebih baik di mobil, baik dengan maupun tanpa distant display. Namun lebih dari itu, aplikasi juga berfungsi lebih baik pada faktor bentuk lainnya. Pada perangkat yang dapat memutar layar atau memungkinkan pengguna mengubah ukuran jendela aplikasi, aplikasi kini juga beradaptasi dengan lancar dalam situasi tersebut.

Selain itu, berkat integrasi sesi media, pemutaran aplikasi dapat dikontrol tidak hanya oleh kontrol hardware dan software di mobil, tetapi juga oleh sumber lain, seperti kueri Asisten Google atau tombol jeda pada perangkat headphone, sehingga pengguna memiliki lebih banyak opsi untuk mengontrol aplikasi di berbagai faktor bentuk.

10. Menguji aplikasi dengan konfigurasi sistem yang berbeda

Karena aplikasi berfungsi dengan baik di layar utama dan distant display, hal terakhir yang harus diperiksa adalah cara aplikasi menangani konfigurasi kolom sistem dan potongan layar yang berbeda. Seperti dijelaskan dalam Menggunakan inset jendela dan potongan layar, perangkat Android Automotive OS mungkin memiliki konfigurasi yang merusak asumsi yang umumnya berlaku pada faktor bentuk seluler.

Di bagian ini, Anda akan mempelajari cara mengonfigurasi emulator agar memiliki kolom sistem sebelah kiri, dan menguji aplikasi dalam konfigurasi tersebut.

Mengonfigurasi kolom sistem samping

Seperti dijelaskan dalam Pengujian menggunakan emulator yang dapat dikonfigurasi, ada berbagai opsi untuk mengemulasi berbagai konfigurasi sistem yang ada di mobil.

Untuk tujuan codelab ini, com.android.systemui.rro.left dapat digunakan untuk menguji berbagai konfigurasi kolom sistem. Untuk mengaktifkan kode tersebut, gunakan perintah berikut:

adb shell cmd overlay enable --user 0 com.android.systemui.rro.left

b642703a7278b219.png

Karena aplikasi menggunakan pengubah systemBars sebagai contentWindowInsets dalam Scaffold, konten sudah digambar di area yang aman di kolom sistem. Untuk melihat apa yang akan terjadi jika aplikasi mengasumsikan bahwa kolom sistem hanya muncul di bagian atas dan bawah layar, ubah parameter tersebut menjadi berikut:

RoadReelsApp.kt

contentWindowInsets = if (route?.equals(Screen.Player.name) == true) WindowInsets(0.dp) else WindowInsets.systemBars.only(WindowInsetsSides.Vertical)

Maaf. Layar daftar dan detail dirender di belakang kolom sistem. Berkat pekerjaan sebelumnya, layar pemutar akan berfungsi dengan baik, meskipun kolom sistem tidak dapat dikontrol setelahnya.

9898f7298a7dfb4.gif

Sebelum melanjutkan ke bagian berikutnya, pastikan untuk mengembalikan perubahan yang baru saja Anda buat pada parameter windowContentPadding.

11. Menggunakan potongan layar

Terakhir, beberapa mobil memiliki layar dengan potongan layar yang sangat berbeda jika dibandingkan dengan yang terlihat di perangkat seluler. Sebagai ganti potongan kamera pinhole atau notch, beberapa kendaraan Android Automotive OS memiliki layar melengkung yang membuat layar menjadi non-persegi panjang.

Untuk melihat bagaimana perilaku aplikasi ketika potongan layar seperti itu ada, pertama-tama aktifkan potongan layar menggunakan perintah berikut:

adb shell cmd overlay enable --user 0 com.android.internal.display.cutout.emulation.top_and_right

Untuk benar-benar menguji seberapa baik perilaku aplikasi, aktifkan juga kolom sistem sebelah kiri yang digunakan di bagian terakhir, jika belum diaktifkan:

adb shell cmd overlay enable --user 0 com.android.systemui.rro.left

Sama halnya, aplikasi tidak merender ke potongan layar (bentuk persis potongan tersebut sulit diketahui saat ini, tetapi akan menjadi jelas di langkah berikutnya). Hal ini tidak menjadi masalah dan memberikan pengalaman yang lebih baik daripada aplikasi yang merender ke potongan, tetapi tidak beradaptasi dengan hati-hati.

935aa1d4ee3eb72.png

Merender ke potongan layar

Untuk memberi pengalaman yang paling imersif kepada pengguna, Anda dapat memanfaatkan ruang layar yang lebih luas dengan merender ke potongan layar.

  1. Untuk merender ke potongan layar, buat file integers.xml guna menyimpan penggantian khusus untuk mobil. Untuk melakukannya, gunakan penentu mode UI dengan nilai Dok Mobil (nama ini adalah peninggalan dari saat hanya Android Auto yang ada, tetapi kini juga digunakan Android Automotive OS). Selain itu, karena nilai yang akan Anda gunakan, LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS, diperkenalkan di Android R, tambahkan juga penentu Versi Android dengan nilai 30. Lihat Menggunakan resource alternatif untuk mengetahui detail selengkapnya.

22b7f17657cac3fd.png

  1. Dalam file yang baru saja Anda buat (res/values-car-v30/integers.xml), tambahkan kode berikut:

integers.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <integer name="windowLayoutInDisplayCutoutMode">3</integer>
</resources>

Nilai bilangan bulat 3 sesuai dengan LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS dan menggantikan nilai default 0 dari res/values/integers.xml, yang sesuai dengan LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT. Nilai bilangan bulat ini sudah direferensikan di MainActivity.kt untuk mengganti mode yang ditetapkan oleh enableEdgeToEdge(). Untuk mengetahui informasi selengkapnya tentang atribut ini, lihat dokumentasi referensi.

Sekarang, saat Anda menjalankan aplikasi, perhatikan bahwa konten meluas ke potongan dan terlihat sangat imersif. Namun, panel aplikasi atas dan beberapa konten terhalang sebagian oleh potongan layar, menyebabkan masalah yang mirip dengan yang terjadi saat aplikasi menganggap kolom sistem hanya akan muncul di bagian atas dan bawah.

1d791b9e2ec91bda.png

Memperbaiki panel aplikasi atas

Untuk memperbaiki panel aplikasi atas, Anda dapat menambahkan parameter windowInsets berikut ke Composable CenterAlignedTopAppBar:

RoadReelsApp.kt

import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.safeDrawing

...

windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal + WindowInsetsSides.Top)

Karena safeDrawing terdiri dari inset displayCutout dan systemBars, hal ini meningkatkan parameter windowInsets default, yang hanya menggunakan systemBars saat memosisikan panel aplikasi atas.

Selain itu, karena panel aplikasi atas diposisikan di bagian atas jendela, Anda tidak boleh menyertakan komponen bawah inset safeDrawing karena hal ini berpotensi menambahkan padding yang tidak perlu.

21d9a237572f85c2.png

Memperbaiki layar utama

Salah satu opsi untuk memperbaiki konten di layar utama dan layar detail adalah menggunakan safeDrawing, bukan systemBars untuk contentWindowInsets dari Scaffold. Namun, aplikasi terlihat kurang imersif menggunakan opsi tersebut, dengan konten yang tiba-tiba terpotong saat potongan layar dimulai – tidak lebih baik dibandingkan jika aplikasi tidak dirender ke potongan layar sama sekali.

80bca44f0962a4a1.png

Untuk antarmuka pengguna yang lebih imersif, Anda dapat menangani inset pada setiap komponen dalam layar.

  1. Perbarui contentWindowInsets dari Scaffold agar selalu menjadi 0 dp (bukan hanya untuk PlayerScreen). Hal ini memungkinkan setiap layar dan/atau komponen dalam layar menentukan perilakunya terkait inset.

RoadReelsApp.kt

Scaffold(
    ...,
    contentWindowInsets = WindowInsets(0.dp)
) { ... }
  1. Setel windowInsetsPadding dari composable Text header baris untuk menggunakan komponen horizontal inset safeDrawing. Komponen atas inset ini ditangani oleh panel aplikasi atas, dan komponen bawah akan ditangani nanti.

MainScreen.kt

import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.windowInsetsPadding

...

LazyColumn(
    contentPadding = PaddingValues(bottom = dimensionResource(R.dimen.screen_edge_padding))
) {
    items(NUM_ROWS) { rowIndex: Int ->
        Text(
            "Row $rowIndex",
            style = MaterialTheme.typography.headlineSmall,
            modifier = Modifier
                .padding(
                    horizontal = dimensionResource(R.dimen.screen_edge_padding),
                    vertical = dimensionResource(R.dimen.row_header_vertical_padding)
                )
                .windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal))
        )
    ...
}
  1. Hapus parameter contentPadding dari LazyRow. Kemudian, di awal dan akhir setiap LazyRow, tambahkan Spacer selebar komponen safeDrawing yang sesuai untuk memastikan semua thumbnail dapat dilihat sepenuhnya. Gunakan pengubah widthIn untuk memastikan pengatur jarak ini setidaknya selebar padding konten. Tanpa elemen ini, item di awal dan akhir baris mungkin akan terhalang di belakang kolom sistem dan/atau potongan layar, bahkan saat digeser sepenuhnya ke awal/akhir baris.

MainScreen.kt

import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.layout.windowInsetsEndWidth
import androidx.compose.foundation.layout.windowInsetsStartWidth

...

LazyRow(
    horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.list_item_spacing)),
) {
    item {
        Spacer(
            Modifier
                .windowInsetsStartWidth(WindowInsets.safeDrawing)
                .widthIn(min = dimensionResource(R.dimen.screen_edge_padding))
        )
    }
    items(NUM_ITEMS_PER_ROW) { ... }
    item {
        Spacer(
            Modifier
                .windowInsetsEndWidth(WindowInsets.safeDrawing)
                .widthIn(min = dimensionResource(R.dimen.screen_edge_padding))
        )
    }
}
  1. Terakhir, tambahkan Spacer di akhir LazyColumn untuk memperhitungkan setiap kolom sistem atau inset potongan layar yang potensial di bagian bawah layar. Pengatur jarak yang setara di bagian atas LazyColumn tidak diperlukan karena panel aplikasi atas akan menanganinya. Jika aplikasi menggunakan panel aplikasi bawah, bukan panel aplikasi atas, Anda perlu menambahkan Spacer di awal daftar menggunakan pengubah windowInsetsTopHeight. Dan jika aplikasi menggunakan panel aplikasi atas dan bawah, tidak ada pengatur jarak yang diperlukan.

MainScreen.kt

import androidx.compose.foundation.layout.windowInsetsBottomHeight

...

LazyColumn(...){
    items(NUM_ROWS) { ... }
    item {
        Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.safeDrawing))
    }
}

Panel aplikasi atas sepenuhnya terlihat dan, saat men-scroll ke akhir baris, Anda kini dapat melihat semua thumbnail secara keseluruhan.

b437a762e31abd02.png

Memperbaiki layar detail

f622958a8d0c16c8.png

Layar detail tidak terlalu buruk, tetapi konten masih terpotong.

Karena layar detail tidak memiliki konten yang dapat di-scroll, yang diperlukan untuk memperbaikinya adalah menambahkan pengubah windowInsetsPadding pada Box tingkat atas.

DetailScreen.kt

import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.windowInsetsPadding

...

Box(
    modifier = modifier
        .padding(dimensionResource(R.dimen.screen_edge_padding))
        .windowInsetsPadding(WindowInsets.safeDrawing)
) { ... }

adf17e27b576ec5a.png

Memperbaiki layar pemutar

Meskipun PlayerScreen sudah menerapkan padding untuk beberapa atau semua inset jendela kolom sistem di bagian Memenuhi persyaratan kualitas Android Automotive OS: Kemampuan Navigasi, hal itu tidak cukup untuk memastikan bahwa sekarang layar pemutar tidak tertutup sehingga aplikasi dirender ke potongan layar. Pada perangkat seluler, potongan layar hampir selalu sepenuhnya berada dalam kolom sistem. Namun, di mobil, potongan layar mungkin jauh melampaui kolom sistem, sehingga melanggar asumsi.

fc14798bc71110d3.png

Untuk memperbaikinya, cukup ubah nilai awal variabel windowInsetsForPadding dari nilai nol menjadi displayCutout:

PlayerScreen.kt

import androidx.compose.foundation.layout.displayCutout

...

var windowInsetsForPadding = WindowInsets(WindowInsets.displayCutout)

cce55d3f8129935d.png

Bagus, aplikasi ini benar-benar mengoptimalkan layar sekaligus tetap dapat digunakan.

Selain itu, jika Anda menjalankan aplikasi di perangkat seluler, aplikasi tersebut juga akan lebih imersif. Item daftar dirender hingga ke tepi layar, termasuk di belakang menu navigasi.

dc7918499a33df31.png

12. Selamat

Anda berhasil memigrasikan dan mengoptimalkan aplikasi parkir pertama Anda. Sekarang saatnya menggunakan yang telah Anda pelajari dan menerapkannya ke aplikasi Anda sendiri.

Untuk dicoba

Bacaan lebih lanjut