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

Obraz w obrazie (PIP) to specjalny rodzaj trybu wielu okien używany głównie do odtwarzania filmów. Umożliwia użytkownikowi oglądanie filmu w małym oknie przypiętym do rogu ekranu podczas nawigowania między aplikacjami lub przeglądania treści na ekranie głównym.

Funkcja PiP wykorzystuje interfejsy API wielu okien dostępne w Androidzie 7.0, aby wyświetlać przypięte okno nakładki wideo. Aby dodać obraz w obrazie w aplikacji, musisz zarejestrować swoją aktywność, w razie potrzeby przełączyć ją na tryb obrazu w obrazie i upewnić się, że elementy interfejsu są ukryte, a odtwarzanie filmu będzie kontynuowane, gdy aktywność jest w tym trybie.

Z tego przewodnika dowiesz się, jak dodać obraz w obrazie w sekcji Utwórz do aplikacji za pomocą implementacji wideo w funkcji Utwórz. Te sprawdzone metody w praktyce znajdziesz w aplikacji socialite.

Konfigurowanie funkcji PIP w aplikacji

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

  1. Dodaj supportsPictureInPicture i ustaw jego wartość true, aby zadeklarować, że będziesz używać PiP w aplikacji.
  2. Dodaj właściwość configChanges i ustaw ją jako orientation|screenLayout|screenSize|smallestScreenSize, aby określić, że Twoja aktywność ma obsługiwać zmiany konfiguracji układu. Dzięki temu aktywność nie zostanie ponownie uruchomiona, gdy zmieni się układ podczas przenoszenia trybu obrazu w obrazie.

      <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. Aby uzyskać dostęp do aktywności, będziesz używać tego rozszerzenia wiele razy w całym przewodniku.
    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 funkcji PIP w aplikacji na Androida w wersji wcześniejszej niż 12

Aby dodać obraz w obrazie w przypadku systemu starszego niż Android 12, użyj narzędzia addOnUserLeaveHintProvider. Aby dodać obraz w obrazie w wersji starszej na Androida 12, wykonaj te czynności:

  1. Dodaj bramę wersji, aby dostęp do tego kodu był możliwy tylko od wersji O do R.
  2. Użyj klucza DisposableEffect z kluczem Context.
  3. W elemencie DisposableEffect zdefiniuj działanie wywoływane w przypadku wywołania funkcji onUserLeaveHintProvider za pomocą funkcji lambda. W funkcji lambda wywołaj metodę enterPictureInPictureMode() w elemencie findActivity() i przekaż go w elemencie PictureInPictureParams.Builder().build().
  4. Dodaj funkcję addOnUserLeaveHintListener, używając funkcji findActivity(), i przekaż parametr lambda.
  5. W funkcji onDispose dodaj funkcję removeOnUserLeaveHintListener, używając parametru findActivity() i przekaż go w elemencie 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_TAG, "API does not support PiP")
}

Dodawanie obrazu w obrazie w aplikacji w przypadku rezygnacji z Androida 12

Po wprowadzeniu Androida 12 element PictureInPictureParams.Builder jest dodawany przez modyfikator przekazywany do odtwarzacza wideo w aplikacji.

  1. Utwórz modifier i wywołaj na nim onGloballyPositioned. Współrzędne układu przydadzą się w późniejszym kroku.
  2. Utwórz zmienną dla kolumny PictureInPictureParams.Builder().
  3. Dodaj instrukcję if, aby sprawdzić, czy pakiet SDK ma wersję S lub wyższą. Jeśli tak, dodaj parametr setAutoEnterEnabled do kreatora i ustaw go na true, aby włączać tryb obrazu w obrazie po przesunięciu palcem. Zapewnia to płynność animacji niż enterPictureInPictureMode.
  4. Użyj aplikacji findActivity(), by zadzwonić pod numer setPictureInPictureParams(). Zadzwoń do build() na builder i przekaż je.

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)

Dodawanie obrazu w obrazie za pomocą przycisku

Aby włączyć tryb obrazu w obrazie kliknięciem przycisku, wywołaj metodę enterPictureInPictureMode() w findActivity().

Te parametry są już ustawiane przez poprzednie wywołania funkcji PictureInPictureParams.Builder, więc nie musisz ustawiać nowych parametrów w konstruktorze. Jeśli jednak zechcesz zmienić parametry kliknięcia przycisku, możesz je tutaj ustawić.

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ługa UI w trybie PIP

Gdy włączysz tryb obraz w obrazie, cały interfejs Twojej aplikacji pojawi się w tym oknie, chyba że określisz, jak ma wyglądać interfejs w tym trybie i jak z niego wyjść.

Po pierwsze musisz wiedzieć, czy aplikacja działa w trybie PIP. Aby to zrobić, użyj narzędzia OnPictureInPictureModeChangedProvider. Poniższy kod informuje, czy Twoja aplikacja działa w trybie obrazu w obrazie.

@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ą być wyświetlane, gdy aplikacja przechodzi w tryb obrazu w obrazie:

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 włącza tryb obraz w obrazie w odpowiednich momentach

Tryb obraz w obrazie aplikacji nie powinien włączać się w tych sytuacjach:

  • Zatrzymanie lub wstrzymanie filmu.
  • Jeśli jesteś na innej stronie aplikacji niż odtwarzacz wideo:

Aby kontrolować, kiedy aplikacja ma przechodzić w tryb obrazu w obrazie, dodaj zmienną śledzącą stan odtwarzacza za pomocą mutableStateOf.

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

Aby przełączać stan w zależności od tego, czy odtwarzacz jest odtwarzany, dodaj do niego detektor. Przełącz stan zmiennej stanu w zależności od tego, czy odtwarzacz odtwarza treści:

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

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

Po zwolnieniu odtwarzacza ustaw zmienną stanową na false:

fun releasePlayer() {
    shouldEnterPipMode = false
}

Użyj stanu, aby określić, czy tryb obrazu w obrazie ma być włączony (starszy niż 12)

  1. Dodanie parametru PIP sprzed 12. roku używa parametru DisposableEffect, więc musisz utworzyć nową zmienną za pomocą rememberUpdatedState, podając newValue jako zmienną stanu. Dzięki temu zaktualizowana wersja będzie używana w: DisposableEffect.
  2. Do funkcji lambda, która określa działanie po wywołaniu wywołania OnUserLeaveHintListener, dodaj instrukcję if ze zmienną stanową otaczającą wywołanie 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_TAG, "API does not support PiP")
    }

Użyj stanu, aby określić, czy tryb obraz w obrazie ma być włączony (po Androidzie 12)

Przekaż zmienną stanową do obiektu setAutoEnterEnabled, aby aplikacja wchodziła w tryb PiP tylko 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 setSourceRectHint w celu zaimplementowania płynnej animacji

Interfejs API setSourceRectHint tworzy płynniejszą animację przejścia w tryb obrazu w obrazie. W Androidzie 12 i nowszych tworzy ona również płynniejszą animację wychodzenia z trybu obrazu w obrazie. Dodaj ten interfejs API do kreatora funkcji PIP, aby wskazać obszar aktywności, który będzie widoczny po przejściu do tej funkcji.

  1. Dodaj setSourceRectHint() do interfejsu builder tylko wtedy, gdy stan określa, że aplikacja powinna przechodzić w tryb obrazu w obrazie. Pozwala to uniknąć obliczania sourceRect, gdy aplikacja nie musi korzystać z trybu PIP.
  2. Aby ustawić wartość sourceRect, użyj layoutCoordinates z funkcji onGloballyPositioned na modyfikatorze.
  3. Wywołaj setSourceRectHint() w builder i przekaż zmienną 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ć format obrazu okna PIP

Aby ustawić współczynnik proporcji okna obrazu w obrazie, możesz wybrać konkretny współczynnik lub użyć szerokości i wysokości zmiennej 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)
        builder.setAspectRatio(
            Rational(sourceRect.width(), sourceRect.height())
        )
    }

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

VideoPlayer(pipModifier)

Dodaj działania zdalne

Jeśli chcesz dodać elementy sterujące (odtwarzanie, wstrzymywanie itp.) do okna obrazu w obrazie, utwórz RemoteAction dla każdego elementu sterującego, który chcesz dodać.

  1. Dodaj stałe do sterowania 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 dla elementów sterujących w oknie obrazu w obrazie.
  3. Następnie dodaj BroadcastReceiver i zastąp onReceive(), aby określić działania każdego przycisku. Aby zarejestrować odbiornik i działania zdalne, użyj DisposableEffect. Po zutylizowaniu odtwarzacza wyrejestruj odbiornik.
    @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ż listę działań zdalnych do PictureInPictureParams.Builder:
    val context = LocalContext.current
    
    val pipModifier = modifier.onGloballyPositioned { layoutCoordinates ->
        val builder = PictureInPictureParams.Builder()
        builder.setActions(
            listOfRemoteActions()
        )
    
        if (shouldEnterPipMode) {
            val sourceRect = layoutCoordinates.boundsInWindow().toAndroidRectF().toRect()
            builder.setSourceRectHint(sourceRect)
            builder.setAspectRatio(
                Rational(sourceRect.width(), sourceRect.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 poznaliśmy sprawdzone metody dodawania obrazu w obrazie do tworzenia wiadomości zarówno w wersji sprzed Androida 12, jak i nowszej.