Core Compose

Thư viện media3-ui-compose cung cấp các thành phần cơ bản để xây dựng giao diện người dùng cho nội dung nghe nhìn trong Jetpack Compose. Thư viện này được thiết kế cho các nhà phát triển cần tuỳ chỉnh nhiều hơn so với những gì mà thư viện media3-ui-compose-material3 cung cấp. Trang này giải thích cách sử dụng các thành phần cốt lõi và phần tử giữ trạng thái để tạo giao diện người dùng trình phát nội dung nghe nhìn tuỳ chỉnh.

Kết hợp các thành phần Material3 và Compose tuỳ chỉnh

Thư viện media3-ui-compose-material3 được thiết kế để linh hoạt. Bạn có thể sử dụng các thành phần dựng sẵn cho hầu hết giao diện người dùng, nhưng thay thế một thành phần bằng một cách triển khai tuỳ chỉnh khi cần kiểm soát nhiều hơn. Đây là lúc thư viện media3-ui-compose phát huy tác dụng.

Ví dụ: giả sử bạn muốn sử dụng tiêu chuẩn PreviousButtonNextButton từ thư viện Material3, nhưng bạn cần một PlayPauseButton hoàn toàn tuỳ chỉnh. Bạn có thể thực hiện việc này bằng cách sử dụng PlayPauseButton từ thư viện media3-ui-compose cốt lõi và đặt bên cạnh các thành phần dựng sẵn.

Row {
  // Use prebuilt component from the Media3 UI Compose Material3 library
  PreviousButton(player)
  // Use the scaffold component from Media3 UI Compose library
  PlayPauseButton(player) {
    // `this` is PlayPauseButtonState
    FilledTonalButton(
      onClick = {
        Log.d("PlayPauseButton", "Clicking on play-pause button")
        this.onClick()
      },
      enabled = this.isEnabled,
    ) {
      Icon(
        imageVector = if (showPlay) Icons.Default.PlayArrow else Icons.Default.Pause,
        contentDescription = if (showPlay) "Play" else "Pause",
      )
    }
  }
  // Use prebuilt component from the Media3 UI Compose Material3 library
  NextButton(player)
}

Các thành phần có sẵn

Thư viện media3-ui-compose cung cấp một tập hợp các thành phần kết hợp dựng sẵn cho các nút điều khiển trình phát phổ biến. Dưới đây là một số thành phần bạn có thể sử dụng trực tiếp trong ứng dụng:

Thành phần Mô tả
PlayPauseButton Vùng chứa trạng thái cho một nút chuyển đổi giữa phát và tạm dừng.
SeekBackButton Vùng chứa trạng thái cho một nút tua lại theo một mức tăng xác định.
SeekForwardButton Vùng chứa trạng thái cho một nút tua đi theo một mức tăng xác định.
NextButton Vùng chứa trạng thái cho một nút tua đến mục nội dung nghe nhìn tiếp theo.
PreviousButton Vùng chứa trạng thái cho một nút tua đến mục nội dung nghe nhìn trước đó.
RepeatButton Vùng chứa trạng thái cho một nút chuyển đổi qua các chế độ lặp lại.
ShuffleButton Vùng chứa trạng thái cho một nút chuyển đổi chế độ phát ngẫu nhiên.
MuteButton Vùng chứa trạng thái cho một nút tắt và bật tiếng trình phát.
TimeText Vùng chứa trạng thái cho một thành phần kết hợp hiển thị tiến trình của trình phát.
ContentFrame Một surface để hiển thị nội dung nghe nhìn, xử lý việc quản lý tỷ lệ khung hình, đổi kích thước và màn trập
PlayerSurface Surface thô bao bọc SurfaceViewTextureView trong AndroidView.

Tuỳ chỉnh thành phần kết hợp Player

Khi sử dụng thư viện media3-ui-compose-material3, thành phần kết hợp Player mang đến trải nghiệm phát nội dung nghe nhìn hoàn chỉnh. Bạn có thể tuỳ chỉnh bố cục của thành phần này bằng cách cung cấp các thành phần kết hợp của riêng bạn cho các vùng nội dung, hoặc bạn có thể tận dụng đối tượng PlayerDefaults để tuỳ chỉnh các phần cụ thể của giao diện người dùng, chẳng hạn như TopControls, CenterControls, BottomControlsErrorOverlay.

Ví dụ: bạn có thể sử dụng PlayerDefaults.CenterControls để chỉ ghi đè nút phát/tạm dừng ở giữa trong khi vẫn giữ nguyên các nút điều khiển ở giữa khác. Bạn cũng có thể chuyển một thành phần kết hợp hoàn toàn tuỳ chỉnh cho một tham số như topControls và hoàn toàn bỏ qua các tham số khác như bottomControls để giữ nguyên giá trị mặc định.

@Composable
fun CustomPlayerSlots(player: Player, modifier: Modifier = Modifier) {
  Player(
    player = player,
    modifier = modifier,
    topControls = { p, visible ->
      // Fully custom top controls
      AnimatedVisibility(visible) { Text("My custom title") }
    },
    centerControls = { p, visible ->
      // Use default CenterControls but override the central button
      PlayerDefaults.CenterControls(
        player = p,
        visible = visible,
        central = {
          // A custom play/pause button
          Material3PlayPauseButton(it, modifier = Modifier.size(64.dp))
        },
      )
    },
    // bottomControls are left as default
  )
}

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

Nếu không có thành phần khung nào đáp ứng nhu cầu của bạn, bạn cũng có thể sử dụng trực tiếp các đối tượng trạng thái. Bạn nên sử dụng các phương thức remember tương ứng để giữ nguyên giao diện người dùng giữa các lần kết hợp lại.

Để 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 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, thư viện giả định rằng các trạng thái này rất có thể sẽ được sử dụng bởi các thành phần kết hợp giống như nút.

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

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

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),
  )
}

Phần tử giữ trạng thái đầu ra trực quan

CurrentMediaItemState cung cấp thông tin về MediaItem hiện đang phát, trong khi PlaylistState hiển thị thông tin về tập hợp MediaItems trên trình phát. Các phần tử này hữu ích cho việc hiển thị thông tin siêu dữ liệu trong giao diện người dùng tuỳ chỉnh.

ErrorState cung cấp thông tin về trạng thái lỗi hiện tại của trình phát, có thể dùng để hiển thị lớp phủ lỗi.

PresentationState giữ thông tin về thời điểm có thể hiển thị đầu ra video trong PlayerSurface hoặc nên được che phủ bằng một phần tử giao diện người dùng giữ chỗ. ContentFrame thành phần kết hợp kết hợp việc xử lý tỷ lệ khung hình với việc hiển thị màn trập trên một surface chưa sẵn sàng.

@Composable
fun ContentFrame(
  player: Player?,
  modifier: Modifier = Modifier,
  surfaceType: @SurfaceType Int = SURFACE_TYPE_SURFACE_VIEW,
  contentScale: ContentScale = ContentScale.Fit,
  keepContentOnReset: Boolean = false,
  shutter: @Composable () -> Unit = { Box(Modifier.fillMaxSize().background(Color.Black)) },
) {
  val presentationState = rememberPresentationState(player, keepContentOnReset)
  val scaledModifier =
    modifier.resizeWithContentScale(contentScale, presentationState.videoAspectRatio)

  // Always leave PlayerSurface to be part of the Compose tree because it will be initialized 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, scaledModifier, surfaceType)

  if (presentationState.coverSurface) {
    // Cover the surface that is being prepared with a shutter
    shutter()
  }
}

Ở đây, chúng ta có thể sử dụng cả presentationState.videoAspectRatio để điều chỉnh tỷ lệ Surface theo tỷ lệ khung hình đã chọn (xem Tỷ lệ nội dung để 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ờ ở trên cùng của surface. Màn trập này sẽ biến mất khi surface sẵn sàng. ContentFrame cho phép bạn tuỳ chỉnh màn trập dưới dạng lambda cuối, nhưng theo mặc định, màn trập sẽ là một @Composable Box màu đen có kích thước bằng với vùng chứa mẹ.

Flow ở đâu?

Nhiều nhà phát triển Android đã quen với việc sử dụng các đối tượng Flow Kotlin để thu thập dữ liệu giao diện người dùng luôn thay đổi. Ví dụ: bạn có thể đang 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 flow 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à bản chất không đồng bộ của việc truyền dữ liệu. Chúng tôi muốn đạt được độ trễ càng thấp càng tốt giữa Player.Event và việc sử dụng nó ở 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 flow có tất cả Player.Events sẽ không tuân thủ nguyên tắc một 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 flow cho mỗi Player.Event sẽ yêu cầu bạn kết hợp chúng (với combine) cho từng thành phần giao diện người dùng. Có mối quan hệ ánh xạ nhiều với nhiều giữa Player.Event và sự thay đổi thành phầ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 rơi vào 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. Hãy xem 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ẽ thực hiện những việc sau:

  1. Nhận đối tượng Player.
  2. Đăng ký Player bằng cách sử dụng coroutine. Xem Player.listen để biết thêm chi tiết.
  3. Phản hồi các 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 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ì một khung hiển thị nhất quán về trạng thái của Trình phát.
  6. Hiển thị các trường State Compose mà một thành phần kết hợp có thể sử dụng để phản hồi động đối với 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 kết hợp.

Những gì xảy ra ở hậu trường:

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

  var someFieldValue by mutableStateOf(someFieldDefault)
    private set

  fun onClick() {
    player.actionA()
  }

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

Để phản ứng với Player.Events của riêng bạn, bạn có thể nắm bắt chúng bằng cách sử dụng Player.listen Đây là một suspend fun cho phép bạn tham gia vào thế giới coroutine và nghe Player.Events vô thời hạn. Việc triển khai Media3 của 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.