Compose 동영상 플레이어로 앱에 PIP 모드 추가

PIP (Picture-in-picture)는 주로 멀티 윈도우 모드에서 사용되는 특수한 유형의 멀티 윈도우 모드입니다. 있습니다. 사용자가 홈 화면에 고정된 작은 창을 통해 Google Home 앱에서 앱을 탐색하거나 확인할 수 있습니다.

PIP는 Android 7.0에서 사용할 수 있는 멀티 윈도우 API를 활용하여 고정 동영상 오버레이 창을 제공합니다. 앱에 PIP 모드를 추가하려면 필요에 따라 활동을 PIP 모드로 전환하고 UI 요소가 활동이 PIP 모드일 때 동영상이 계속 재생됩니다.

이 가이드에서는 Compose 동영상을 사용하여 Compose의 PIP를 앱에 추가하는 방법을 설명합니다. 있습니다. Socialite 앱에서 살펴봤습니다

PIP용 앱 설정

AndroidManifest.xml 파일의 활동 태그에서 다음을 실행합니다.

  1. supportsPictureInPicture를 추가하고 true로 설정하여 다음을 선언하세요. 하세요.
  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">
    드림

Compose 코드에서 다음을 실행합니다.

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

Android 12 이전 버전용 출발 시 PIP 추가

Android 12 이전 버전의 PIP를 추가하려면 addOnUserLeaveHintProvider를 사용합니다. 팔로우 Android 12 이전 버전의 PIP를 추가하려면 다음 단계를 따르세요.

  1. R까지 버전 O에서만 이 코드에 액세스할 수 있도록 버전 게이트를 추가합니다.
  2. Context와 함께 DisposableEffect를 키로 사용합니다.
  3. DisposableEffect 내에서 onUserLeaveHintProvider는 람다를 사용하여 트리거됩니다. 람다에서 다음을 호출합니다. findActivity()에서 enterPictureInPictureMode()을(를) 받고 전달합니다. PictureInPictureParams.Builder().build()입니다.
  4. findActivity()를 사용하여 addOnUserLeaveHintListener를 추가하고 람다를 전달합니다.
  5. onDispose에서 findActivity()를 사용하여 removeOnUserLeaveHintListener를 추가합니다. 람다를 전달합니다.

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

Android 12 이후 출발 시 앱에서 PIP 추가

Android 12 이후에는 앱의 동영상 플레이어에 전달되는 수정자를 통해 PictureInPictureParams.Builder가 추가됩니다.

  1. modifier를 만들고 onGloballyPositioned를 호출합니다. 레이아웃 좌표는 이후 단계에서 사용됩니다.
  2. PictureInPictureParams.Builder()의 변수를 만듭니다.
  3. if 문을 추가하여 SDK가 S 이상인지 확인합니다. 그렇다면 빌더에 setAutoEnterEnabled를 설정하고 true로 설정하여 PIP 모드를 시작합니다. <ph type="x-smartling-void-element"><wbr /></ph> 이렇게 하면 스크롤하는 것보다 매끄러운 애니메이션이 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 모드를 시작하려면 findActivity()enterPictureInPictureMode().

매개변수는 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 모드에서 UI 처리

PIP 모드를 시작하면 앱의 전체 UI가 PIP 창으로 들어갑니다. PIP 모드에서 UI가 어떻게 보이는지 지정합니다.

먼저 앱이 언제 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를 사용하여 동영상 플레이어의

동영상 재생 여부에 따라 상태 전환

동영상 플레이어의 재생 여부에 따라 상태를 전환하려면 있습니다. 플레이어가 재생 중인지 여부에 따라 상태 변수의 상태를 전환합니다.

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

플레이어가 손을 놓았는지에 따라 상태 전환

플레이어가 해제되면 상태 변수를 false로 설정합니다.

fun releasePlayer() {
    shouldEnterPipMode = false
}

상태를 사용하여 PIP 모드로 전환되는지 정의 (Android 12 이전)

  1. 12 이전 PIP를 추가하려면 DisposableEffect를 사용하므로 newValue로 설정된 rememberUpdatedState의 새 변수 상태 변수로 사용할 수 있습니다. 이렇게 하면 업데이트된 버전이 DisposableEffect 내에서 사용됩니다.
  2. OnUserLeaveHintListener가 트리거될 때의 동작을 정의하는 람다에서 enterPictureInPictureMode() 호출 주위에 상태 변수가 있는 if 문을 추가합니다.

    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. builder에서 setSourceRectHint()를 호출하고 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 창의 가로세로 비율을 설정하려면 특정 가로세로 비율을 선택하거나 플레이어의 동영상 크기 너비와 높이를 사용하면 됩니다. 만약 미디어3 플레이어를 사용하는 경우 플레이어가 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. PIP 창에서 컨트롤의 RemoteActions 목록을 만듭니다.
  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)

다음 단계

이 가이드에서는 Compose에 PIP를 추가하는 방법과 Android 12 이전 버전과 Android 12 이후 버전을 출시합니다