Tạo và kiểm thử một ứng dụng đỗ xe dành cho Android Automotive OS

1. Trước khi bắt đầu

Không bao gồm

  • Hướng dẫn về cách tạo ứng dụng đa phương tiện (âm thanh, ví dụ: nhạc, đài phát, podcast) cho Android Auto và Android Automotive OS. Xem bài viết Tạo ứng dụng đa phương tiện cho ô tô để biết thông tin về cách tạo loại ứng dụng này.

Bạn cần

Sản phẩm bạn sẽ tạo ra

Trong lớp học lập trình này, bạn sẽ tìm hiểu cách di chuyển một ứng dụng di động phát trực tuyến video hiện có, với tên gọi Road Reels, sang Android Automotive OS.

Phiên bản bắt đầu của ứng dụng này chạy trên một chiếc điện thoại

Phiên bản hoàn chỉnh của ứng dụng này chạy trên một trình mô phỏng Android Automotive OS có vết cắt trên màn hình.

Phiên bản bắt đầu của ứng dụng này chạy trên một chiếc điện thoại

Phiên bản hoàn chỉnh của ứng dụng này chạy trên một trình mô phỏng Android Automotive OS có vết cắt trên màn hình.

Kiến thức bạn sẽ học được

  • Cách sử dụng trình mô phỏng Android Automotive OS.
  • Cách thực hiện các thay đổi cần thiết để tạo một bản dựng Android Automotive OS
  • Những giả định có thể bị phá vỡ khi ứng dụng chạy trên Android Automotive OS, thường được đưa ra khi phát triển các ứng dụng dành cho thiết bị di động
  • Các cấp chất lượng khác nhau dành cho ứng dụng trong ô tô
  • Cách sử dụng phiên phát nội dung nghe nhìn để các ứng dụng khác có thể kiểm soát quá trình phát của ứng dụng
  • Sự khác biệt giữa giao diện người dùng hệ thống và phần lồng ghép cửa sổ trên các thiết bị chạy Android Automotive OS so với các thiết bị di động

2. Bắt đầu thiết lập

Lấy mã nguồn

  1. Bạn có thể tìm thấy đoạn mã dành cho lớp học lập trình này trong thư mục build-a-parked-app trong kho lưu trữ GitHub car-codelabs. Để sao chép đoạn mã đó, hãy chạy lệnh sau:
git clone https://github.com/android/car-codelabs.git
  1. Ngoài ra, bạn có thể tải kho lưu trữ ở dạng định dạng tệp ZIP:

Mở dự án

  • Sau khi chạy Android Studio, hãy nhập dự án, chỉ chọn thư mục build-a-parked-app/start. Thư mục build-a-parked-app/end chứa đoạn mã giải pháp mà bạn có thể tham khảo bất cứ lúc nào nếu gặp khó khăn hoặc đơn giản là xem toàn bộ dự án.

Làm quen với đoạn mã

  • Sau khi mở dự án trong Android Studio, hãy dành chút thời gian để xem qua đoạn mã khởi đầu.

3. Tìm hiểu về ứng dụng đỗ xe dành cho Android Automotive OS

Ứng dụng đỗ xe tạo thành một nhóm nhỏ các danh mục ứng dụng được Android Automotive OS hỗ trợ. Tại thời điểm viết bài, những danh mục này bao gồm các ứng dụng phát video trực tuyến, trình duyệt web và trò chơi. Đây là những ứng dụng phù hợp để dùng trong ô tô do phần cứng hiện có trên các phương tiện này được tích hợp sẵn Google và xe điện thì ngày càng phổ biến hơn. Do vậy, thời điểm xe sạc điện chính là lúc để lái xe và hành khách tương tác với những loại ứng dụng này.

Ô tô có nhiều điểm tương đồng với các thiết bị màn hình lớn khác như máy tính bảng và thiết bị có thể gập lại. Ô tô có màn hình cảm ứng với kích thước, độ phân giải và tỷ lệ khung hình tương tự, và có thể ở hướng dọc hoặc ngang (tuy nhiên, không giống như máy tính bảng, hướng của màn hình ô tô là cố định). Chúng cũng là những thiết bị được kết nối có thể có/không có kết nối mạng. Với tất cả những điều nêu trên, không có gì ngạc nhiên khi các ứng dụng vốn được tối ưu hoá cho màn hình lớn thường tốn ít công sức để mang lại trải nghiệm tuyệt vời cho người dùng trên ô tô.

Giống như màn hình lớn, cũng có các cấp độ chất lượng ứng dụng đối với những ứng dụng trên ô tô:

  • Cấp độ 3 – Dành cho ô tô: Ứng dụng của bạn tương thích với màn hình lớn và có thể dùng khi ô tô đang đỗ. Mặc dù ứng dụng này có thể không có tính năng nào được tối ưu hoá cho ô tô, nhưng người dùng vẫn có thể trải nghiệm ứng dụng giống như trên mọi thiết bị Android có màn hình lớn khác. Các ứng dụng di động đáp ứng những yêu cầu này sẽ đủ điều kiện được phân phối nguyên trạng cho ô tô thông qua chương trình Ứng dụng di động dành cho ô tô.
  • Cấp độ 2 – Được tối ưu hoá cho ô tô: Ứng dụng của bạn mang lại trải nghiệm tuyệt vời trên màn hình ngăn xếp trung tâm của ô tô. Để đạt được điều này, ứng dụng của bạn sẽ áp dụng kỹ thuật dành riêng cho ô tô để bao gồm các chức năng có thể dùng được ở chế độ lái xe hoặc đỗ xe, tuỳ thuộc vào danh mục ứng dụng của bạn.
  • Cấp độ 1 – Khác biệt theo loại ô tô: Ứng dụng của bạn được xây dựng để hoạt động trên nhiều loại phần cứng trong ô tô và có thể điều chỉnh trải nghiệm trong các chế độ lái xe và đỗ xe. Cấp độ này mang đến trải nghiệm tốt nhất cho người dùng, được thiết kế cho nhiều màn hình trong ô tô, chẳng hạn như bảng điều khiển trung tâm, cụm đồng hồ và các màn hình khác (như màn hình toàn cảnh trong nhiều loại ô tô hạng sang).

4. Chạy ứng dụng trong trình mô phỏng Android Automotive OS

Cài đặt Automotive bằng hình ảnh Hệ thống Cửa hàng Play

  1. Trước tiên, hãy mở Trình quản lý SDK trong Android Studio rồi chọn thẻ SDK Platforms (Nền tảng SDK) nếu chưa chọn. Ở góc dưới bên phải của cửa sổ Trình quản lý SDK, hãy đảm bảo rằng hộp Show package details (Hiện thông tin về gói) được chọn.
  2. Cài đặt một trong những hình ảnh của trình mô phỏng Automotive trên Cửa hàng Play nêu trong phần Thêm hình ảnh hệ thống chung. Các hình ảnh hệ thống chỉ chạy được trên máy có cùng cấu trúc (x86/ARM) với chính chúng.

Tạo thiết bị Android ảo Android Automotive OS

  1. Sau khi mở Device Manager (Trình quản lý thiết bị), hãy chọn Automotive trong cột Category (Danh mục) ở bên trái cửa sổ. Sau đó, chọn cấu hình phần cứng theo cụm Automotive (1024p landscape) (Automotive (hướng ngang 1024 pixel)) trong danh sách rồi nhấp vào Next (Tiếp theo).

Trình hướng dẫn Cấu hình thiết bị ảo cho thấy cấu hình phần cứng "Automotive (1024p landscape)" ("Automotive (hướng ngang 1024 pixel)") được chọn.

  1. Trên trang tiếp theo, hãy chọn hình ảnh hệ thống ở bước trước. Nhấp vào Next (Tiếp theo) rồi chọn mọi tuỳ chọn nâng cao mà bạn muốn trước khi tạo AVD bằng cách nhấp vào Finish (Hoàn tất). Lưu ý: Nếu bạn chọn hình ảnh API 30, thì có thể hình ảnh đó nằm trong một thẻ khác không phải là thẻ Đề xuất.

Chạy ứng dụng

Chạy ứng dụng trên trình mô phỏng bạn vừa tạo bằng cấu hình chạy app hiện có. Hãy dùng thử ứng dụng trên nhiều loại màn hình và so sánh cách ứng dụng hoạt động với việc chạy ứng dụng trên trình mô phỏng điện thoại hoặc máy tính bảng.

301e6c0d3675e937.png

5. Tạo bản dựng Android Automotive OS

Mặc dù ứng dụng vẫn hoạt động, nhưng bạn cần thực hiện một vài thay đổi nhỏ để ứng dụng hoạt động tốt trên Android Automotive OS và đáp ứng các yêu cầu để có thể xuất bản trên Cửa hàng Play. Chỉ một số thay đổi có hiệu quả để đưa vào phiên bản ứng dụng dành cho thiết bị di động. Vì vậy, trước tiên, bạn sẽ tạo một biến thể bản dựng Android Automotive OS.

Thêm nhóm phiên bản theo kiểu dáng

Để bắt đầu, hãy thêm một nhóm phiên bản cho kiểu dáng mà bản dựng nhắm mục tiêu bằng cách sửa đổi flavorDimensions trong tệp build.gradle.kts. Sau đó, hãy thêm khối productFlavors và các phiên bản cho từng kiểu dáng (mobileautomotive).

Để biết thêm thông tin, hãy xem phần Định cấu hình phiên bản sản phẩm.

build.gradle.kts (Module :app)

android {
    ...
    flavorDimensions += "formFactor"
    productFlavors {
        create("mobile") {
            // Inform Android Studio to use this flavor as the default (e.g. in the Build Variants tool window)
            isDefault = true
            // Since there is only one flavor dimension, this is optional
            dimension = "formFactor"
        }
        create("automotive") {
            // Since there is only one flavor dimension, this is optional
            dimension = "formFactor"
            // Adding a suffix makes it easier to differentiate builds (e.g. in the Play Console)
            versionNameSuffix = "-automotive"
        }
    }
    ...
}

Sau khi cập nhật tệp build.gradle.kts, bạn sẽ thấy một biểu ngữ ở đầu tệp cho biết rằng "Gradle files have changed since the last project sync. A project sync may be necessary for the IDE to work properly" (Tệp Gradle đã thay đổi kể từ lần đồng bộ hoá dự án gần đây nhất. Cần đồng bộ hoá dự án để IDE hoạt động bình thường). Nhấp vào nút Sync Now (Đồng bộ hoá ngay) trong biểu ngữ đó để Android Studio có thể nhập những thay đổi về cấu hình bản dựng này.

8685bcde6b21901f.png

Tiếp theo, hãy mở cửa sổ công cụ Build Variants (Biến thể bản dựng) từ mục trong trình đơn Build (Bản dựng) > Select Build Variant... (Chọn biến thể bản dựng) rồi chọn biến thể automotiveDebug. Việc này sẽ đảm bảo rằng bạn thấy các tệp của nhóm tài nguyên automotive trong cửa sổ Project (Dự án) và biến thể bản dựng này sẽ được dùng khi chạy ứng dụng thông qua Android Studio.

19e4aa8135553f62.png

Tạo tệp kê khai Android Automotive OS

Tiếp theo, bạn sẽ tạo tệp AndroidManifest.xml cho nhóm tài nguyên automotive. Tệp này chứa các phần tử cần thiết theo yêu cầu của các ứng dụng trên Android Automotive OS.

  1. Trong cửa sổ Project (Dự án), hãy nhấp chuột phải vào mô-đun app. Trên trình đơn thả xuống hiện ra, hãy chọn New > Other > Android Manifest File (Mới > Khác > Tệp kê khai Android)
  2. Trong cửa sổ New Android Component (Thành phần Android mới) mở ra, hãy chọn automotive làm Target Source Set (Nhóm tài nguyên đích) cho tệp mới này. Nhấp vào Finish (Hoàn tất) để tạo tệp.

3fe290685a1026f5.png

  1. Trong tệp AndroidManifest.xml vừa tạo (trong đường dẫn app/src/automotive/AndroidManifest.xml), hãy thêm đoạn mã sau:

AndroidManifest.xml (automotive)

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <!--  https://developer.android.com/training/cars/parked#required-features  -->
    <uses-feature
        android:name="android.hardware.type.automotive"
        android:required="true" />
    <uses-feature
        android:name="android.hardware.wifi"
        android:required="false" />
    <uses-feature
        android:name="android.hardware.screen.portrait"
        android:required="false" />
    <uses-feature
        android:name="android.hardware.screen.landscape"
        android:required="false" />
</manifest>

Bạn phải khai báo lần đầu để tải cấu phần phần mềm bản dựng lên kênh Android Automotive OS trên Play Console. Google Play sử dụng sự hiện diện của tính năng này để chỉ phân phối ứng dụng cho các thiết bị có tính năng android.hardware.type.automotive (tức là ô tô).

Các nội dung khai báo khác là bắt buộc để đảm bảo rằng ứng dụng có thể cài đặt trên nhiều cấu hình phần cứng có trên ô tô. Để biết thêm thông tin, hãy xem Các tính năng bắt buộc của Android Automotive OS.

Đánh dấu ứng dụng là ứng dụng video

Tệp automotive_app_desc.xml là phần siêu dữ liệu cuối cùng cần được thêm vào. Tệp này dùng để khai báo danh mục ứng dụng của bạn trong ngữ cảnh là Android cho Ô tô và độc lập với danh mục mà bạn chọn cho ứng dụng của mình trong Play Console.

  1. Nhấp chuột phải vào mô-đun app, chọn tuỳ chọn New > Android Resource File (Mới > Tệp tài nguyên Android) rồi nhập các giá trị sau đây trước khi nhấp vào OK:
  • Tên tệp: automotive_app_desc.xml
  • Loại tài nguyên: XML
  • Phần tử gốc: automotiveApp
  • Nhóm tài nguyên: automotive
  • Tên thư mục: xml

47ac6bf76ef8ad45.png

  1. Trong tệp đó, hãy thêm phần tử <uses> sau để khai báo rằng ứng dụng của bạn là một ứng dụng 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. Trong tệp AndroidManifest.xml của nhóm tài nguyên automotive (tệp mà bạn vừa thêm các phần tử <uses-feature>), hãy thêm một phần tử <application> trống. Trong đó, hãy thêm phần tử <meta-data> sau đây tham chiếu đến tệp automotive_app_desc.xml mà bạn vừa tạo.

AndroidManifest.xml (automotive)

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    ...

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

Như vậy, bạn đã thực hiện tất cả các thay đổi cần thiết để tạo một bản dựng Android Automotive OS của ứng dụng này!

6. Đáp ứng các yêu cầu về chất lượng của Android Automotive OS: Khả năng điều hướng

Mặc dù việc tạo biến thể bản dựng Android Automotive OS là một phần của quá trình đưa ứng dụng của bạn vào sử dụng trên ô tô, nhưng bạn vẫn cần đảm bảo ứng dụng đó hữu ích và an toàn khi dùng.

Thêm thành phần điều hướng

Khi chạy ứng dụng trong trình mô phỏng Android Automotive OS, có thể bạn nhận thấy rằng không thể quay lại từ màn hình chi tiết về màn hình chính hoặc từ màn hình trình phát về màn hình chi tiết. Không giống như các kiểu dáng khác (có thể yêu cầu nút quay lại hoặc cử chỉ chạm để bật tính năng điều hướng quay lại), các thiết bị chạy Android Automotive OS không có yêu cầu như vậy. Do đó, ứng dụng phải cung cấp các thành phần điều hướng trong giao diện người dùng để đảm bảo người dùng có thể điều hướng mà không gặp phải sự cố bị treo trên một màn hình trong ứng dụng. Yêu cầu này được mã hoá dưới dạng nguyên tắc về chất lượng AN-1.

Để hỗ trợ thao tác điều hướng quay lại từ màn hình chi tiết về màn hình chính, hãy thêm một tham số navigationIcon khác cho CenterAlignedTopAppBar của màn hình chi tiết như sau:

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

...

navigationIcon = {
    IconButton(onClick = { navController.popBackStack() }) {
        Icon(
            Icons.AutoMirrored.Filled.ArrowBack,
            contentDescription = null
        )
    }
}

Cách hỗ trợ thao tác điều hướng quay lại từ màn hình trình phát về màn hình chính:

  1. Cập nhật thành phần kết hợp TopControls để lấy tham số callback (gọi lại) có tên là onClose và thêm IconButton để gọi tham số này khi nhấp vào.

PlayerControls.kt

@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. Cập nhật thành phần kết hợp PlayerControls để lấy cả tham số callback (gọi lại) onClose và truyền tham số này vào TopControls

PlayerControls.kt

fun PlayerControls(
    visible: Boolean,
    playerState: PlayerState,
    onClose: () -> Unit,
    onPlayPause: () -> Unit,
    onSeek: (seekToMillis: Long) -> Unit,
    modifier: Modifier = Modifier,
) {
    AnimatedVisibility(
        visible = visible,
        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 = playerState.mediaMetadata.title?.toString(),
                onClose = onClose
            )
            ...
        }
    }
}
  1. Tiếp theo, hãy cập nhật thành phần kết hợp PlayerScreen để lấy cùng một tham số và truyền tham số đó xuống PlayerControls.

PlayerScreen.kt

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

    PlayerControls(
        modifier = Modifier
            .fillMaxSize(),
        visible = isShowingControls,
        playerState = playerState,
        onClose = onClose,
        onPlayPause = { if (playerState.isPlaying) player.pause() else player.play() },
        onSeek = { player.seekTo(it) }
    )
}
  1. Cuối cùng, trong RoadReelsNavHost, hãy cung cấp phương thức triển khai được truyền đến PlayerScreen:

RoadReelsNavHost.kt

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

Thật tuyệt, giờ đây, người dùng có thể dễ dàng di chuyển giữa các màn hình! Trải nghiệm người dùng cũng có thể được cải thiện trên các kiểu dáng khác. Ví dụ: trên điện thoại có kiểu dáng dài, khi tay người dùng đã ở gần đầu màn hình, họ có thể dễ dàng điều hướng trong ứng dụng hơn mà không cần dùng tay để di chuyển thiết bị.

43122e716eeeeb20.gif

Hỗ trợ điều chỉnh theo hướng màn hình

Khác với phần lớn các thiết bị di động, hầu hết màn hình ô tô đều có hướng cố định. Tức là chúng hỗ trợ chế độ ngang hoặc dọc, nhưng không hỗ trợ cả hai chế độ, vì màn hình của ô tô không xoay được. Do đó, các ứng dụng nên tránh giả định rằng cả hai hướng đều được hỗ trợ.

Trong phần Tạo tệp kê khai Android Automotive OS, bạn đã thêm 2 phần tử <uses-feature> cho các tính năng android.hardware.screen.portraitandroid.hardware.screen.landscape với thuộc tính required được đặt thành false. Thao tác này giúp đảm bảo rằng việc không có tính năng ngầm ẩn nào phụ thuộc vào 1 trong 2 hướng màn hình có thể ngăn ứng dụng được phân phối cho ô tô. Tuy nhiên, các phần tử tệp kê khai đó không thay đổi hành vi của ứng dụng, chỉ thay đổi cách phân phối ứng dụng.

Hiện tại, ứng dụng có một tính năng hữu ích là tự động đặt hướng của hoạt động thành hướng ngang khi trình phát video mở. Nhờ vậy, người dùng điện thoại không phải thao tác với thiết bị để thay đổi hướng nếu hướng hiện tại không phải là hướng ngang.

Rất tiếc, hành vi tương tự đó có thể dẫn đến vòng lặp nhấp nháy hoặc hiệu ứng hòm thư trên các thiết bị cố định ở hướng dọc, trong đó có nhiều ô tô đang lưu thông trên đường hiện nay.

Để khắc phục vấn đề này, bạn có thể thêm một quy trình kiểm tra dựa trên hướng màn hình mà thiết bị hiện tại hỗ trợ.

  1. Để đơn giản hoá quy trình triển khai, trước tiên hãy thêm phần sau vào 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. Sau đó, hãy bảo vệ lệnh gọi để đặt hướng theo yêu cầu. Vì ứng dụng có thể gặp phải vấn đề tương tự ở chế độ nhiều cửa sổ trên thiết bị di động, nên bạn cũng có thể tiến hành kiểm tra để không tự động đặt hướng trong trường hợp đó.

PlayerScreen.kt

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

...

LaunchedEffect(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 (context.supportedOrientations().contains(SupportedOrientation.Landscape)
        && !context.isInMultiWindowMode
    ) {
        context.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
    }

    ...
}

Màn hình của trình phát sẽ chuyển sang vòng lặp nhấp nháy trên trình mô phỏng Polestar 2 trước khi thêm bước kiểm tra (khi hoạt động không xử lý các thay đổi về hướng đối với cấu hình)

Màn hình trình phát được tạo hiệu ứng hòm thư trên trình mô phỏng Polestar 2 trước khi thêm bước kiểm tra (khi hoạt động xử lý các thay đổi về hướng đối với cấu hình)

Màn hình trình phát không được tạo hiệu ứng hòm thư trên trình mô phỏng Polestar 2 sau khi thêm bước kiểm tra.

Màn hình trình phát chuyển sang vòng lặp nhấp nháy trên trình mô phỏng Polestar 2 trước khi thêm bước kiểm tra (khi hoạt động không xử lý các thay đổi về cấu hình của orientation)

Màn hình trình phát được được tạo hiệu ứng hòm thư trên trình mô phỏng Polestar 2 trước khi thêm bước kiểm tra (khi hoạt động xử lý các thay đổi về cấu hình của orientation)

Màn hình trình phát được tạo hiệu ứng hòm thư trên trình mô phỏng Polestar 2 sau khi thêm bước kiểm tra

Vì đây là vị trí duy nhất trong ứng dụng đặt hướng màn hình, nên ứng dụng hiện tránh được hiệu ứng hòm thư! Trong ứng dụng của riêng bạn, hãy kiểm tra mọi thuộc tính screenOrientation hoặc lệnh gọi setRequestedOrientation chỉ dành cho hướng ngang hoặc hướng dọc (bao gồm cả biến thể cảm biến, đảo ngược và biến thể người dùng của mỗi hướng) và xoá hoặc bảo vệ các thuộc tính đó khi cần thiết để hạn chế hiệu ứng hòm thư. Để biết thêm thông tin, hãy xem phần Chế độ tương thích màn hình lớn.

Điều chỉnh theo khả năng kiểm soát thanh hệ thống

Rất tiếc, mặc dù thay đổi trước đảm bảo ứng dụng không chuyển sang vòng lặp nhấp nháy hoặc bị hiệu ứng hòm thư, nhưng thay đổi này cũng tiết lộ một giả định khác đã bị phá bỏ – cụ thể là các thanh hệ thống có thể luôn bị ẩn! Vì nhu cầu của người dùng khi sử dụng ô tô là khác nhau (so với việc sử dụng điện thoại hoặc máy tính bảng), nên Nhà sản xuất thiết bị gốc (OEM) có thể ngăn các ứng dụng ẩn thanh hệ thống để đảm bảo rằng các chức năng điều khiển của xe, chẳng hạn như chế độ kiểm soát nhiệt độ và độ ẩm, luôn truy cập được trên màn hình.

Do đó, các ứng dụng có thể hiển thị phía sau các thanh hệ thống khi đang hiển thị ở chế độ hiển thị tối đa và giả định rằng các thanh này có thể bị ẩn. Bạn có thể thấy điều này ở bước trước, vì các nút điều khiển trên và dưới của trình phát sẽ không còn hiển thị khi ứng dụng không ở chế độ hòm thư! Trong trường hợp cụ thể này, ứng dụng không còn điều hướng được vì nút để đóng trình phát bị che khuất và chức năng của ứng dụng này bị cản trở do không thể sử dụng thanh tua.

Cách khắc phục dễ nhất là áp dụng khoảng đệm của các phần lồng ghép cửa sổ systemBars cho trình phát như sau:

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(...)
}

Tuy nhiên, đây không phải là giải pháp lý tưởng vì nó khiến các thành phần trên giao diện người dùng dịch chuyển khi các thanh hệ thống chuyển động.

9c51956e2093820a.gif

Để cải thiện trải nghiệm người dùng, bạn có thể cập nhật ứng dụng để theo dõi những phần lồng ghép có thể được kiểm soát và chỉ áp dụng khoảng đệm cho những phần lồng ghép không thể kiểm soát.

  1. Vì những màn hình khác trong ứng dụng có thể muốn kiểm soát các phần lồng ghép cửa sổ, nên việc truyền các phần lồng ghép có thể điều khiển dưới dạng CompositionLocal là hợp lý. Tạo một tệp mới LocalControllableInsets.kt trong gói com.example.android.cars.roadreels và thêm nội dung sau:

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. Thiết lập OnControllableInsetsChangedListener để theo dõi các thay đổi.

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. Thêm một CompositionLocalProvider cấp cao nhất có chứa giao diện và thành phần kết hợp ứng dụng, đồng thời liên kết các giá trị với LocalControllableInsets.

MainActivity.kt

import androidx.compose.runtime.CompositionLocalProvider

...

CompositionLocalProvider(LocalControllableInsets provides controllableInsetsTypeMask) {
    RoadReelsTheme {
        RoadReelsApp(calculateWindowSizeClass(this))
    }
}
  1. Trong trình phát, hãy đọc giá trị hiện tại và sử dụng giá trị đó để xác định các phần lồng ghép dùng cho khoảng đệm.

PlayerScreen.kt

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

...

val controllableInsetsTypeMask = LocalControllableInsets.current

// 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(...)
}

Nội dung không dịch chuyển khi các thanh hệ thống có thể bị ẩn

Nội dung vẫn hiển thị khi các thanh hệ thống không thể ẩn được

Nội dung không dịch chuyển khi các thanh hệ thống có thể bị ẩn

Nội dung vẫn hiển thị khi các thanh hệ thống không thể ẩn được

Tốt hơn nhiều! Nội dung không dịch chuyển, đồng thời, các nút điều khiển sẽ hiển thị đầy đủ ngay cả trên những ô tô không thể điều khiển thanh hệ thống.

7. Đáp ứng các yêu cầu về chất lượng của Android Automotive OS: Sự phân tâm của người lái xe

Cuối cùng, có một điểm khác biệt lớn giữa ứng dụng dùng trên ô tô và trên các kiểu dáng khác đó là chúng được dùng để lái xe! Do đó, việc hạn chế các yếu tố gây phân tâm khi lái xe là rất quan trọng. Tất cả ứng dụng đỗ xe dành cho Android Automotive OS phải tạm dừng quá trình phát khi bắt đầu lái xe. Một lớp phủ hệ thống sẽ xuất hiện khi bắt đầu lái xe và từ đó, sự kiện trong vòng đời onPause sẽ được gọi cho ứng dụng đang được phủ. Khi gọi lệnh này, các ứng dụng sẽ tạm dừng quá trình phát.

Mô phỏng hoạt động lái xe

Chuyển đến khung hiển thị của trình phát trong trình mô phỏng và bắt đầu phát nội dung. Sau đó, hãy làm theo các bước để mô phỏng hoạt động lái xe. Lưu ý rằng mặc dù giao diện người dùng của ứng dụng bị hệ thống che khuất nhưng quá trình phát sẽ không tạm dừng. Điều này vi phạm nguyên tắc về chất lượng của ứng dụng dành cho ô tô DD-2.

839af1382c1f10ca.png

Tạm dừng phát khi bắt đầu lái xe

  1. Thêm phần phụ thuộc vào cấu phần phần mềm androidx.lifecycle:lifecycle-runtime-compose chứa LifecycleEventEffect giúp chạy mã trên các sự kiện trong vòng đời.

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. Sau khi đồng bộ hoá dự án để tải phần phụ thuộc xuống, hãy thêm LifecycleEventEffect chạy trên sự kiện ON_PAUSE để tạm dừng quá trình phát.

PlayerScreen.kt

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

...

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

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

Sau khi đã thực hiện sửa lỗi, hãy làm theo các bước tương tự như trước đó để mô phỏng hoạt động lái xe trong khi chủ động phát và nhận thấy quá trình phát sẽ dừng lại, đáp ứng yêu cầu DD-2!

8. Kiểm thử ứng dụng trong trình mô phỏng màn hình từ xa

Một cấu hình mới bắt đầu xuất hiện trên các ô tô là thiết lập hai màn hình, một màn hình chính ở bảng điều khiển trung tâm và một màn hình phụ ở trên cao trong bảng điều khiển gần kính chắn gió. Các ứng dụng có thể được di chuyển từ màn hình trung tâm sang màn hình phụ và ngược lại để người lái xe và hành khách có thêm nhiều lựa chọn.

Cài đặt Automotive bằng hình ảnh màn hình từ xa

  1. Trước tiên, hãy mở Trình quản lý SDK trong Android Studio rồi chọn thẻ SDK Platforms (Nền tảng SDK) nếu chưa chọn. Ở góc dưới bên phải của cửa sổ Trình quản lý SDK, hãy đảm bảo rằng hộp Show package details (Hiện thông tin về gói) được chọn.
  2. Cài đặt hình ảnh trình mô phỏng màn hình từ xa Automotive có API của Google cho cấu trúc máy tính (x86/ARM).

Tạo thiết bị Android ảo Android Automotive OS

  1. Sau khi mở Device Manager (Trình quản lý thiết bị), hãy chọn Automotive trong cột Category (Danh mục) ở bên trái cửa sổ. Sau đó, chọn hồ sơ phần cứng theo gói Màn hình từ xa của ô tô trong danh sách rồi nhấp vào Next (Tiếp theo).
  2. Trên trang tiếp theo, hãy chọn hình ảnh hệ thống ở bước trước. Nhấp vào Next (Tiếp theo) rồi chọn mọi tuỳ chọn nâng cao mà bạn muốn trước khi tạo AVD bằng cách nhấp vào Finish (Hoàn tất).

Chạy ứng dụng

Chạy ứng dụng trên trình mô phỏng bạn vừa tạo bằng cấu hình chạy app hiện có. Làm theo hướng dẫn trong phần Sử dụng trình mô phỏng màn hình từ xa để di chuyển ứng dụng đến và từ màn hình từ xa. Hãy thử di chuyển ứng dụng cả khi ở trên màn hình chính/màn hình chi tiết và khi ở trên màn hình trình phát và cố gắng tương tác với ứng dụng trên cả hai màn hình.

b277bd18a94e9c1b.png

9. Cải thiện trải nghiệm trong ứng dụng trên màn hình từ xa

Khi dùng ứng dụng trên màn hình từ xa, bạn có thể nhận thấy 2 điều sau:

  1. Quá trình phát sẽ bắt đầu lại khi ứng dụng được di chuyển đến và ra khỏi màn hình từ xa
  2. Bạn không thể tương tác với ứng dụng khi ứng dụng đang ở trên màn hình từ xa, bao gồm cả việc thay đổi trạng thái phát.

Cải thiện khả năng dùng ứng dụng liên tục

Sự cố trong đó quá trình phát bắt đầu lại là do hoạt động được tạo lại khi thay đổi cấu hình. Vì ứng dụng được viết bằng Compose và cấu hình thay đổi theo kích thước, nên việc cho phép Compose xử lý các thay đổi về cấu hình cho bạn rất đơn giản bằng cách hạn chế việc tạo lại hoạt động đối với các thay đổi về cấu hình theo kích thước. Nhờ vậy, quá trình chuyển tiếp giữa các màn hình diễn ra liền mạch, không bị dừng phát hoặc tải lại do việc tạo lại hoạt động.

AndroidManifest.xml

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

Triển khai các bộ điều khiển chế độ phát

Để khắc phục lỗi trong đó không điều khiển được ứng dụng khi ứng dụng ở trên màn hình từ xa, bạn có thể triển khai MediaSession. Phiên phát nội dung nghe nhìn là cách thức tương tác phổ biến với trình phát âm thanh hoặc trình phát video. Để biết thêm thông tin, hãy xem phần Kiểm soát và quảng cáo tính năng phát bằng MediaSession.

  1. Thêm phần phụ thuộc vào cấu phần phần mềm 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. Tạo MediaSession bằng trình tạo tương ứng.

PlayerScreen.kt

import androidx.media3.session.MediaSession

@Composable
fun PlayerScreen(...) {
    ...
    val mediaSession = remember(context, player) {
        MediaSession.Builder(context, player).build()
    }
    ...
}
  1. Sau đó, hãy thêm một dòng bổ sung trong khối onDispose của DisposableEffect trong thành phần kết hợp Player để phát hành MediaSession khi Player rời khỏi cây thành phần Compose.

PlayerScreen.kt

DisposableEffect(Unit) {
    onDispose {
        mediaSession.release()
        player.release()
        ...
    }
}
  1. Cuối cùng, khi ở màn hình trình phát, bạn có thể thử nghiệm các nút điều khiển nội dung nghe nhìn bằng lệnh 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

Nhờ đó, ứng dụng sẽ hoạt động hiệu quả hơn nhiều trong những ô tô có màn hình từ xa! Hơn thế nữa, tính năng này còn hoạt động tốt hơn trên các kiểu dáng khác! Trên các thiết bị có thể xoay màn hình hoặc cho phép người dùng đổi kích thước cửa sổ của ứng dụng, thì giờ đây, ứng dụng cũng có thể thích ứng một cách liền mạch trong những tình huống đó.

Ngoài ra, nhờ tích hợp phiên nội dung nghe nhìn, chế độ phát của ứng dụng không chỉ được điều khiển bằng các chế độ điều khiển phần cứng và phần mềm trong ô tô mà còn bằng các nguồn khác, chẳng hạn như truy vấn Trợ lý Google hoặc nút tạm dừng trên tai nghe. Nhờ vậy mà người dùng có thêm nhiều lựa chọn để điều khiển ứng dụng trên các kiểu dáng!

10. Kiểm thử ứng dụng theo nhiều cấu hình hệ thống

Khi ứng dụng hoạt động tốt trên màn hình chính và màn hình từ xa, điều cuối cùng bạn cần kiểm tra là cách ứng dụng xử lý các cấu hình thanh hệ thống và vết cắt trên màn hình. Như mô tả trong phần Xử lý phần lồng ghép cửa sổ và vết cắt trên màn hình, các thiết bị Android Automotive OS có thể có các cấu hình phá vỡ các giả định thường đúng trên các kiểu dáng thiết bị di động.

Trong phần này, bạn sẽ tải xuống một trình mô phỏng có thể được định cấu hình trong thời gian chạy, định cấu hình trình mô phỏng để có thanh hệ thống bên trái và kiểm thử ứng dụng trong cấu hình đó.

Cài đặt hình ảnh Android Automotive có API của Google

  1. Trước tiên, hãy mở Trình quản lý SDK trong Android Studio rồi chọn thẻ SDK Platforms (Nền tảng SDK) nếu chưa chọn. Ở góc dưới bên phải của cửa sổ Trình quản lý SDK, hãy đảm bảo rằng hộp Show package details (Hiện thông tin về gói) được chọn.
  2. Cài đặt hình ảnh trình mô phỏng API 33 Android Automotive có API của Google cho cấu trúc máy tính (x86/ARM).

Tạo thiết bị Android ảo Android Automotive OS

  1. Sau khi mở Device Manager (Trình quản lý thiết bị), hãy chọn Automotive trong cột Category (Danh mục) ở bên trái cửa sổ. Sau đó, chọn hồ sơ phần cứng theo gói Automotive (1080p theo chiều ngang) trong danh sách rồi nhấp vào Next (Tiếp theo).
  2. Trên trang tiếp theo, hãy chọn hình ảnh hệ thống ở bước trước. Nhấp vào Next (Tiếp theo) rồi chọn mọi tuỳ chọn nâng cao mà bạn muốn trước khi tạo AVD bằng cách nhấp vào Finish (Hoàn tất).

Định cấu hình thanh hệ thống bên

Như đã nêu chi tiết trong bài viết Kiểm thử bằng trình mô phỏng có thể định cấu hình, có nhiều tuỳ chọn để mô phỏng nhiều cấu hình hệ thống có trên ô tô.

Trong phạm vi của lớp học lập trình này, bạn có thể dùng com.android.systemui.rro.left để kiểm thử một cấu hình thanh hệ thống khác. Để kích hoạt mã này, hãy dùng lệnh sau:

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

b642703a7278b219.png

Vì ứng dụng đang dùng đối tượng sửa đổi systemBars làm contentWindowInsets trong Scaffold, nên nội dung đã được vẽ trong một vùng an toàn của các thanh hệ thống. Để xem điều gì sẽ xảy ra nếu ứng dụng giả định rằng các thanh hệ thống chỉ xuất hiện ở đầu và cuối màn hình, hãy thay đổi tham số đó thành như sau:

RoadReelsApp.kt

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

Rất tiếc! Màn hình danh sách và màn hình chi tiết hiển thị phía sau thanh hệ thống. Do thao tác trước đó, màn hình trình phát sẽ hoạt động tốt, ngay cả khi các thanh hệ thống không thể điều khiển được.

9898f7298a7dfb4.gif

Trước khi chuyển sang phần tiếp theo, hãy nhớ huỷ bỏ thay đổi mà bạn vừa thực hiện đối với tham số windowContentPadding!

11. Xử lý vết cắt trên màn hình

Cuối cùng, màn hình của một số ô tô có vết cắt trên màn hình rất khác so với các vết cắt trên thiết bị di động. Thay vì các đường cắt hoặc lỗ camera hình lỗ chốt, một số xe chạy Android Automotive OS có màn hình cong khiến màn hình không phải là hình chữ nhật.

Để xem ứng dụng hoạt động như thế nào khi có vết cắt trên màn hình như vậy, trước tiên, hãy bật chế độ vết cắt trên màn hình bằng lệnh sau:

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

Để kiểm thử xem ứng dụng có hoạt động tốt hay không, hãy bật thanh hệ thống bên trái được dùng ở phần sau cùng, nếu chưa:

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

Ứng dụng không hiển thị vào vết cắt trên màn hình (hiện tại khó xác định chính xác hình dạng của vết cắt này, nhưng sẽ rõ ràng hơn ở bước tiếp theo). Điều này hoàn toàn bình thường và sẽ đem lại trải nghiệm chất lượng hơn so với ứng dụng chỉ hiển thị vào vết cắt nhưng không điều chỉnh cẩn thận cho phù hợp.

212628db84981025.gif

Hiển thị vào vết cắt trên màn hình

Để mang lại cho người dùng trải nghiệm sống động nhất có thể, bạn có thể tận dụng nhiều không gian màn hình hơn bằng cách hiển thị vào vết cắt trên màn hình.

  1. Để hiển thị vào vết cắt trên màn hình, hãy tạo một tệp integers.xml để duy trì chế độ ghi đè dành riêng cho ô tô. Để làm việc này, hãy sử dụng bộ hạn định chế độ giao diện người dùng có giá trị Car Dock (tên này được giữ lại từ thời điểm chỉ có Android Auto, nhưng Android Automotive OS cũng sử dụng). Ngoài ra, vì LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS đã được ra mắt trong Android R, hãy thêm bộ hạn định Version của Android có giá trị 30. Hãy xem phần Sử dụng tài nguyên thay thế để biết thêm thông tin.

22b7f17657cac3fd.png

  1. Trong tệp bạn vừa tạo (res/values-car-v30/integers.xml), hãy thêm nội dung sau:

integers.xml

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

Giá trị số nguyên 3 tương ứng với LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS và ghi đè giá trị mặc định là 0 từ res/values/integers.xml, tương ứng với LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT của chúng tôi. Giá trị số nguyên này đã được tham chiếu trong MainActivity.kt để ghi đè chế độ do enableEdgeToEdge() đặt. Để biết thêm thông tin về thuộc tính này, hãy xem tài liệu tham khảo.

Bây giờ, khi bạn chạy ứng dụng, hãy chú ý rằng nội dung sẽ mở rộng vào phần vết cắt và trông rất sống động! Tuy nhiên, thanh ứng dụng trên cùng và một số nội dung bị che khuất một phần bởi vết cắt trên màn hình, gây ra vấn đề tương tự như khi ứng dụng giả định rằng các thanh hệ thống sẽ chỉ xuất hiện ở trên cùng và dưới cùng.

f0eefa42dee6f7c7.gif

Cố định thanh ứng dụng trên cùng

Để cố định thanh ứng dụng trên cùng, bạn có thể thêm tham số windowInsets sau đây vào các Thành phần kết hợp CenterAlignedTopAppBar:

RoadReelsApp.kt

import androidx.compose.foundation.layout.safeDrawing

...

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

safeDrawing bao gồm cả phần lồng ghép displayCutoutsystemBars, nên điều này sẽ cải thiện tham số windowInsets mặc định, chỉ sử dụng systemBars khi định vị thanh ứng dụng trên cùng.

Ngoài ra, vì thanh ứng dụng trên cùng nằm ở đầu cửa sổ, nên bạn không nên thêm thành phần dưới cùng của phần lồng ghép safeDrawing – làm như vậy có thể thêm khoảng đệm không cần thiết.

7d59ebb63ada5f71.gif

Cố định màn hình chính

Một lựa chọn để cố định nội dung trên màn hình chính và màn hình chi tiết là sử dụng safeDrawing thay vì systemBars cho contentWindowInsets của Scaffold. Tuy nhiên, khi sử dụng tuỳ chọn này, ứng dụng sẽ trở nên kém sống động hơn, với nội dung đột ngột bị cắt ngay tại nơi vết cắt trên màn hình bắt đầu. Điều này cũng không khả thi hơn so với trường hợp ứng dụng hoàn toàn không hiển thị vào vết cắt trên màn hình.

6b3824ca3214cbfa.gif

Để có giao diện người dùng sống động hơn, bạn có thể xử lý các phần lồng ghép trên từng thành phần trong màn hình.

  1. Cập nhật contentWindowInsets của Scaffold để luôn là 0 dp (thay vì chỉ cho PlayerScreen). Thao tác này cho phép mỗi màn hình và/hoặc thành phần trong một màn hình xác định cách hoạt động của màn hình và/hoặc thành phần đó liên quan đến các phần lồng ghép.

RoadReelsApp.kt

Scaffold(
    ...,
    contentWindowInsets = WindowInsets(0.dp)
) { ... }
  1. Thiết lập thành phần kết hợp windowInsetsPadding của tiêu đề hàng Text để sử dụng các thành phần ngang của các phần lồng ghép safeDrawing. Thành phần trên cùng của các phần lồng ghép này sẽ do thanh ứng dụng trên cùng xử lý và thành phần dưới cùng sẽ được xử lý sau.

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. Xoá tham số contentPadding của LazyRow. Sau đó, ở đầu và cuối mỗi LazyRow, hãy thêm Spacer vào chiều rộng của thành phần safeDrawing tương ứng để đảm bảo có thể xem toàn bộ hình thu nhỏ. Sử dụng đối tượng sửa đổi widthIn để đảm bảo các khoảng trống này ít nhất phải rộng bằng khoảng đệm nội dung trước đây. Nếu không có các phần tử này, các mục ở đầu và cuối hàng có thể bị che khuất phía sau các thanh hệ thống và/hoặc vết cắt trên màn hình, ngay cả khi vuốt hết về đầu/cuối hàng.

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. Cuối cùng, hãy thêm Spacer vào cuối LazyColumn để tính đến mọi thanh hệ thống hoặc phần lồng ghép của vết cắt trên màn hình có thể có ở cuối màn hình. Không cần một khoảng trống tương đương ở đầu LazyColumn vì thanh ứng dụng trên cùng sẽ xử lý các khoảng trống đó. Nếu ứng dụng dùng thanh ứng dụng dưới cùng thay vì thanh ứng dụng trên cùng, bạn sẽ thêm Spacer ở đầu danh sách bằng cách sử dụng đối tượng sửa đổi windowInsetsTopHeight. Và nếu ứng dụng dùng cả thanh ứng dụng trên cùng và dưới cùng, thì không cần khoảng trống nào.

MainScreen.kt

import androidx.compose.foundation.layout.windowInsetsBottomHeight

...

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

Tuyệt vời! Các thanh ứng dụng trên cùng được hiển thị hoàn toàn và khi cuộn đến cuối hàng, giờ đây bạn có thể thấy tất cả hình thu nhỏ!

543706473398114a.gif

Cố định màn hình chi tiết

f622958a8d0c16c8.png

Chất lượng màn hình chi tiết không quá kém nhưng nội dung vẫn bị cắt bớt.

Vì màn hình chi tiết không có nội dung nào có thể cuộn, nên để cố định, bạn chỉ cần thêm một đối tượng sửa đổi windowInsetsPaddingBox cấp cao nhất.

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)
) { ... }

bdd6de6010fc139d.png

Cố định màn hình trình phát

Mặc dù PlayerScreen đã áp dụng khoảng đệm cho một số hoặc tất cả các phần lồng ghép cửa sổ thanh hệ thống từ phía sau trong Đáp ứng yêu cầu về chất lượng của Android Automotive OS: Khả năng điều hướng, nhưng như vậy vẫn chưa đủ để đảm bảo rằng ứng dụng không bị che khuất khi đang hiển thị vào vết cắt trên màn hình. Trên thiết bị di động, vết cắt trên màn hình hầu như luôn nằm hoàn toàn trong các thanh hệ thống. Tuy nhiên, trên ô tô, vết cắt trên màn hình có thể vượt quá thanh hệ thống, phá vỡ các giả định.

427227df5e44f554.png

Để cố định màn hình này, bạn chỉ cần thay đổi giá trị ban đầu của biến windowInsetsForPadding từ giá trị 0 thành displayCutout:

PlayerScreen.kt

import androidx.compose.foundation.layout.displayCutout

...

var windowInsetsForPadding = WindowInsets(WindowInsets.displayCutout)

b523d8c1e1423757.gif

Thật tuyệt! Ứng dụng thực sự vừa khai thác tối đa màn hình trong khi vẫn duy trì tính hữu dụng!

Và nếu bạn chạy ứng dụng trên một thiết bị di động thì ứng dụng trên đó cũng sẽ sống động hơn! Các mục trong danh sách hiển thị tận dụng các cạnh của màn hình, bao gồm cả phía sau thanh điều hướng.

dc7918499a33df31.png

12. Xin chúc mừng

Bạn đã di chuyển và tối ưu hoá thành công ứng dụng đỗ xe đầu tiên của mình. Bây giờ là lúc bạn áp dụng kiến thức đã học vào ứng dụng của riêng bạn!

Những điều nên thử

Tài liệu đọc thêm