Bắt đầu với giao diện người dùng dựa trên Compose

Thêm phần phụ thuộc

Thư viện Media3 bao gồm một mô-đun giao diện người dùng dựa trên Jetpack Compose. Để sử dụng, hãy thêm phần phụ thuộc sau:

Kotlin

implementation("androidx.media3:media3-ui-compose:1.6.0")

Groovy

implementation "androidx.media3:media3-ui-compose:1.6.0"

Bạn nên phát triển ứng dụng theo hướng ưu tiên Compose hoặc di chuyển từ việc sử dụng Khung hiển thị.

Ứng dụng minh hoạ đầy đủ về Compose

Mặc dù thư viện media3-ui-compose không bao gồm các thành phần kết hợp sẵn (chẳng hạn như nút, chỉ báo, hình ảnh hoặc hộp thoại), nhưng bạn có thể tìm thấy một ứng dụng minh hoạ được viết hoàn toàn trong Compose để tránh mọi giải pháp tương tác như gói PlayerView trong AndroidView. Ứng dụng minh hoạ sử dụng các lớp chủ thể trạng thái giao diện người dùng từ mô-đun media3-ui-compose và sử dụng thư viện Compose Material3.

Phần tử giữ trạng thái giao diện người dùng

Để hiểu rõ hơn về cách bạn có thể sử dụng tính linh hoạt của phần tử giữ trạng thái giao diện người dùng so với các thành phần kết hợp, hãy đọc về cách Compose quản lý Trạng thái.

Phần tử giữ trạng thái nút

Đối với một số trạng thái giao diện người dùng, chúng tôi giả định rằng các trạng thái đó rất có thể sẽ được các Thành phần kết hợp giống nút sử dụng.

Trạng thái remember*State Loại
PlayPauseButtonState rememberPlayPauseButtonState 2-Toggle
PreviousButtonState rememberPreviousButtonState Hằng số
NextButtonState rememberNextButtonState Hằng số
RepeatButtonState rememberRepeatButtonState 3-Toggle
ShuffleButtonState rememberShuffleButtonState 2-Toggle
PlaybackSpeedState rememberPlaybackSpeedState Trình đơn hoặc N-Toggle

Ví dụ về cách sử dụng PlayPauseButtonState:

@Composable
fun PlayPauseButton(player: Player, modifier: Modifier = Modifier) {
  val state = rememberPlayPauseButtonState(player)

  IconButton(onClick = state::onClick, modifier = modifier, enabled = state.isEnabled) {
    Icon(
      imageVector = if (state.showPlay) Icons.Default.PlayArrow else Icons.Default.Pause,
      contentDescription =
        if (state.showPlay) stringResource(R.string.playpause_button_play)
        else stringResource(R.string.playpause_button_pause),
    )
  }
}

Lưu ý cách state không có thông tin về giao diện, chẳng hạn như biểu tượng để phát hoặc tạm dừng. Trách nhiệm duy nhất của lớp này là chuyển đổi Player thành trạng thái giao diện người dùng.

Sau đó, bạn có thể kết hợp các nút trong bố cục theo ý muốn:

Row(
  modifier = modifier.fillMaxWidth(),
  horizontalArrangement = Arrangement.SpaceEvenly,
  verticalAlignment = Alignment.CenterVertically,
) {
  PreviousButton(player)
  PlayPauseButton(player)
  NextButton(player)
}

Phần tử giữ trạng thái đầu ra hình ảnh

PresentationState lưu giữ thông tin về thời điểm đầu ra video trong PlayerSurface có thể hiển thị hoặc phải được một phần tử giao diện người dùng giữ chỗ che phủ.

val presentationState = rememberPresentationState(player)
val scaledModifier = Modifier.resize(ContentScale.Fit, presentationState.videoSizeDp)

Box(modifier) {
  // Always leave PlayerSurface to be part of the Compose tree because it will be initialised in
  // the process. If this composable is guarded by some condition, it might never become visible
  // because the Player won't emit the relevant event, e.g. the first frame being ready.
  PlayerSurface(
    player = player,
    surfaceType = SURFACE_TYPE_SURFACE_VIEW,
    modifier = scaledModifier,
  )

  if (presentationState.coverSurface) {
    // Cover the surface that is being prepared with a shutter
    Box(Modifier.background(Color.Black))
  }

Ở đây, chúng ta có thể sử dụng cả presentationState.videoSizeDp để điều chỉnh tỷ lệ khung hình của Surface theo tỷ lệ khung hình mong muốn (xem tài liệu về ContentScale để biết thêm các loại) và presentationState.coverSurface để biết thời điểm không phù hợp để hiển thị Surface. Trong trường hợp này, bạn có thể đặt một màn trập mờ lên trên bề mặt. Màn trập này sẽ biến mất khi bề mặt đã sẵn sàng.

Flows ở đâu?

Nhiều nhà phát triển Android đã quen với việc sử dụng các đối tượng Flow của Kotlin để thu thập dữ liệu giao diện người dùng luôn thay đổi. Ví dụ: bạn có thể tìm kiếm luồng Player.isPlaying mà bạn có thể collect theo cách nhận biết được vòng đời. Hoặc một nội dung như Player.eventsFlow để cung cấp cho bạn một Flow<Player.Events> mà bạn có thể filter theo cách bạn muốn.

Tuy nhiên, việc sử dụng flow cho trạng thái giao diện người dùng Player có một số hạn chế. Một trong những mối lo ngại chính là tính chất không đồng bộ của quá trình chuyển dữ liệu. Chúng tôi muốn đảm bảo độ trễ càng nhỏ càng tốt giữa Player.Event và mức sử dụng của Player.Event ở phía giao diện người dùng, tránh hiển thị các thành phần giao diện người dùng không đồng bộ với Player.

Các điểm khác bao gồm:

  • Một luồng có tất cả Player.Events sẽ không tuân thủ một nguyên tắc trách nhiệm duy nhất, mỗi người dùng sẽ phải lọc ra các sự kiện có liên quan.
  • Để tạo luồng cho mỗi Player.Event, bạn cần kết hợp các luồng đó (với combine) cho từng phần tử giao diện người dùng. Có mối liên kết nhiều với nhiều giữa Player.Event và thay đổi thành phần trên giao diện người dùng. Việc phải sử dụng combine có thể khiến giao diện người dùng chuyển sang các trạng thái có thể bất hợp pháp.

Tạo trạng thái giao diện người dùng tuỳ chỉnh

Bạn có thể thêm các trạng thái giao diện người dùng tuỳ chỉnh nếu các trạng thái hiện có không đáp ứng nhu cầu của bạn. Hãy kiểm tra mã nguồn của trạng thái hiện có để sao chép mẫu. Một lớp phần tử giữ trạng thái giao diện người dùng thông thường thực hiện những việc sau:

  1. Nhận Player.
  2. Đăng ký Player bằng coroutine. Hãy xem Player.listen để biết thêm thông tin chi tiết.
  3. Phản hồi một Player.Events cụ thể bằng cách cập nhật trạng thái nội bộ của Player.Events đó.
  4. Chấp nhận các lệnh logic kinh doanh sẽ được chuyển đổi thành bản cập nhật Player thích hợp.
  5. Có thể được tạo ở nhiều vị trí trên cây giao diện người dùng và sẽ luôn duy trì chế độ xem nhất quán về trạng thái của Người chơi.
  6. Hiển thị các trường State của Compose mà Thành phần kết hợp có thể sử dụng để phản hồi linh động các thay đổi.
  7. Đi kèm với hàm remember*State để ghi nhớ thực thể giữa các thành phần.

Những gì diễn ra ở hậu trường:

class SomeButtonState(private val player: Player) {
  var isEnabled by mutableStateOf(player.isCommandAvailable(Player.COMMAND_ACTION_A))
    private set

  var someField by mutableStateOf(someFieldDefault)
    private set

  fun onClick() {
    player.actionA()
  }

  suspend fun observe() =
    player.listen { events ->
      if (
        events.containsAny(
          Player.EVENT_B_CHANGED,
          Player.EVENT_C_CHANGED,
          Player.EVENT_AVAILABLE_COMMANDS_CHANGED,
        )
      ) {
        someField = this.someField
        isEnabled = this.isCommandAvailable(Player.COMMAND_ACTION_A)
      }
    }
}

Để phản ứng với Player.Events của riêng mình, bạn có thể phát hiện chúng bằng Player.listen. Đây là một suspend fun cho phép bạn bước vào thế giới coroutine và nghe Player.Events vô thời hạn. Việc triển khai Media3 cho nhiều trạng thái giao diện người dùng giúp nhà phát triển cuối không phải lo lắng về việc tìm hiểu về Player.Events.