เพิ่มการแสดงภาพซ้อนภาพ (PIP) ลงในแอปด้วยโปรแกรมเล่นวิดีโอ Compose

การแสดงภาพซ้อนภาพ (PIP) คือโหมดหลายหน้าต่างประเภทพิเศษที่ใช้สำหรับการเล่นวิดีโอเป็นส่วนใหญ่ ทำให้ผู้ใช้ดูวิดีโอในหน้าต่างขนาดเล็กที่ตรึงไว้ มุมของหน้าจอขณะไปยังแอปต่างๆ หรือเรียกดูเนื้อหาบน หน้าจอหลัก

PIP ใช้ประโยชน์จาก API หลายหน้าต่างที่มีให้ใช้งานใน Android 7.0 เพื่อให้บริการ หน้าต่างวางซ้อนวิดีโอที่ปักหมุดไว้ หากต้องการเพิ่ม PIP ลงในแอป คุณจะต้องลงทะเบียน กิจกรรม เปลี่ยนกิจกรรมเป็นโหมด PIP ตามความจำเป็น และตรวจสอบว่าองค์ประกอบ UI จะซ่อนไว้และเล่นวิดีโอต่อเมื่อกิจกรรมอยู่ในโหมด 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 ในแอปออกจากแอปสำหรับ Android 12 ล่วงหน้า

หากต้องการเพิ่ม PiP สำหรับ Android ก่อนเวอร์ชัน 12 ให้ใช้ addOnUserLeaveHintProvider ติดตาม ขั้นตอนเหล่านี้เพื่อเพิ่ม PIP สำหรับก่อน Android 12

  1. เพิ่มเกตเวอร์ชันเพื่อให้โค้ดนี้เข้าถึงได้เฉพาะในเวอร์ชัน O จนถึง R
  2. ใช้ DisposableEffect ที่มี Context เป็นคีย์
  3. ใน DisposableEffect ให้กำหนดลักษณะการทำงานเมื่อ onUserLeaveHintProvider จะถูกทริกเกอร์โดยใช้ lambda ใน lambda ให้เรียก enterPictureInPictureMode() ใน findActivity() และผ่านไป PictureInPictureParams.Builder().build()
  4. เพิ่ม addOnUserLeaveHintListener โดยใช้ findActivity() แล้วข้ามผ่าน lambda
  5. ใน onDispose ให้เพิ่ม removeOnUserLeaveHintListener โดยใช้ findActivity() และส่งค่า 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")
}

เพิ่ม PIP ในแอปออกจากแอปสำหรับโพสต์ Android 12

หลัง Android 12 จะมีการเพิ่ม PictureInPictureParams.Builder ผ่าน แป้นกดร่วมที่ส่งไปยังโปรแกรมเล่นวิดีโอของแอป

  1. สร้าง modifier แล้วเรียก onGloballyPositioned ในนั้น เลย์เอาต์ พิกัดจะนำไปใช้ในขั้นตอนถัดไป
  2. สร้างตัวแปรสำหรับ PictureInPictureParams.Builder()
  3. เพิ่มคำสั่ง if เพื่อตรวจสอบว่า SDK เป็น S หรือสูงกว่า หากใช่ ให้เพิ่ม 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!")
}

จัดการ UI ในโหมด PIP

เมื่อเข้าสู่โหมด PIP แล้ว UI ทั้งหมดของแอปจะเข้าสู่หน้าต่าง PIP เว้นแต่คุณจะ ระบุว่า UI ควรมีลักษณะอย่างไรทั้งในและนอกโหมด 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() เพื่อเปิด/ปิดองค์ประกอบ UI ที่จะแสดงได้แล้ว เมื่อแอปเข้าสู่โหมด 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()
}

ตรวจสอบว่าแอปเข้าสู่โหมด PIP ในเวลาที่เหมาะสม

แอปของคุณไม่ควรเข้าสู่โหมด PIP ในสถานการณ์ต่อไปนี้

  • หากวิดีโอหยุดหรือหยุดชั่วคราว
  • หากคุณอยู่ในหน้าอื่นของแอปซึ่งไม่ใช่โปรแกรมเล่นวิดีโอ

หากต้องการควบคุมเวลาที่แอปเข้าสู่โหมด PIP ให้เพิ่มตัวแปรที่ติดตามสถานะ ของโปรแกรมเล่นวิดีโอโดยใช้ mutableStateOf

สลับสถานะอิงตามว่าวิดีโอกำลังเล่นหรือไม่

หากต้องการสลับสถานะตามว่าวิดีโอเพลเยอร์กำลังเล่นอยู่หรือไม่ ให้เพิ่ม Listener ลงใน โปรแกรมเล่นวิดีโอ สลับสถานะของตัวแปรสถานะขึ้นอยู่กับว่าโปรแกรมเล่นวิดีโอ กำลังเล่นอยู่หรือไม่

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

สลับสถานะตามที่มีการเปิดใช้โปรแกรมเล่นหรือไม่

เมื่อปล่อยโปรแกรมเล่นวิดีโอแล้ว ให้กำหนดตัวแปรสถานะเป็น false

fun releasePlayer() {
    shouldEnterPipMode = false
}

แสดงสถานะเพื่อระบุว่ามีการเข้าสู่โหมด PIP หรือไม่ (ก่อน Android 12)

  1. เนื่องจากการเพิ่ม PIP ก่อนปี 12 จะใช้ DisposableEffect คุณจึงต้องสร้าง ตัวแปรใหม่โดย rememberUpdatedState ที่ตั้งค่า newValue เป็น ตัวแปรสถานะ วิธีนี้จะช่วยให้มั่นใจได้ว่าระบบใช้เวอร์ชันที่อัปเดตแล้วภายใน DisposableEffect
  2. ใน Lambda ที่กําหนดลักษณะการทํางานเมื่อมีการเรียกใช้ OnUserLeaveHintListener ให้เพิ่มคำสั่ง if ที่มีตัวแปรสถานะรอบการเรียกใช้ 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 หรือไม่ (หลัง Android 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. เพิ่ม setSourceRectHint() ใน builder เฉพาะเมื่อรัฐระบุว่า แอปควรเข้าสู่โหมด PIP การดำเนินการนี้จะหลีกเลี่ยงการคำนวณ 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 ให้ตรวจสอบว่าโปรแกรมเล่นไม่ใช่ค่า Null และขนาดวิดีโอของโปรแกรมเล่นไม่เท่ากับ 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)

หากคุณใช้เพลเยอร์ที่กำหนดเอง ให้ตั้งค่าสัดส่วนภาพสำหรับความสูงและความกว้างของเพลเยอร์โดยใช้ไวยากรณ์เฉพาะสำหรับเพลเยอร์ โปรดทราบว่าหากเพลเยอร์ปรับขนาดระหว่างการเริ่มต้น หากอยู่นอกขอบเขตที่ยอมรับได้ของอัตราส่วนภาพ แอปจะขัดข้อง คุณอาจต้องเพิ่มการตรวจสอบ เมื่อคำนวณสัดส่วนการแสดงผลได้ คล้ายกับที่ทำกับสื่อ 3 โปรแกรมเล่นวิดีโอ

เพิ่มการดำเนินการจากระยะไกล

หากต้องการเพิ่มการควบคุม (เล่น หยุดชั่วคราว ฯลฯ) ลงในหน้าต่าง 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 ทั้งก่อนและหลัง Android 12