Oluştur video oynatıcısıyla uygulamanıza pencere içinde pencere (PIP) ekleme

Pencere içinde pencere (PIP), çoğunlukla video oynatma için kullanılan özel bir çoklu pencere modu türüdür. Kullanıcının uygulamalar arasında gezinirken veya ana ekranda içeriklere göz atarken ekranın bir köşesine sabitlenmiş küçük bir pencerede video izlemesini sağlar.

PiP, sabitlenmiş video yer paylaşımı penceresi sağlamak için Android 7.0'da kullanıma sunulan çoklu pencere API'lerinden yararlanır. Uygulamanıza PiP eklemek için etkinliğinizi kaydetmeniz, gerektiğinde etkinliğinizi PiP moduna geçirmeniz, kullanıcı arayüzü öğelerinin gizlendiğinden ve etkinlik PiP modundayken video oynatmanın devam ettiğinden emin olmanız gerekir.

Bu kılavuzda, Compose video uygulamasıyla uygulamanıza Compose'da PiP'nin nasıl ekleneceği açıklanmaktadır. Bu en iyi uygulamaları uygulamalı olarak görmek için Socialite uygulamasına bakın.

Uygulamanızı PiP için ayarlama

AndroidManifest.xml dosyanızın etkinlik etiketinde aşağıdakileri yapın:

  1. Uygulamanızda PiP kullanacağınızı belirtmek için supportsPictureInPicture ekleyip bu bilgiyi true olarak ayarlayın.
  2. Etkinliğinizin düzen yapılandırma değişikliklerini gerçekleştirmesini belirtmek için configChanges ekleyip orientation|screenLayout|screenSize|smallestScreenSize olarak ayarlayın. Bu sayede, PiP modu geçişleri sırasında düzen değişiklikleri olduğunda etkinliğiniz yeniden başlatılmaz.

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

Oluşturma kodunuzda aşağıdakileri yapın:

  1. Bu uzantıyı Context sitesine ekleyin. Etkinliğe erişmek için kılavuz boyunca bu uzantıyı birden çok kez kullanacaksınız.
    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")
    }

Android 12 öncesi sürüm için izin verilen uygulamaya PiP ekleme

Android 12 öncesi için PiP eklemek istiyorsanız addOnUserLeaveHintProvider kullanın. Android 12 öncesi için PiP eklemek üzere şu adımları uygulayın:

  1. Bu koda yalnızca R tarihine kadar O sürümlerinde erişilebilmesi için bir sürüm kapısı ekleyin.
  2. Anahtar olarak Context içeren bir DisposableEffect kullanın.
  3. DisposableEffect içinde, lambda kullanılarak onUserLeaveHintProvider tetiklendiğinde sergilenecek davranışı tanımlayın. Lambda'da, findActivity() üzerinden enterPictureInPictureMode() numarasını arayın ve PictureInPictureParams.Builder().build() numarasını iletin.
  4. findActivity() kullanarak addOnUserLeaveHintListener ekleyin ve lambdayı geçin.
  5. onDispose oyununda, findActivity() kullanarak removeOnUserLeaveHintListener değerini ekleyin ve lambda'yı geçin.

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

Android sonrası 12 sürümü için izin verilen uygulamaya PiP ekleyin

Android 12'den sonra PictureInPictureParams.Builder, uygulamanın video oynatıcısına iletilen bir düzenleyici aracılığıyla eklenir.

  1. Bir modifier oluşturun ve buna ilişkin onGloballyPositioned numarasını arayın. Düzen koordinatları sonraki bir adımda kullanılacaktır.
  2. PictureInPictureParams.Builder() için bir değişken oluşturun.
  3. SDK'nın S veya daha yüksek bir sürüm olup olmadığını kontrol etmek için if ifadesi ekleyin. Bu durumda, oluşturucuya setAutoEnterEnabled ekleyin ve kaydırma işleminden sonra PiP moduna girmek için bunu true olarak ayarlayın. Böylece, enterPictureInPictureMode üzerinden geçmekten daha akıcı bir animasyon sağlanır.
  4. setPictureInPictureParams() numaralı telefonu aramak için findActivity() numaralı telefonu kullanın. builder üzerinden build() numarasını arayıp gönderin.

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)

Düğme aracılığıyla PiP ekleme

PiP moduna tek bir düğmeye tıklayarak girmek için findActivity() numaralı telefondan enterPictureInPictureMode() numaralı telefonu arayın.

Parametreler, PictureInPictureParams.Builder öğesine yapılan önceki çağrılar tarafından zaten ayarlandığı için oluşturucuda yeni parametreler ayarlamanız gerekmez. Ancak, düğme tıklandığında herhangi bir parametreyi değiştirmek isterseniz bu parametreleri burada ayarlayabilirsiniz.

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!")
}

Kullanıcı arayüzünüzü PiP modunda işleme

PiP moduna girdiğinizde, kullanıcı arayüzünüzün PiP modunda nasıl görüneceğini belirtmediğiniz sürece uygulamanızın tüm kullanıcı arayüzü PiP penceresine girer.

Öncelikle, uygulamanızın ne zaman PiP modunda olduğunu bilmeniz gerekir. Bunun için OnPictureInPictureModeChangedProvider kullanabilirsiniz. Aşağıdaki kod, uygulamanızın PiP modunda olup olmadığını belirtir.

@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
    }
}

Artık uygulama PiP moduna girdiğinde gösterilecek kullanıcı arayüzü öğelerini değiştirmek için rememberIsInPipMode() kullanabilirsiniz:

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

Uygulamanızın doğru zamanlarda PiP moduna girdiğinden emin olun

Uygulamanız aşağıdaki durumlarda PiP moduna geçmemelidir:

  • Videonun durdurulup durdurulduğu veya duraklatıldığı.
  • Uygulamanın, video oynatıcıdan farklı bir sayfasındaysanız.

Uygulamanızın ne zaman PiP moduna girdiğini kontrol etmek için mutableStateOf kullanarak video oynatıcının durumunu izleyen bir değişken ekleyin.

Videonun oynatılma durumuna göre açma/kapatma durumu

Video oynatıcının oynatılma durumuna göre durumu değiştirmek için video oynatıcıya bir işleyici ekleyin. Oynatıcının oynayıp oynamadığına bağlı olarak durum değişkeninizin durumunu değiştirin:

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

Oynatıcının serbest bırakılıp bırakılmadığına bağlı olarak durum değiştirme

Oynatıcı serbest bırakıldığında durum değişkeninizi false olarak ayarlayın:

fun releasePlayer() {
    shouldEnterPipMode = false
}

PiP moduna girilip girilmeyeceğini tanımlamak için durumu kullan (Android 12 öncesi)

  1. 12 öncesi PiP'yi eklerken DisposableEffect kullanıldığından, rememberUpdatedState tarihine kadar durum değişkeni olarak newValue ayarlanmış bir değişken oluşturmanız gerekir. Bu işlem, güncellenmiş sürümün DisposableEffect içinde kullanılmasını sağlar.
  2. OnUserLeaveHintListener tetiklendiğinde sergilediği davranışı tanımlayan lambda'da, enterPictureInPictureMode() çağrısının etrafına durum değişkeni içeren bir if ifadesi ekleyin:

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

PiP moduna girilip girilmeyeceğini tanımlamak için durumu kullan (Android 12 sonrası)

Eyalet değişkeninizi setAutoEnterEnabled öğesine iletin. Böylece uygulamanız yalnızca doğru zamanda PiP moduna girer:

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)

Akıcı bir animasyon uygulamak için setSourceRectHint kullanma

setSourceRectHint API, PiP moduna girmek için daha yumuşak bir animasyon oluşturur. Bu özellik, Android 12 ve sonraki sürümlerde PiP modundan çıkmak için daha akıcı bir animasyon da oluşturur. PiP'ye geçişten sonra etkinliğin görünür olan alanını belirtmek için bu API'yi PiP oluşturucuya ekleyin.

  1. setSourceRectHint() öğesini builder öğesine yalnızca durum, uygulamanın PiP moduna girmesi gerektiğini tanımlıyorsa ekleyin. Bu, uygulamanın PiP'ye girmesi gerekmediğinde sourceRect hesaplamasının önüne geçer.
  2. sourceRect değerini ayarlamak için değiştiricideki onGloballyPositioned işlevinden verilen layoutCoordinates özelliğini kullanın.
  3. builder öğesine setSourceRectHint() yöntemini çağırın ve sourceRect değişkenini iletin.

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)

PiP penceresinin en boy oranını ayarlamak için setAspectRatio öğesini kullanın

PiP penceresinin en boy oranını ayarlamak için belirli bir en boy oranı seçebilir veya en boy oranını ayarlamak için sourceRect değişkeninin genişliğini ve yüksekliğini kullanabilirsiniz.

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)

Uzaktan işlem ekle

PiP pencerenize kontroller (oynatma, duraklatma vb.) eklemek isterseniz eklemek istediğiniz her kontrol için bir RemoteAction oluşturun.

  1. Yayın kontrolleriniz için sabit değerler ekleyin:
    // 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. PiP pencerenizdeki kontroller için bir RemoteActions listesi oluşturun.
  3. Daha sonra, bir BroadcastReceiver ekleyin ve her bir düğmenin işlemlerini ayarlamak için onReceive() öğesini geçersiz kılın. Alıcıyı ve uzak işlemleri kaydetmek için bir DisposableEffect kullanın. Oynatıcı imha edildiğinde alıcının kaydını silin.
    @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. Uzaktan gerçekleştirdiğiniz işlemlerin listesini PictureInPictureParams.Builder'a aktarın:
    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)

Sonraki adımlar

Bu kılavuzda, Compose'da hem Android 12 öncesi hem de Android 12 sonrası sürümler için PiP eklemeyle ilgili en iyi uygulamaları öğrendiniz.

  • Composer PiP ile ilgili en iyi uygulamaları çalışırken görmek için Socialite uygulamasına göz atın.
  • Daha fazla bilgi için PIP tasarım kılavuzuna bakın.