refactor: pip optimize
This commit is contained in:
@@ -25,6 +25,7 @@
|
||||
|
||||
## 已知问题
|
||||
|
||||
- 因为没有设备用于(也懒得)测试,应用可能无法正常运行在安卓版本较低的设备上,特别是画中画功能,非常取决于国产 ROM 的实现
|
||||
- 关闭画中画后不会停止 scrcpy 串流,仍然需要回到应用中点击停止
|
||||
- 虚拟按键的截图实现方式为发送
|
||||
`keycode 120`,安卓官方([keycodes.h#349](https://android.googlesource.com/platform/frameworks/native/+/master/include/android/keycodes.h#349))的定义为
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
package io.github.miuzarte.scrcpyforandroid
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.app.PictureInPictureUiState
|
||||
import android.app.RemoteAction
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.drawable.Icon
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.app.PictureInPictureParamsCompat
|
||||
import androidx.core.pip.BasicPictureInPicture
|
||||
import androidx.core.pip.PictureInPictureDelegate
|
||||
import io.github.miuzarte.scrcpyforandroid.pages.StreamScreen
|
||||
import io.github.miuzarte.scrcpyforandroid.services.PictureInPictureActionReceiver
|
||||
import io.github.miuzarte.scrcpyforandroid.widgets.VideoOutputTarget
|
||||
@@ -20,26 +21,19 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
class StreamActivity : ComponentActivity() {
|
||||
private lateinit var basicPip: BasicPictureInPicture // = this
|
||||
private val basicPip = BasicPictureInPicture(this)
|
||||
|
||||
// pip 是否已被配置、允许进入
|
||||
private var pipConfigured: Boolean = false
|
||||
private var pipParams = PictureInPictureParamsCompat
|
||||
.Builder()
|
||||
.setEnabled(false)
|
||||
.build()
|
||||
|
||||
// 是否处于 PiP
|
||||
// MIUI 上进入 PiP 时,动画事件比 onPictureInPictureModeChanged() 更稳定,
|
||||
// 所以进入 PiP 直接由动画事件置为 true
|
||||
// 是否处于 pip
|
||||
// 回到全屏时会因重建而变回初始值
|
||||
private val _pipModeState = MutableStateFlow(false)
|
||||
val pipModeState: StateFlow<Boolean> = _pipModeState
|
||||
|
||||
val pipStopAction: RemoteAction by lazy {
|
||||
val intent = Intent(this, PictureInPictureActionReceiver::class.java).apply {
|
||||
action = PictureInPictureActionReceiver.ACTION_STOP_SCRCPY
|
||||
`package` = packageName
|
||||
}
|
||||
val intent = Intent(this, PictureInPictureActionReceiver::class.java)
|
||||
.apply {
|
||||
action = PictureInPictureActionReceiver.ACTION_STOP_SCRCPY
|
||||
`package` = packageName
|
||||
}
|
||||
val pendingIntent = PendingIntent.getBroadcast(
|
||||
this,
|
||||
1,
|
||||
@@ -54,27 +48,31 @@ class StreamActivity : ComponentActivity() {
|
||||
)
|
||||
}
|
||||
|
||||
// 每次 进出全屏/进出画中画
|
||||
// 都会重建 activity
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
basicPip = BasicPictureInPicture(this)
|
||||
|
||||
// 声明要画中画
|
||||
basicPip.setEnabled(true)
|
||||
|
||||
setContent {
|
||||
StreamScreen(activity = this)
|
||||
}
|
||||
|
||||
/*
|
||||
// 可能以后有用
|
||||
basicPip.addOnPictureInPictureEventListener(
|
||||
executor = mainExecutor,
|
||||
listener = object : PictureInPictureDelegate.OnPictureInPictureEventListener {
|
||||
override fun onPictureInPictureEvent(
|
||||
event: PictureInPictureDelegate.Event,
|
||||
newConfig: Configuration?,
|
||||
config: Configuration?,
|
||||
) {
|
||||
// HyperOS 3 下稳定收到的是进入动画事件
|
||||
// 这里直接把进入动画开始视为已经进入 PiP,尽量少依赖平台回调
|
||||
// MIUI 只有这些事件
|
||||
when (event) {
|
||||
PictureInPictureDelegate.Event.ENTER_ANIMATION_START -> {
|
||||
_pipModeState.value = true
|
||||
VideoOutputTargetState.set(VideoOutputTarget.PICTURE_IN_PICTURE)
|
||||
}
|
||||
|
||||
PictureInPictureDelegate.Event.ENTER_ANIMATION_END -> {
|
||||
_pipModeState.value = true
|
||||
}
|
||||
PictureInPictureDelegate.Event.ENTER_ANIMATION_START -> {}
|
||||
PictureInPictureDelegate.Event.ENTER_ANIMATION_END -> {}
|
||||
|
||||
PictureInPictureDelegate.Event.STASHED -> {}
|
||||
PictureInPictureDelegate.Event.UNSTASHED -> {}
|
||||
@@ -86,49 +84,18 @@ class StreamActivity : ComponentActivity() {
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
setContent {
|
||||
StreamScreen(activity = this)
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
fun configurePictureInPicture(
|
||||
enabled: Boolean,
|
||||
params: PictureInPictureParamsCompat,
|
||||
) {
|
||||
// 由 StreamScreen 决定何时更新 PiP 参数;
|
||||
// StreamActivity 这里只负责缓存并转发最新配置
|
||||
pipConfigured = enabled
|
||||
pipParams = params
|
||||
basicPip
|
||||
.setEnabled(enabled)
|
||||
.setPictureInPictureParams(params)
|
||||
}
|
||||
|
||||
fun enterConfiguredPip(): Boolean {
|
||||
// onUserLeaveHint() 可能发生在 PiP 还没准备好之前,
|
||||
// 所以这里先做一次兜底判断
|
||||
if (!pipConfigured) return false
|
||||
return runCatching {
|
||||
enterPictureInPictureMode(pipParams)
|
||||
true
|
||||
}.getOrElse {
|
||||
VideoOutputTargetState.set(VideoOutputTarget.FULLSCREEN)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onUserLeaveHint() {
|
||||
super.onUserLeaveHint()
|
||||
|
||||
enterConfiguredPip()
|
||||
fun configurePip(params: PictureInPictureParamsCompat) {
|
||||
basicPip.setPictureInPictureParams(params)
|
||||
}
|
||||
|
||||
/*
|
||||
// 回到全屏也会停止, 暂时不做
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
|
||||
// 回到全屏也会停止
|
||||
/*
|
||||
if (_pipModeState.value) {
|
||||
Thread {
|
||||
runBlocking {
|
||||
@@ -136,19 +103,28 @@ class StreamActivity : ComponentActivity() {
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
*/
|
||||
}
|
||||
*/
|
||||
|
||||
// MIUI 不进
|
||||
override fun onPictureInPictureModeChanged(
|
||||
isInPictureInPictureMode: Boolean,
|
||||
newConfig: Configuration,
|
||||
) {
|
||||
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
|
||||
//- onPictureInPictureModeChanged
|
||||
//+ onPictureInPictureUiStateChanged
|
||||
//- onUserLeaveHint
|
||||
|
||||
if (!isInPictureInPictureMode) {
|
||||
_pipModeState.value = false
|
||||
VideoOutputTargetState.set(VideoOutputTarget.FULLSCREEN)
|
||||
@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
|
||||
override fun onPictureInPictureUiStateChanged(pipState: PictureInPictureUiState) {
|
||||
super.onPictureInPictureUiStateChanged(pipState)
|
||||
|
||||
when {
|
||||
// 进入画中画
|
||||
pipState.isTransitioningToPip -> if (!_pipModeState.value) {
|
||||
_pipModeState.value = true
|
||||
VideoOutputTargetState.set(VideoOutputTarget.PICTURE_IN_PICTURE)
|
||||
}
|
||||
// 收进边缘
|
||||
pipState.isStashed -> if (!_pipModeState.value) {
|
||||
_pipModeState.value = true
|
||||
VideoOutputTargetState.set(VideoOutputTarget.PICTURE_IN_PICTURE)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -80,6 +80,10 @@ internal fun AboutScreen() {
|
||||
val lazyListState = rememberLazyListState()
|
||||
var logoHeightPx by remember { mutableIntStateOf(0) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
AppUpdateChecker.ensureChecked(BuildConfig.VERSION_NAME)
|
||||
}
|
||||
|
||||
val scrollProgress by remember {
|
||||
derivedStateOf {
|
||||
if (logoHeightPx <= 0) {
|
||||
|
||||
@@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.MoreVert
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -238,6 +239,8 @@ fun DeviceTabPage(
|
||||
|
||||
var audioForwardingSupported by rememberSaveable { mutableStateOf(true) }
|
||||
var cameraMirroringSupported by rememberSaveable { mutableStateOf(true) }
|
||||
var pendingScrollToPreview by rememberSaveable { mutableStateOf(false) }
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
val currentTarget =
|
||||
if (currentTargetHost.isNotBlank())
|
||||
@@ -528,6 +531,7 @@ fun DeviceTabPage(
|
||||
suspend fun startScrcpySession() {
|
||||
val options = scrcpyOptions.toClientOptions(soBundleShared).fix()
|
||||
val session = scrcpy.start(options)
|
||||
pendingScrollToPreview = true
|
||||
if (options.disableScreensaver) {
|
||||
setKeepScreenOn(true)
|
||||
}
|
||||
@@ -553,6 +557,15 @@ fun DeviceTabPage(
|
||||
snackbar.show("scrcpy 已启动")
|
||||
}
|
||||
|
||||
LaunchedEffect(pendingScrollToPreview, sessionInfo) {
|
||||
if (!pendingScrollToPreview) return@LaunchedEffect
|
||||
val session = sessionInfo ?: return@LaunchedEffect
|
||||
if (session.width <= 0 || session.height <= 0) return@LaunchedEffect
|
||||
// status/device list/scrcpy panel are above the preview card
|
||||
listState.animateScrollToItem(index = 3)
|
||||
pendingScrollToPreview = false
|
||||
}
|
||||
|
||||
suspend fun handleAdbConnected(host: String, port: Int) {
|
||||
currentTargetHost = host
|
||||
currentTargetPort = port
|
||||
@@ -707,6 +720,7 @@ fun DeviceTabPage(
|
||||
LazyColumn(
|
||||
contentPadding = contentPadding,
|
||||
scrollBehavior = scrollBehavior,
|
||||
state = listState,
|
||||
) {
|
||||
item {
|
||||
StatusCard(
|
||||
|
||||
@@ -77,14 +77,12 @@ fun StreamScreen(activity: StreamActivity) {
|
||||
).also { ratio ->
|
||||
lastPipAspectRatio = ratio
|
||||
}
|
||||
activity.configurePictureInPicture(
|
||||
enabled = true,
|
||||
params = PictureInPictureParamsCompat.Builder()
|
||||
|
||||
activity.configurePip(
|
||||
PictureInPictureParamsCompat.Builder()
|
||||
.setEnabled(true)
|
||||
.setAspectRatio(pipAspectRatio)
|
||||
.setSourceRectHint(
|
||||
if (!isInPip) pipSourceRectHint else null
|
||||
)
|
||||
.setSourceRectHint(pipSourceRectHint)
|
||||
.setSeamlessResizeEnabled(true)
|
||||
.setCloseAction(activity.pipStopAction)
|
||||
.build(),
|
||||
|
||||
@@ -44,7 +44,7 @@ class AppSettings(context: Context) : Settings(context, "AppSettings") {
|
||||
)
|
||||
val DEVICE_PREVIEW_CARD_HEIGHT_DP = Pair(
|
||||
intPreferencesKey("device_preview_card_height_dp"),
|
||||
320
|
||||
1080/3
|
||||
)
|
||||
val PREVIEW_VIRTUAL_BUTTON_SHOW_TEXT = Pair(
|
||||
booleanPreferencesKey("preview_virtual_button_show_text"),
|
||||
|
||||
@@ -764,17 +764,27 @@ fun ScrcpyVideoSurface(
|
||||
session: Scrcpy.Session.SessionInfo?,
|
||||
target: VideoOutputTarget,
|
||||
) {
|
||||
|
||||
val taskScope = remember { CoroutineScope(SupervisorJob() + Dispatchers.IO) }
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
var currentSurface by remember { mutableStateOf<Surface?>(null) }
|
||||
var currentSurfaceView by remember { mutableStateOf<SurfaceView?>(null) }
|
||||
val scope = rememberCoroutineScope()
|
||||
val taskScope = remember { CoroutineScope(SupervisorJob() + Dispatchers.IO) }
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
val currentTarget by VideoOutputTargetState.current.collectAsState()
|
||||
val latestSession by rememberUpdatedState(session)
|
||||
val latestCurrentTarget by rememberUpdatedState(currentTarget)
|
||||
|
||||
LaunchedEffect(session?.width, session?.height, currentSurfaceView) {
|
||||
val currentTarget by VideoOutputTargetState.current.collectAsState()
|
||||
val latestCurrentTarget by rememberUpdatedState(currentTarget)
|
||||
val latestSession by rememberUpdatedState(session)
|
||||
val latestRequestedTarget by rememberUpdatedState(target)
|
||||
|
||||
LaunchedEffect(session?.width, session?.height, currentSurfaceView, target) {
|
||||
val surfaceView = currentSurfaceView ?: return@LaunchedEffect
|
||||
if (target == VideoOutputTarget.PICTURE_IN_PICTURE) {
|
||||
// In PiP, let SurfaceView buffer follow viewport to avoid stale portrait frame crop.
|
||||
surfaceView.holder.setSizeFromLayout()
|
||||
return@LaunchedEffect
|
||||
}
|
||||
|
||||
val currentSession = session ?: return@LaunchedEffect
|
||||
if (currentSession.width > 0 && currentSession.height > 0) {
|
||||
surfaceView.holder.setFixedSize(currentSession.width, currentSession.height)
|
||||
@@ -782,8 +792,8 @@ fun ScrcpyVideoSurface(
|
||||
}
|
||||
|
||||
LaunchedEffect(session, currentSurface, currentTarget, target) {
|
||||
val surface = currentSurface
|
||||
if (currentTarget == target && session != null && surface != null && surface.isValid) {
|
||||
val surface = currentSurface ?: return@LaunchedEffect
|
||||
if (currentTarget == target && session != null && surface.isValid) {
|
||||
NativeCoreFacade.attachVideoSurface(surface)
|
||||
}
|
||||
}
|
||||
@@ -844,9 +854,14 @@ fun ScrcpyVideoSurface(
|
||||
height: Int,
|
||||
) {
|
||||
if (width <= 0 || height <= 0) return
|
||||
if (!holder.surface.isValid) return
|
||||
val surface = holder.surface
|
||||
if (!surface.isValid) return
|
||||
currentSurface = surface
|
||||
if (latestCurrentTarget == latestRequestedTarget && latestSession != null) {
|
||||
scope.launch {
|
||||
NativeCoreFacade.attachVideoSurface(surface)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun surfaceDestroyed(holder: SurfaceHolder) {
|
||||
@@ -940,7 +955,7 @@ internal fun DeviceTile(
|
||||
.weight(1f),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
// statu dot
|
||||
// status dot
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(8.dp)
|
||||
|
||||
@@ -351,6 +351,9 @@ class VirtualButtonBar(
|
||||
maxX * offsetXFraction.coerceIn(0f, 1f)
|
||||
val currentY =
|
||||
maxY * offsetYFraction.coerceIn(0f, 1f)
|
||||
val popupAlignment =
|
||||
if (offsetXFraction > 0.5f) PopupPositionProvider.Align.TopEnd
|
||||
else PopupPositionProvider.Align.TopStart
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
@@ -422,6 +425,7 @@ class VirtualButtonBar(
|
||||
showActions = false
|
||||
},
|
||||
renderInRootScaffold = true,
|
||||
popupAlignment = popupAlignment,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -434,6 +438,7 @@ class VirtualButtonBar(
|
||||
onDismiss: () -> Unit,
|
||||
onAction: suspend (VirtualButtonAction) -> Unit,
|
||||
renderInRootScaffold: Boolean,
|
||||
popupAlignment: PopupPositionProvider.Align = PopupPositionProvider.Align.TopEnd,
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val haptics = LocalAppHaptics.current
|
||||
@@ -456,7 +461,7 @@ class VirtualButtonBar(
|
||||
OverlayListPopup(
|
||||
show = show,
|
||||
popupPositionProvider = ListPopupDefaults.ContextMenuPositionProvider,
|
||||
alignment = PopupPositionProvider.Align.TopEnd,
|
||||
alignment = popupAlignment,
|
||||
onDismissRequest = onDismiss,
|
||||
renderInRootScaffold = renderInRootScaffold,
|
||||
enableWindowDim = false,
|
||||
|
||||
Reference in New Issue
Block a user