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 đa phương tiện trong Jetpack Compose. Thư viện này được thiết kế cho những nhà phát triển cần tuỳ chỉnh nhiều hơn so với những gì 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 Material3 và các thành phần tuỳ chỉnh của Compose

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 tạo sẵn cho hầu hết giao diện người dùng, nhưng hãy hoán đổi một thành phần duy nhất để triển khai tuỳ chỉnh khi bạn 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 PreviousButton và NextButton tiêu chuẩn trong thư viện Material3, nhưng bạn cần một PlayPauseButton hoàn toàn tuỳ chỉnh. Bạn có thể đạt được điều này bằng cách sử dụng PlayPauseButton trong thư viện media3-ui-compose chính và đặt thành phần này cùng với các thành phần tạo 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)
}

Thành phần hiện có

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 tạo sẵn cho các nút điều khiển trình phát phổ biến. Sau đâ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 nút chuyển đổi giữa chế độ phát và tạm dừng.
SeekBackButton Vùng chứa trạng thái cho nút tua lại theo một khoảng thời gian xác định.
SeekForwardButton Vùng chứa trạng thái cho nút tua đi theo một khoảng thời gian xác định.
NextButton Vùng chứa trạng thái cho 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 nút tua đến mục nội dung nghe nhìn trước đó.
RepeatButton Vùng chứa trạng thái cho nút chuyển đổi giữa các chế độ lặp lại.
ShuffleButton Vùng chứa trạng thái cho nút bật/tắt chế độ phát ngẫu nhiên.
MuteButton Vùng chứa trạng thái cho nút bật và tắt tiếng trình phát.
TimeText Vùng chứa trạng thái cho một composable hiển thị tiến trình của người chơi.
ContentFrame Một nền tảng để 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 Nền thô bao bọc SurfaceView và TextureView 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 sẽ 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 ô 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, BottomControls và ErrorOverlay.

Hình 1 cho thấy vị trí mà thành phần kết hợp Player đặt các vùng chứa của nó. Hình 2 cho thấy thứ tự vẽ các lớp.

Một trình phát video có đường viền nét đứt đánh dấu các vị trí của trình phát. Từ trên xuống dưới: topControls dọc theo cạnh trên, errorOverlay phía trên chính giữa, centerControls ở giữa với các nút backSecondary, back, central, forward và forwardSecondary, phụ đề ở gần dưới cùng và bottomControls dọc theo cạnh dưới với các khe above, left, progressSlider, right và below. ContentFrame lấp đầy khu vực phía sau.
Hình 1. Các vùng của thành phần kết hợp Player. Các chế độ điều khiển ở giữa và dưới cùng cho thấy các vùng của PlayerDefaults.CenterControls và PlayerDefaults.BottomControls. Các vùng topControls, above và below đều trống theo mặc định.
Hình ảnh phân rã của sáu lớp xếp chồng lên nhau. Từ dưới lên trên: 1, PlayerSurface; 2, shutter; 3, artwork; 4, subtitles; 5, errorOverlay; và 6, topControls, centerControls và bottomControls. Lớp 1 đến 4 tạo nên ContentFrame và cả 6 lớp tạo nên Player.
Hình 2. Các lớp của thành phần kết hợp Player, được đánh số theo thứ tự vẽ.

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 còn lại ở giữa. Bạn cũng có thể truyền một composable hoàn toàn tuỳ chỉnh cho một tham số như topControls, và bỏ qua hoàn toàn các tham số khác như bottomControls để giữ nguyên giá trị mặc định.

@Composable
fun CustomPlayerSlots(player: Player, showControls: Boolean, modifier: Modifier = Modifier) {
  Player(
    player = player,
    modifier = modifier,
    showControls = showControls,
    topControls = {
      // Fully custom top controls
      Text("My custom title")
    },
    centerControls = {
      // Use default CenterControls but override the central button
      PlayerDefaults.CenterControls(
        player = player,
        central = {
          // A custom play/pause button
          Material3PlayPauseButton(player, 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 nào trong số các thành phần tạo khung đáp ứng được nhu cầu của bạn, thì bạn cũng có thể sử dụng trực tiếp các đối tượng trạng thái. Nhìn chung, 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 phần tử giữ trạng thái giao diện người dùng so với 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 đó có nhiều khả năng sẽ được các thành phần kết hợp giống như nút sử dụng.

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

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 hình ảnh

CurrentMediaItemState cung cấp thông tin về MediaItem đang phát, trong khi PlaylistState hiển thị thông tin về MediaItems được đặt trên trình phát. Các giá trị này rất 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 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 nên che đầu ra video bằng một phần tử giao diện người dùng giữ chỗ. Thành phần kết hợp ContentFrame kết hợp việc xử lý tỷ lệ khung hình với việc đảm bảo hiển thị màn trập trên một nền tảng 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()
  }
}

Tại đây, chúng ta có thể sử dụng cả presentationState.videoAspectRatio để điều chỉnh tỷ lệ của 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ờ 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. ContentFrame cho phép bạn tuỳ chỉnh màn trập dưới dạng hàm lambda theo sau, nhưng theo mặc định, màn trập sẽ là một @Composable Box màu đen lấp đầy kích thước của vùng chứa mẹ.

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 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ể đang tìm kiếm luồng Player.isPlaying mà bạn có thể collect theo cách nhận biết vòng đời. Hoặc một số thứ 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 luồng 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 việc chuyển dữ liệu. Chúng ta muốn đạt được độ trễ thấp nhất có thể giữa 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ử trê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ủ 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 đó (bằng combine) cho mỗi phần tử trên 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 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 rơi vào trạng thái không hợp lệ.

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

Bạn có thể thêm 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 thông thường sẽ làm những việc sau:

  1. Nhận một đối tượng 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 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 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 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 linh động trước 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.

Điều gì xảy ra trong hệ thố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 mình, 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 bước 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 cần phải tìm hiểu về Player.Events.