Dodaj obraz w obrazie do swojej aplikacji za pomocą odtwarzacza wideo do tworzenia wiadomości

Tryb obrazu w obrazie (PIP) to specjalny rodzaj trybu wielu okien używany głównie do do odtwarzania filmów. Pozwala użytkownikowi obejrzeć film w małym oknie przypiętym w rogu ekranu podczas przechodzenia między aplikacjami lub przeglądania treści na ekranie głównym.

Obraz w obrazie wykorzystuje interfejsy API trybu wielu okien dostępne na Androidzie 7.0, przypiętego okna nakładki wideo. Aby dodać obraz w obrazie do aplikacji, musisz zarejestrować aktywności, w razie potrzeby przełączaj ją na tryb obrazu w obrazie i upewnij się, że elementy interfejsu są ukryte, a film jest odtwarzany w trybie PIP.

Z tego przewodnika dowiesz się, jak dodać PiP w Compose do swojej aplikacji za pomocą implementacji Compose Video. Zobacz aplikację Społecznościowe, aby zobaczyć najlepsze w praktyce.

Konfigurowanie aplikacji pod kątem obrazu w obrazie

W tagu aktywności w pliku AndroidManifest.xml wykonaj te czynności:

  1. Dodaj supportsPictureInPicture i ustaw wartość true, aby zadeklarować, że będziesz za pomocą funkcji obraz w obrazie w aplikacji.
  2. Dodaj configChanges i ustaw tę wartość na orientation|screenLayout|screenSize|smallestScreenSize, aby określić, że aktywność obsługuje zmiany konfiguracji układu. Dzięki temu Twoja aktywność nie jest uruchamiany ponownie po zmianie układu w trakcie przechodzenia w trybie PIP.

      <activity
        android:name=".SnippetsActivity"
        android:exported="true"
        android:supportsPictureInPicture="true"
        android:configChanges="orientation|screenLayout|screenSize|smallestScreenSize"
        android:theme="@style/Theme.Snippets">

W kodzie tworzenia wykonaj te czynności:

  1. Dodaj to rozszerzenie na stronie Context. Użyjesz tego rozszerzenia wiele razy w trakcie całego przewodnika.
    internal fun Context.findActivity(): ComponentActivity {
        var context = this
        while (context is ContextWrapper) {
            if (context is ComponentActivity) return context
            context = context.baseContext
        }
        throw IllegalStateException("Picture in picture should be called in the context of an Activity")
    }

Dodawanie obrazu w obrazie podczas opuszczania aplikacji na Androidzie 12

Aby dodać obraz w obrazie na urządzeniach z systemem starszym niż Android 12, użyj addOnUserLeaveHintProvider. Obserwuj Aby dodać obraz w obrazie na urządzeniach z systemem starszym niż Android 12:

  1. Dodaj bramkę wersji, aby dostęp do tego kodu był możliwy tylko w wersjach O aż do R.
  2. Użyj klucza DisposableEffect z kluczem Context.
  3. W DisposableEffect określ zachowanie, które ma być wykonywane, gdy funkcja onUserLeaveHintProvider zostanie wywołana za pomocą funkcji lambda. W funkcji lambda wywołaj funkcję enterPictureInPictureMode() w obiekcie findActivity() i przekaż parametr PictureInPictureParams.Builder().build().
  4. Dodaj addOnUserLeaveHintListener za pomocą funkcji findActivity() i przekazuj do niej wyrażenie lambda.
  5. W usłudze onDispose dodaj użytkownika removeOnUserLeaveHintListener za pomocą funkcji findActivity() i przepowiem lambda.

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
    Build.VERSION.SDK_INT < Build.VERSION_CODES.S
) {
    val context = LocalContext.current
    DisposableEffect(context) {
        val onUserLeaveBehavior: () -> Unit = {
            context.findActivity()
                .enterPictureInPictureMode(PictureInPictureParams.Builder().build())
        }
        context.findActivity().addOnUserLeaveHintListener(
            onUserLeaveBehavior
        )
        onDispose {
            context.findActivity().removeOnUserLeaveHintListener(
                onUserLeaveBehavior
            )
        }
    }
} else {
    Log.i("PiP info", "API does not support PiP")
}

Dodanie PiP w aplikacji podczas opuszczania aplikacji na Androidzie 12

Po Androidzie 12 sekcja PictureInPictureParams.Builder jest dodawana za pomocą który jest przekazywany do odtwarzacza wideo aplikacji.

  1. Utwórz modifier i wywołaj za jego pomocą onGloballyPositioned. współrzędnych układu, które będą używane w dalszym kroku.
  2. Utwórz zmienną dla: PictureInPictureParams.Builder().
  3. Dodaj instrukcję if, by sprawdzić, czy pakiet SDK ma wersję S lub wyższą. Jeśli tak, dodaj setAutoEnterEnabled w kreatorze i ustaw na true, aby włączyć obraz w obrazie tryb po przesunięciu palcem. Zapewnia to płynniejszą animację niż przejście przez cały czas. enterPictureInPictureMode
  4. Użyj kodu findActivity(), aby zadzwonić pod numer setPictureInPictureParams(). Zadzwoń pod numer build() na builder i przekaż informacje.

val pipModifier = modifier.onGloballyPositioned { layoutCoordinates ->
    val builder = PictureInPictureParams.Builder()

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
        builder.setAutoEnterEnabled(true)
    }
    context.findActivity().setPictureInPictureParams(builder.build())
}
VideoPlayer(pipModifier)

Dodaj obraz w obrazie za pomocą przycisku

Aby przejść do trybu obrazu w obrazie, kliknij przycisk enterPictureInPictureMode() na findActivity().

Te parametry są już ustawiane przez poprzednie wywołania funkcji PictureInPictureParams.Builder, więc nie musisz ustawiać nowych parametrów w kreatorze. Jeśli jednak chcesz zmienić parametry przycisku, kliknij, aby ustawić je tutaj.

val context = LocalContext.current
Button(onClick = {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        context.findActivity().enterPictureInPictureMode(
            PictureInPictureParams.Builder().build()
        )
    } else {
        Log.i(PIP_TAG, "API does not support PiP")
    }
}) {
    Text(text = "Enter PiP mode!")
}

Obsługuj interfejs w trybie obrazu w pionie

Po włączeniu trybu obrazu w obrazie cały interfejs aplikacji pojawi się w oknie tej funkcji, chyba że określ, jak powinien wyglądać interfejs w trybie PIP.

Przede wszystkim musisz wiedzieć, kiedy aplikacja jest w trybie PIP. Za pomocą OnPictureInPictureModeChangedProvider. Poniższy kod informuje, czy aplikacja działa w trybie PIP.

@Composable
fun rememberIsInPipMode(): Boolean {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        val activity = LocalContext.current.findActivity()
        var pipMode by remember { mutableStateOf(activity.isInPictureInPictureMode) }
        DisposableEffect(activity) {
            val observer = Consumer<PictureInPictureModeChangedInfo> { info ->
                pipMode = info.isInPictureInPictureMode
            }
            activity.addOnPictureInPictureModeChangedListener(
                observer
            )
            onDispose { activity.removeOnPictureInPictureModeChangedListener(observer) }
        }
        return pipMode
    } else {
        return false
    }
}

Za pomocą rememberIsInPipMode() możesz teraz wybierać elementy interfejsu, które mają się wyświetlać gdy aplikacja przejdzie w tryb PIP:

val inPipMode = rememberIsInPipMode()

Column(modifier = modifier) {
    // This text will only show up when the app is not in PiP mode
    if (!inPipMode) {
        Text(
            text = "Picture in Picture",
        )
    }
    VideoPlayer()
}

Upewnij się, że aplikacja przechodzi w tryb obrazu w obrazie we właściwym czasie

Aplikacja nie powinna przechodzić w tryb obrazu w pliku w tych sytuacjach:

  • czy odtwarzanie zostało zatrzymane lub wstrzymane;
  • Jeśli jesteś na innej stronie aplikacji niż odtwarzacz wideo:

Aby kontrolować, kiedy aplikacja przechodzi w tryb PiP, dodaj zmienną, która śledzi stan odtwarzacza za pomocą mutableStateOf.

przełączać stan w zależności od tego, czy odtwarzany jest film;

Aby przełączać stan w zależności od tego, czy odtwarza się film, dodaj detektor w odtwarzaczu wideo. Przełącz stan zmiennej stanu w zależności od tego, czy gracz gra:

player.addListener(object : Player.Listener {
    override fun onIsPlayingChanged(isPlaying: Boolean) {
        shouldEnterPipMode = isPlaying
    }
})

Przełącz stan w zależności od tego, czy gracz jest zwolniony

Po uwolnieniu odtwarzacza ustaw zmienną stanu na false:

fun releasePlayer() {
    shouldEnterPipMode = false
}

Używanie stanu do określania, czy ma być włączony tryb obrazu w obrazie (starsze wersje Androida 12)

  1. Dodawanie obrazu w obrazie sprzed 12 roku wymaga użycia DisposableEffect, więc musisz utworzyć nową zmienną według wymiaru rememberUpdatedState z wartością newValue ustawioną jako „stan”. Dzięki temu w ramach DisposableEffect zostanie użyta zaktualizowana wersja.
  2. W parametrze lambda określającym działanie funkcji OnUserLeaveHintListener dodaj instrukcję if ze zmienną stanu wokół wywołania funkcji enterPictureInPictureMode():

    val currentShouldEnterPipMode by rememberUpdatedState(newValue = shouldEnterPipMode)
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
        Build.VERSION.SDK_INT < Build.VERSION_CODES.S
    ) {
        val context = LocalContext.current
        DisposableEffect(context) {
            val onUserLeaveBehavior: () -> Unit = {
                if (currentShouldEnterPipMode) {
                    context.findActivity()
                        .enterPictureInPictureMode(PictureInPictureParams.Builder().build())
                }
            }
            context.findActivity().addOnUserLeaveHintListener(
                onUserLeaveBehavior
            )
            onDispose {
                context.findActivity().removeOnUserLeaveHintListener(
                    onUserLeaveBehavior
                )
            }
        }
    } else {
        Log.i("PiP info", "API does not support PiP")
    }

Użycie stanu do określenia, czy włączono tryb obrazu w obrazie (po Androidzie 12)

Przekazuj zmienną stanu do funkcji setAutoEnterEnabled, tak aby aplikacja trafiała tylko do Tryb obraz w obrazie w odpowiednim momencie:

val pipModifier = modifier.onGloballyPositioned { layoutCoordinates ->
    val builder = PictureInPictureParams.Builder()

    // Add autoEnterEnabled for versions S and up
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
        builder.setAutoEnterEnabled(shouldEnterPipMode)
    }
    context.findActivity().setPictureInPictureParams(builder.build())
}

VideoPlayer(pipModifier)

Użyj polecenia setSourceRectHint, aby zaimplementować płynną animację

Interfejs API setSourceRectHint tworzy płynniejszą animację podczas włączania trybu PiP. W Androidzie 12 i nowszych ta funkcja zapewnia też płynniejszą animację przy zamykaniu trybu obraz w obrazie. Dodaj ten interfejs API do konstruktora obrazu w obrazie, aby wskazać obszar działania, który jest widoczne po przejściu do obrazu w obrazie.

  1. Dodaj setSourceRectHint() do builder tylko wtedy, gdy stan określa, że aplikacja powinna przejść do trybu PiP. Pozwala to uniknąć obliczania wartości sourceRect, gdy aplikacja nie muszą wprowadzać funkcji PIP.
  2. Aby ustawić wartość sourceRect, użyj podanego parametru layoutCoordinates z funkcji onGloballyPositioned na modyfikatorze.
  3. Zadzwoń do setSourceRectHint() na builder i wjedź w sourceRect .

val context = LocalContext.current

val pipModifier = modifier.onGloballyPositioned { layoutCoordinates ->
    val builder = PictureInPictureParams.Builder()
    if (shouldEnterPipMode) {
        val sourceRect = layoutCoordinates.boundsInWindow().toAndroidRectF().toRect()
        builder.setSourceRectHint(sourceRect)
    }

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
        builder.setAutoEnterEnabled(shouldEnterPipMode)
    }
    context.findActivity().setPictureInPictureParams(builder.build())
}

VideoPlayer(pipModifier)

Użyj setAspectRatio, aby ustawić współczynnik proporcji okna PIP

Aby ustawić format obrazu okna PiP, możesz wybrać konkretny format obrazu lub użyć szerokości i wysokości rozmiaru filmu odtwarzacza. Jeśli jesteś używając odtwarzacza media3, sprawdź, czy odtwarzacz nie ma wartości null oraz przed ustawieniem proporcji rozmiar filmu nie jest równy VideoSize.UNKNOWN współczynnik proporcji.

val context = LocalContext.current

val pipModifier = modifier.onGloballyPositioned { layoutCoordinates ->
    val builder = PictureInPictureParams.Builder()
    if (shouldEnterPipMode && player != null && player.videoSize != VideoSize.UNKNOWN) {
        val sourceRect = layoutCoordinates.boundsInWindow().toAndroidRectF().toRect()
        builder.setSourceRectHint(sourceRect)
        builder.setAspectRatio(
            Rational(player.videoSize.width, player.videoSize.height)
        )
    }

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
        builder.setAutoEnterEnabled(shouldEnterPipMode)
    }
    context.findActivity().setPictureInPictureParams(builder.build())
}

VideoPlayer(pipModifier)

Jeśli korzystasz z odtwarzacza niestandardowego, ustaw format obrazu na jego wysokości i szerokości, użyj składni charakterystycznej dla Twojego odtwarzacza. Pamiętaj, że jeśli Twój odtwarzacz zmienia rozmiar podczas inicjowania, jeśli wykracza poza dozwolony zakres w jakim formacie obrazu, aplikacja ulega awarii. Konieczne może być dodanie weryfikacji gdy współczynnik proporcji można obliczać, podobnie jak w przypadku mediów3. .

Dodaj działania zdalne

Jeśli chcesz dodać elementy sterujące (odtwarzanie, wstrzymywanie itp.) do okna PIP, utwórz RemoteAction dla każdego ustawienia, które chcesz dodać.

  1. Dodaj stałe wartości dla elementów sterujących transmisją:
    // Constant for broadcast receiver
    const val ACTION_BROADCAST_CONTROL = "broadcast_control"
    
    // Intent extras for broadcast controls from Picture-in-Picture mode.
    const val EXTRA_CONTROL_TYPE = "control_type"
    const val EXTRA_CONTROL_PLAY = 1
    const val EXTRA_CONTROL_PAUSE = 2
  2. Utwórz listę RemoteActions elementów sterujących w oknie PiP.
  3. Następnie dodaj BroadcastReceiver i zastąp onReceive(), aby ustawić działania każdego z przycisków. Aby zarejestrować odbiornik i działania zdalne, użyj DisposableEffect. Po utylizacji odtwarzacza wyrejestruj odbiorcy.
    @RequiresApi(Build.VERSION_CODES.O)
    @Composable
    fun PlayerBroadcastReceiver(player: Player?) {
        val isInPipMode = rememberIsInPipMode()
        if (!isInPipMode || player == null) {
            // Broadcast receiver is only used if app is in PiP mode and player is non null
            return
        }
        val context = LocalContext.current
    
        DisposableEffect(player) {
            val broadcastReceiver: BroadcastReceiver = object : BroadcastReceiver() {
                override fun onReceive(context: Context?, intent: Intent?) {
                    if ((intent == null) || (intent.action != ACTION_BROADCAST_CONTROL)) {
                        return
                    }
    
                    when (intent.getIntExtra(EXTRA_CONTROL_TYPE, 0)) {
                        EXTRA_CONTROL_PAUSE -> player.pause()
                        EXTRA_CONTROL_PLAY -> player.play()
                    }
                }
            }
            ContextCompat.registerReceiver(
                context,
                broadcastReceiver,
                IntentFilter(ACTION_BROADCAST_CONTROL),
                ContextCompat.RECEIVER_NOT_EXPORTED
            )
            onDispose {
                context.unregisterReceiver(broadcastReceiver)
            }
        }
    }
  4. Przekaż do PictureInPictureParams.Builder listę swoich czynności zdalnych:
    val context = LocalContext.current
    
    val pipModifier = modifier.onGloballyPositioned { layoutCoordinates ->
        val builder = PictureInPictureParams.Builder()
        builder.setActions(
            listOfRemoteActions()
        )
    
        if (shouldEnterPipMode && player != null && player.videoSize != VideoSize.UNKNOWN) {
            val sourceRect = layoutCoordinates.boundsInWindow().toAndroidRectF().toRect()
            builder.setSourceRectHint(sourceRect)
            builder.setAspectRatio(
                Rational(player.videoSize.width, player.videoSize.height)
            )
        }
    
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
            builder.setAutoEnterEnabled(shouldEnterPipMode)
        }
        context.findActivity().setPictureInPictureParams(builder.build())
    }
    VideoPlayer(modifier = pipModifier)

Dalsze kroki

W tym przewodniku przedstawiliśmy sprawdzone metody dodawania obrazu w obrazie podczas tworzenia starszych niż Android 12 i nowszych wersji Androida 12.