با پخش‌کننده ویدیوی Compose، تصویر در تصویر (PiP) را به برنامه خود اضافه کنید

تصویر در تصویر (PiP) نوع خاصی از حالت چند پنجره ای است که بیشتر برای پخش ویدئو استفاده می شود. این امکان را به کاربر می دهد که در یک پنجره کوچک که به گوشه ای از صفحه سنجاق شده است، هنگام حرکت بین برنامه ها یا مرور محتوا در صفحه اصلی، ویدیویی را تماشا کند.

PiP از APIهای چندپنجره‌ای که در Android 7.0 در دسترس هستند برای ارائه پنجره همپوشانی ویدئویی پین شده استفاده می‌کند. برای افزودن PiP به برنامه خود، باید فعالیت خود را ثبت کنید، فعالیت خود را در صورت نیاز به حالت PiP تغییر دهید و مطمئن شوید که عناصر رابط کاربری پنهان هستند و پخش ویدیو زمانی که فعالیت در حالت PiP است ادامه می یابد.

این راهنما نحوه اضافه کردن PiP در Compose را با اجرای ویدیوی Compose به برنامه خود توضیح می دهد. برای مشاهده این بهترین شیوه ها در عمل، به برنامه Socialite مراجعه کنید.

برنامه خود را برای PiP تنظیم کنید

در تگ فعالیت فایل AndroidManifest.xml ، موارد زیر را انجام دهید:

  1. supportsPictureInPicture را اضافه کنید و آن را روی true تنظیم کنید تا اعلام کنید از PiP در برنامه خود استفاده می کنید.
  2. configChanges را اضافه کنید و آن را روی orientation|screenLayout|screenSize|smallestScreenSize تنظیم کنید تا مشخص کنید که فعالیت شما تغییرات پیکربندی طرح را انجام دهد. به این ترتیب، وقتی تغییرات طرح‌بندی در طول انتقال حالت PiP رخ می‌دهد، فعالیت شما دوباره راه‌اندازی نمی‌شود.

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

در کد نوشتن، موارد زیر را انجام دهید:

  1. این پسوند را به Context اضافه کنید. از این برنامه افزودنی چندین بار در طول راهنما برای دسترسی به فعالیت استفاده خواهید کرد.
    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")
    }

برنامه PiP on مرخصی را برای نسخه پیش از اندروید 12 اضافه کنید

برای افزودن PiP برای نسخه پیش از اندروید 12، از addOnUserLeaveHintProvider استفاده کنید. برای افزودن PiP برای نسخه پیش از اندروید 12 این مراحل را دنبال کنید:

  1. یک نسخه گیت اضافه کنید تا این کد فقط در نسخه های O تا R قابل دسترسی باشد.
  2. از یک DisposableEffect با Context به عنوان کلید استفاده کنید.
  3. در داخل DisposableEffect ، رفتار زمانی که onUserLeaveHintProvider با استفاده از لامبدا راه اندازی می شود را تعریف کنید. در لامبدا، enterPictureInPictureMode() در findActivity() فراخوانی کنید و از PictureInPictureParams.Builder().build() عبور دهید.
  4. addOnUserLeaveHintListener با استفاده از findActivity() اضافه کنید و لامبدا را ارسال کنید.
  5. در onDispose ، removeOnUserLeaveHintListener با استفاده از findActivity() اضافه کنید و لامبدا را وارد کنید.

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

برنامه PiP on مرخصی را برای پس از اندروید 12 اضافه کنید

پس از اندروید 12، PictureInPictureParams.Builder از طریق یک اصلاح کننده که به پخش کننده ویدیوی برنامه ارسال می شود، اضافه می شود.

  1. یک modifier بسازید و روی آن با onGloballyPositioned تماس بگیرید. مختصات طرح در مرحله بعدی استفاده خواهد شد.
  2. یک متغیر برای PictureInPictureParams.Builder() ایجاد کنید.
  3. برای بررسی اینکه آیا SDK S یا بالاتر است، یک عبارت if اضافه کنید. اگر چنین است، setAutoEnterEnabled را به سازنده اضافه کنید و آن را روی true تنظیم کنید تا با کشیدن انگشت وارد حالت PiP شوید. این یک انیمیشن روان‌تر از ورود به enterPictureInPictureMode ارائه می‌کند.
  4. از findActivity() برای فراخوانی setPictureInPictureParams() استفاده کنید. build() را روی builder فراخوانی کنید و آن را ارسال کنید.

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)

PiP را از طریق یک دکمه اضافه کنید

برای ورود به حالت PiP با کلیک روی دکمه، enterPictureInPictureMode() را در findActivity() فراخوانی کنید.

پارامترها قبلاً توسط تماس های قبلی PictureInPictureParams.Builder تنظیم شده اند، بنابراین نیازی به تنظیم پارامترهای جدید در سازنده ندارید. با این حال، اگر می خواهید هر پارامتری را با کلیک روی دکمه تغییر دهید، می توانید آنها را در اینجا تنظیم کنید.

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

رابط کاربری خود را در حالت PiP مدیریت کنید

وقتی وارد حالت PiP می‌شوید، کل رابط کاربری برنامه شما وارد پنجره PiP می‌شود، مگر اینکه مشخص کنید رابط کاربری شما چگونه باید در حالت PiP و خارج از آن به نظر برسد.

ابتدا باید بدانید که برنامه شما در حالت PiP است یا نه. برای رسیدن به این هدف می توانید از OnPictureInPictureModeChangedProvider استفاده کنید. کد زیر به شما می گوید که آیا برنامه شما در حالت 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
    }
}

اکنون، می‌توانید از rememberIsInPipMode() استفاده کنید تا هنگام ورود برنامه به حالت PiP، عناصر UI را تغییر دهید:

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

مطمئن شوید که برنامه شما در زمان های مناسب وارد حالت PiP می شود

برنامه شما نباید در شرایط زیر وارد حالت PiP شود:

  • اگر ویدیو متوقف یا متوقف شود.
  • اگر در صفحه ای متفاوت از برنامه پخش کننده ویدیو هستید.

برای کنترل زمانی که برنامه شما وارد حالت PiP می شود، متغیری اضافه کنید که وضعیت پخش کننده ویدیو را با استفاده از mutableStateOf ردیابی کند.

وضعیت را بر اساس پخش ویدیو تغییر دهید

برای تغییر وضعیت بر اساس اینکه پخش کننده ویدیو در حال پخش است، یک شنونده در پخش کننده ویدیو اضافه کنید. وضعیت متغیر حالت خود را بر اساس اینکه بازیکن در حال بازی است یا خیر تغییر دهید:

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

وضعیت را بر اساس آزاد شدن بازیکن تغییر دهید

وقتی پخش کننده آزاد شد، متغیر حالت خود را روی false قرار دهید:

fun releasePlayer() {
    shouldEnterPipMode = false
}

استفاده از حالت برای تعیین اینکه آیا حالت PiP وارد شده است (قبل از اندروید 12)

  1. از آنجایی که افزودن PiP pre-12 از یک DisposableEffect استفاده می‌کند، باید یک متغیر جدید توسط rememberUpdatedState با newValue مجموعه‌ای به‌عنوان متغیر حالت ایجاد کنید. این اطمینان حاصل می کند که نسخه به روز شده در DisposableEffect استفاده می شود.
  2. در لامبدا که رفتار را هنگام راه‌اندازی OnUserLeaveHintListener تعریف می‌کند، یک دستور if با متغیر state در اطراف فراخوانی به 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")
    }

استفاده از حالت برای تعیین اینکه آیا حالت PiP وارد شده است (پس از اندروید 12)

متغیر حالت خود را به setAutoEnterEnabled منتقل کنید تا برنامه شما فقط در زمان مناسب وارد حالت PiP شود:

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)

از setSourceRectHint برای پیاده سازی یک انیمیشن روان استفاده کنید

setSourceRectHint API انیمیشن روان تری برای ورود به حالت PiP ایجاد می کند. در Android 12+، همچنین انیمیشن روان تری برای خروج از حالت PiP ایجاد می کند. این API را به سازنده PiP اضافه کنید تا ناحیه فعالیتی که پس از انتقال به PiP قابل مشاهده است را نشان دهید.

  1. فقط در صورتی که حالت تعریف می کند که برنامه باید وارد حالت PiP شود، setSourceRectHint() را به builder اضافه کنید. این از محاسبه sourceRect در زمانی که برنامه نیازی به وارد کردن PiP ندارد جلوگیری می کند.
  2. برای تنظیم مقدار sourceRect ، از layoutCoordinates که از تابع onGloballyPositioned در اصلاح کننده داده شده است استفاده کنید.
  3. setSourceRectHint() را در builder فراخوانی کنید و متغیر 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)

از setAspectRatio برای تنظیم نسبت ابعاد پنجره PiP استفاده کنید

برای تنظیم نسبت تصویر پنجره PiP، می توانید نسبت تصویر خاصی را انتخاب کنید یا از عرض و ارتفاع اندازه ویدیوی پخش کننده استفاده کنید. اگر از پخش کننده media3 استفاده می کنید، قبل از تنظیم نسبت تصویر، بررسی کنید که پخش کننده خالی نباشد و اندازه ویدیوی پخش کننده با VideoSize.UNKNOWN برابر نباشد.

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)

اگر از پخش کننده سفارشی استفاده می کنید، با استفاده از نحو مخصوص پخش کننده خود، نسبت ابعاد را بر روی ارتفاع و عرض پخش کننده تنظیم کنید. توجه داشته باشید که اگر اندازه پخش کننده شما در حین تنظیم اولیه تغییر اندازه دهد، اگر از محدوده معتبر نسبت ابعاد خارج شود، برنامه شما از کار می افتد. ممکن است لازم باشد بررسی هایی را در زمانی که می توان نسبت تصویر را محاسبه کرد، اضافه کنید، مشابه روشی که برای پخش کننده media3 انجام می شود.

افزودن اقدامات از راه دور

اگر می‌خواهید کنترل‌هایی (پخش، مکث و غیره) را به پنجره PiP خود اضافه کنید، برای هر کنترلی که می‌خواهید اضافه کنید، یک RemoteAction ایجاد کنید.

  1. ثابت هایی را برای کنترل های پخش خود اضافه کنید:
    // 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. فهرستی از RemoteActions برای کنترل‌های موجود در پنجره PiP خود ایجاد کنید.
  3. در مرحله بعد، یک BroadcastReceiver اضافه کنید و onReceive() را لغو کنید تا اعمال هر دکمه را تنظیم کنید. از یک DisposableEffect برای ثبت گیرنده و اقدامات راه دور استفاده کنید. وقتی پخش کننده از بین رفت، گیرنده را لغو ثبت کنید.
    @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. لیستی از اقدامات راه دور خود را به PictureInPictureParams.Builder ارسال کنید:
    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)

مراحل بعدی

در این راهنما بهترین روش‌های افزودن PiP در Compose را هم قبل از اندروید 12 و هم پس از اندروید 12 یاد گرفتید.

  • برای مشاهده بهترین شیوه های Compose PiP در عمل، به برنامه Socialite مراجعه کنید.
  • برای اطلاعات بیشتر به راهنمای طراحی PiP مراجعه کنید.