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 có 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.7.1")

Groovy

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

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

Ứng dụng minh hoạ hoàn toàn bằng 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 có 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 bằng Compose mà không cần đến bất kỳ giải pháp tương tác nào như bao bọc PlayerView trong AndroidView. Ứng dụng minh hoạ sử dụng các lớp trình giữ 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 các 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 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 ta giả định rằng các trạng thái này nhiều khả năng sẽ được dùng bởi các thành phần kết hợp tương tự như nút.

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 ý rằng state không có thông tin về việc tạo giao diện, chẳng hạn như biểu tượng dù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 theo bố cục mà bạn 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 trực quan

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

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ệ Surface theo tỷ lệ khung hình mong muốn (xem tài liệu 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 nằm ở đâu?

Nhiều nhà phát triển Android đã quen với việc sử dụng các đối tượng Kotlin Flow để 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 flow Player.isPlaying mà bạn có thể collect theo cách nhận biết vòng đời. Hoặc một thứ gì đó 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 các luồng cho trạng thái giao diện người dùng Player có một số nhược điểm. Một trong những mối lo ngại chính là tính chất không đồng bộ của việc chuyển dữ liệu. Chúng tôi muốn đảm bảo độ trễ thấp nhất có thể giữa một Player.Event và mức tiêu thụ của nó ở phía giao diện người dùng, tránh hiển thị các phần tử 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ủ 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.
  • Việc tạo một luồng cho mỗi Player.Event sẽ yêu cầu bạ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 quan hệ nhiều với nhiều giữa Player.Event và một thay đổi về phần tử 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ó khả năng 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. 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 điển hình sẽ làm những việc sau:

  1. Nhận một 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ộ.
  4. Chấp nhận các lệnh logic nghiệp vụ sẽ được chuyển đổi thành một bản cập nhật Player thích hợp.
  5. Có thể được tạo ở nhiều nơi trong cây giao diện người dùng và sẽ luôn duy trì một chế độ xem nhất quán về trạng thái của Trình phát.
  6. Hiển thị các trường State của Compose mà một Thành phần kết hợp có thể sử dụng để phản hồi các thay đổi một cách linh động.
  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 bạn, bạn có thể bắt chúng bằng Player.listen. Đây là một suspend fun cho phép bạn tham gia vào thế giới coroutine và lắng 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 Player.Events.