diff --git a/README.md b/README.md index 21dadcb..5cb4f82 100644 --- a/README.md +++ b/README.md @@ -16,11 +16,11 @@ - 可替换 scrcpy-server - 利用 mDNS 服务实现自动连接启用无线调试的设备、自动发现等待配对设备的IP与端口 +- 自动横竖屏切换(算吗 -## 已知问题 +## 已知问题 / TODO -- 多指触控抬起后滞留 -- 快速离开再进入全屏会导致视频流关键帧丢失 +- 组件排序动画一坨 ## 构建 diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ddcf9da..d7c4cac 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -23,8 +23,8 @@ android { applicationId = "io.github.miuzarte.scrcpyforandroid" minSdk = 26 targetSdk = 36 - versionCode = 3 - versionName = "0.0.3" + versionCode = 4 + versionName = "0.0.4" externalNativeBuild { cmake { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 25fe758..fcd8427 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -18,6 +18,7 @@ diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/NativeCoreFacade.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/NativeCoreFacade.kt index 720c9c3..9ba0d51 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/NativeCoreFacade.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/NativeCoreFacade.kt @@ -33,6 +33,9 @@ class NativeCoreFacade(private val appContext: Context) { private val mainHandler = Handler(Looper.getMainLooper()) private val bootstrapLock = Any() private val bootstrapPackets = ArrayDeque() + + @Volatile + private var latestConfigPacket: CachedPacket? = null private var packetCount: Long = 0 @Volatile @@ -181,6 +184,7 @@ class NativeCoreFacade(private val appContext: Context) { releaseAllDecoders() synchronized(bootstrapLock) { bootstrapPackets.clear() + latestConfigPacket = null } if (!request.noVideo) { surfaceMap.forEach { (tag, surface) -> @@ -221,6 +225,7 @@ class NativeCoreFacade(private val appContext: Context) { releaseAllDecoders() synchronized(bootstrapLock) { bootstrapPackets.clear() + latestConfigPacket = null } currentSessionInfo = null sessionManager.clearVideoConsumer() @@ -578,6 +583,20 @@ class NativeCoreFacade(private val appContext: Context) { isKeyFrame = packet.isKeyFrame, ) synchronized(bootstrapLock) { + if (cached.isConfig) { + latestConfigPacket = cached + bootstrapPackets.clear() + bootstrapPackets.addLast(cached) + return + } + + if (cached.isKeyFrame) { + bootstrapPackets.clear() + latestConfigPacket?.let { bootstrapPackets.addLast(it) } + bootstrapPackets.addLast(cached) + return + } + while (bootstrapPackets.size >= MAX_BOOTSTRAP_PACKETS) { bootstrapPackets.removeFirst() } diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/nativecore/AdbPairingTypes.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/nativecore/AdbPairingTypes.kt index 47dbc1d..8f0e6e6 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/nativecore/AdbPairingTypes.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/nativecore/AdbPairingTypes.kt @@ -161,8 +161,8 @@ internal class AdbPairingKey( } } -private const val ANDROID_PUBKEY_MODULUS_SIZE = 2048 / 8 -private const val ANDROID_PUBKEY_MODULUS_SIZE_WORDS = ANDROID_PUBKEY_MODULUS_SIZE / 4 +private const val ANDROID_PUBKEY_MODULUS_SIZE = 2048 / 8 // 256 +private const val ANDROID_PUBKEY_MODULUS_SIZE_WORDS = ANDROID_PUBKEY_MODULUS_SIZE / 4 // 64 private const val RSA_PUBLIC_KEY_SIZE = 524 private fun BigInteger.toAdbEncoded(): IntArray { diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/nativecore/DirectAdbClient.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/nativecore/DirectAdbClient.kt index 08cd96a..ed0deff 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/nativecore/DirectAdbClient.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/nativecore/DirectAdbClient.kt @@ -190,21 +190,8 @@ internal class DirectAdbConnection( ) : AutoCloseable { private val sha1DigestInfoPrefix = byteArrayOf( - 0x30, - 0x21, - 0x30, - 0x09, - 0x06, - 0x05, - 0x2B, - 0x0E, - 0x03, - 0x02, - 0x1A, - 0x05, - 0x00, - 0x04, - 0x14, + 0x30, 0x21, 0x30, 0x09, 0x06, 0x05, 0x2B, 0x0E, + 0x03, 0x02, 0x1A, 0x05, 0x00, 0x04, 0x14, ) private val socket = Socket() diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/FullscreenControlPage.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/FullscreenControlPage.kt index 8257e79..6d8474e 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/FullscreenControlPage.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/FullscreenControlPage.kt @@ -1,7 +1,7 @@ package io.github.miuzarte.scrcpyforandroid.pages import android.app.Activity -import android.content.pm.ActivityInfo +import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize @@ -42,8 +42,13 @@ fun FullscreenControlPage( virtualButtonsInMore: List, showDebugInfo: Boolean, showVirtualButtons: Boolean, + onVideoSizeChanged: (width: Int, height: Int) -> Unit, onDismiss: () -> Unit, ) { + // 全屏控制页不使用预测性返回手势, + // 省得处理解码的问题 + BackHandler(enabled = true, onBack = onDismiss) + val context = LocalContext.current val haptics = rememberAppHaptics() val activity = remember(context) { context as? Activity } @@ -56,9 +61,6 @@ fun FullscreenControlPage( moreActions = virtualButtonLayout.second, ) } - val initialOrientation = remember(activity) { - activity?.requestedOrientation ?: ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED - } var session by remember(launch) { mutableStateOf( ScrcpySessionInfo( @@ -72,18 +74,6 @@ fun FullscreenControlPage( } var currentFps by remember { mutableFloatStateOf(0f) } - DisposableEffect(activity, session.width, session.height) { - val targetOrientation = if (session.width >= session.height) { - ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE - } else { - ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT - } - activity?.requestedOrientation = targetOrientation - onDispose { - activity?.requestedOrientation = initialOrientation - } - } - DisposableEffect(activity) { val window = activity?.window if (window != null) { @@ -107,6 +97,7 @@ fun FullscreenControlPage( DisposableEffect(nativeCore) { val listener: (Int, Int) -> Unit = { w, h -> session = session.copy(width = w, height = h) + onVideoSizeChanged(w, h) } nativeCore.addVideoSizeListener(listener) onDispose { @@ -151,6 +142,7 @@ fun FullscreenControlPage( screenWidth = session.width, screenHeight = session.height, pressure = pressure, + actionButton = 0, buttons = buttons, ) }, diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/MainPage.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/MainPage.kt index e71e2b0..0038cfe 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/MainPage.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/MainPage.kt @@ -1,6 +1,8 @@ package io.github.miuzarte.scrcpyforandroid.pages +import android.app.Activity import android.content.Intent +import android.content.pm.ActivityInfo import android.net.Uri import androidx.activity.compose.PredictiveBackHandler import androidx.activity.compose.rememberLauncherForActivityResult @@ -17,6 +19,7 @@ import androidx.compose.material.icons.filled.Devices import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.filled.Settings import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf @@ -86,6 +89,10 @@ private sealed interface RootScreen : NavKey { @Composable fun MainPage() { val context = LocalContext.current + val activity = remember(context) { context as? Activity } + val initialOrientation = remember(activity) { + activity?.requestedOrientation ?: ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED + } val nativeCore = remember(context) { NativeCoreFacade.get(context.applicationContext) } val initialSettings = remember(context) { loadMainSettings(context) } val initialDeviceSettings = remember(context) { loadDevicePageSettings(context) } @@ -182,9 +189,26 @@ fun MainPage() { var openReorderDevicesAction by remember { mutableStateOf<(() -> Unit)?>(null) } var canClearLogs by remember { mutableStateOf(false) } var showDeviceMenu by rememberSaveable { mutableStateOf(false) } + var fullscreenOrientation by rememberSaveable { + mutableIntStateOf(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT) + } val themeMode = resolveThemeMode(themeBaseIndex, monetEnabled) val themeController = remember(themeMode) { ThemeController(colorSchemeMode = themeMode) } + DisposableEffect(activity) { + onDispose { + activity?.requestedOrientation = initialOrientation + } + } + + LaunchedEffect(activity, currentRootScreen, fullscreenOrientation) { + val targetOrientation = when (currentRootScreen) { + is RootScreen.Fullscreen -> fullscreenOrientation + else -> ActivityInfo.SCREEN_ORIENTATION_PORTRAIT + } + activity?.requestedOrientation = targetOrientation + } + LaunchedEffect( audioEnabled, audioCodec, @@ -232,9 +256,6 @@ fun MainPage() { nativeCore.setAdbKeyName(adbKeyName.ifBlank { AppDefaults.ADB_KEY_NAME }) } - val canNavigateBack = - rootBackStack.size > 1 || pagerState.currentPage != MainTabDestination.Device.ordinal - fun popRoot() { if (rootBackStack.size > 1) { rootBackStack.removeAt(rootBackStack.lastIndex) @@ -257,6 +278,9 @@ fun MainPage() { } } + val canNavigateBack = rootBackStack.size > 1 || + pagerState.currentPage != MainTabDestination.Device.ordinal + PredictiveBackHandler(enabled = canNavigateBack) { progress -> try { progress.collect { } @@ -467,6 +491,12 @@ fun MainPage() { }, onOpenAdvancedPage = { rootBackStack.add(RootScreen.Advanced) }, onOpenFullscreenPage = { session -> + fullscreenOrientation = + if (session.width >= session.height) { + ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE + } else { + ActivityInfo.SCREEN_ORIENTATION_PORTRAIT + } rootBackStack.add( RootScreen.Fullscreen( launch = FullscreenControlLaunch( @@ -511,6 +541,10 @@ fun MainPage() { onDevicePreviewCardHeightDpChange = { devicePreviewCardHeightDp = it.coerceAtLeast(120) }, + showFullscreenVirtualButtons = showFullscreenVirtualButtons, + onShowFullscreenVirtualButtonsChange = { + showFullscreenVirtualButtons = it + }, customServerUri = customServerUri, onPickServer = { picker.launch( @@ -692,6 +726,13 @@ fun MainPage() { virtualButtonsInMore = virtualButtonsInMore, showDebugInfo = fullscreenDebugInfoEnabled, showVirtualButtons = showFullscreenVirtualButtons, + onVideoSizeChanged = { width, height -> + fullscreenOrientation = if (width >= height) { + ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE + } else { + ActivityInfo.SCREEN_ORIENTATION_PORTRAIT + } + }, onDismiss = { popRoot() }, ) } diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/SettingsPage.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/SettingsPage.kt index 63886c8..a0a03a4 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/SettingsPage.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/SettingsPage.kt @@ -65,6 +65,8 @@ fun SettingsScreen( onKeepScreenOnWhenStreamingEnabledChange: (Boolean) -> Unit, devicePreviewCardHeightDp: Int, onDevicePreviewCardHeightDpChange: (Int) -> Unit, + showFullscreenVirtualButtons: Boolean, + onShowFullscreenVirtualButtonsChange: (Boolean) -> Unit, customServerUri: String?, onPickServer: () -> Unit, onClearServer: () -> Unit, @@ -138,6 +140,12 @@ fun SettingsScreen( ?.let { onDevicePreviewCardHeightDpChange(it.coerceAtLeast(120)) } }, ) + SuperSwitch( + title = "全屏显示虚拟按钮", + summary = "在全屏控制页底部显示返回键、主页键等虚拟按钮", + checked = showFullscreenVirtualButtons, + onCheckedChange = onShowFullscreenVirtualButtonsChange, + ) } SectionSmallTitle(text = "scrcpy-server") diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/DeviceWidgets.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/DeviceWidgets.kt index 3b8bfe3..2083ad3 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/DeviceWidgets.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/DeviceWidgets.kt @@ -27,7 +27,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons @@ -114,6 +113,8 @@ private object UiMotionActions { const val POINTER_UP = 6 } +private const val FULLSCREEN_TOUCH_LOG_TAG = "FullscreenTouch" + @Composable internal fun StatusCard( statusLine: String, @@ -647,7 +648,11 @@ fun FullscreenControlScreen( var touchAreaSize by remember { mutableStateOf(IntSize.Zero) } val activePointerIds = remember { linkedSetOf() } val activePointerPositions = remember { linkedMapOf() } + val activePointerDevicePositions = remember { linkedMapOf>() } + val pointerLabels = remember { linkedMapOf() } + var nextPointerLabel by remember { mutableIntStateOf(1) } var activeTouchCount by remember { mutableIntStateOf(0) } + var activeTouchDebug by remember { mutableStateOf("") } BackHandler(enabled = enableBackHandler, onBack = onDismiss) BoxWithConstraints( @@ -659,125 +664,161 @@ fun FullscreenControlScreen( return@pointerInteropFilter true } + val sessionAspect = if (session.height == 0) { + 16f / 9f + } else { + session.width.toFloat() / session.height.toFloat() + } + val containerWidth = touchAreaSize.width.toFloat() + val containerHeight = touchAreaSize.height.toFloat() + val containerAspect = containerWidth / containerHeight + val contentWidth: Float + val contentHeight: Float + if (sessionAspect > containerAspect) { + contentWidth = containerWidth + contentHeight = containerWidth / sessionAspect + } else { + contentHeight = containerHeight + contentWidth = containerHeight * sessionAspect + } + val contentLeft = (containerWidth - contentWidth) / 2f + val contentTop = (containerHeight - contentHeight) / 2f + + fun isInsideContent(rawX: Float, rawY: Float): Boolean { + return rawX in contentLeft..(contentLeft + contentWidth) && + rawY in contentTop..(contentTop + contentHeight) + } + fun mapToDevice(rawX: Float, rawY: Float): Pair { - val x = ((rawX / touchAreaSize.width) * session.width).roundToInt() + val normalizedX = ((rawX - contentLeft) / contentWidth).coerceIn(0f, 1f) + val normalizedY = ((rawY - contentTop) / contentHeight).coerceIn(0f, 1f) + val x = (normalizedX * (session.width - 1).coerceAtLeast(0)).roundToInt() .coerceIn(0, (session.width - 1).coerceAtLeast(0)) - val y = ((rawY / touchAreaSize.height) * session.height).roundToInt() + val y = (normalizedY * (session.height - 1).coerceAtLeast(0)).roundToInt() .coerceIn(0, (session.height - 1).coerceAtLeast(0)) return x to y } - fun syncActivePointersFromEvent(skipPointerId: Int? = null) { - for (i in 0 until event.pointerCount) { - val pointerId = event.getPointerId(i) - if (!activePointerIds.contains(pointerId) || pointerId == skipPointerId) continue - val px = event.getX(i) - val py = event.getY(i) - activePointerPositions[pointerId] = Offset(px, py) - val (x, y) = mapToDevice(px, py) - onInjectTouch( - UiMotionActions.MOVE, - pointerId.toLong(), - x, - y, - event.getPressure(i).coerceIn(0f, 1f), - 1 - ) + fun pointerLabel(pointerId: Int): Int { + val existing = pointerLabels[pointerId] + if (existing != null) { + return existing + } + val assigned = nextPointerLabel + nextPointerLabel += 1 + pointerLabels[pointerId] = assigned + return assigned + } + + fun refreshTouchDebug() { + if (activePointerIds.isEmpty()) { + activeTouchDebug = "" + return + } + activeTouchDebug = activePointerIds + .sortedBy { pointerLabel(it) } + .joinToString(separator = "\n") { pointerId -> + val label = pointerLabel(pointerId) + val pos = activePointerDevicePositions[pointerId] + if (pos == null) { + "#$label(id=$pointerId):?" + } else { + "#$label(id=$pointerId):${pos.first},${pos.second}" + } + } + } + + fun releasePointer(pointerId: Int, reason: String) { + if (!activePointerIds.contains(pointerId)) return + val pos = activePointerPositions[pointerId] ?: Offset.Zero + val (x, y) = mapToDevice(pos.x, pos.y) + // val label = pointerLabel(pointerId) + // Log.d( + // FULLSCREEN_TOUCH_LOG_TAG, + // "抬起($reason): pointer#$label(id=$pointerId) x=$x y=$y" + // ) + onInjectTouch(UiMotionActions.UP, pointerId.toLong(), x, y, 0f, 0) + activePointerIds -= pointerId + activePointerPositions.remove(pointerId) + activePointerDevicePositions.remove(pointerId) + pointerLabels.remove(pointerId) + } + + if (event.actionMasked == MotionEvent.ACTION_CANCEL) { + val toCancel = activePointerIds.toList() + for (pointerId in toCancel) { + releasePointer(pointerId, reason = "cancel") + } + activeTouchCount = activePointerIds.size + refreshTouchDebug() + return@pointerInteropFilter true + } + + val eventPointerIds = HashSet(event.pointerCount) + val eventPositions = HashMap(event.pointerCount) + val eventPressures = HashMap(event.pointerCount) + for (i in 0 until event.pointerCount) { + val pointerId = event.getPointerId(i) + eventPointerIds += pointerId + eventPositions[pointerId] = Offset(event.getX(i), event.getY(i)) + eventPressures[pointerId] = event.getPressure(i).coerceIn(0f, 1f) + } + + val disappearedPointers = activePointerIds.filter { it !in eventPointerIds } + for (pointerId in disappearedPointers) { + releasePointer(pointerId, reason = "missing") + } + + val endedPointerId = when (event.actionMasked) { + MotionEvent.ACTION_UP, MotionEvent.ACTION_POINTER_UP -> event.getPointerId(event.actionIndex) + else -> null + } + + val justPressed = HashSet() + for (i in 0 until event.pointerCount) { + val pointerId = event.getPointerId(i) + if (pointerId == endedPointerId) continue + val raw = eventPositions[pointerId] ?: continue + val pressure = eventPressures[pointerId] ?: 0f + if (!activePointerIds.contains(pointerId)) { + if (!isInsideContent(raw.x, raw.y)) continue + val (x, y) = mapToDevice(raw.x, raw.y) + // val label = pointerLabel(pointerId) + // Log.d( + // FULLSCREEN_TOUCH_LOG_TAG, + // "按下: pointer#$label(id=$pointerId) x=$x y=$y" + // ) + activePointerIds += pointerId + activePointerPositions[pointerId] = raw + activePointerDevicePositions[pointerId] = x to y + justPressed += pointerId + onInjectTouch(UiMotionActions.DOWN, pointerId.toLong(), x, y, pressure, 0) } } - when (event.actionMasked) { - MotionEvent.ACTION_DOWN -> { - activePointerIds.clear() - activePointerPositions.clear() - activeTouchCount = 0 - val (x, y) = mapToDevice(event.x, event.y) - val pointerId = event.getPointerId(0) - activePointerIds += pointerId - activePointerPositions[pointerId] = Offset(event.x, event.y) - activeTouchCount = activePointerIds.size - onInjectTouch( - UiMotionActions.DOWN, - pointerId.toLong(), - x, - y, - event.getPressure(0).coerceIn(0f, 1f), - 1 - ) - } - - MotionEvent.ACTION_POINTER_DOWN -> { - val index = event.actionIndex - val px = event.getX(index) - val py = event.getY(index) - val (x, y) = mapToDevice(px, py) - val pointerId = event.getPointerId(index) - activePointerIds += pointerId - activePointerPositions[pointerId] = Offset(px, py) - activeTouchCount = activePointerIds.size - onInjectTouch( - UiMotionActions.POINTER_DOWN, - pointerId.toLong(), - x, - y, - event.getPressure(index).coerceIn(0f, 1f), - 1 - ) - syncActivePointersFromEvent() - } - - MotionEvent.ACTION_MOVE -> { - for (i in 0 until event.pointerCount) { - val pointerId = event.getPointerId(i) - if (!activePointerIds.contains(pointerId)) continue - val px = event.getX(i) - val py = event.getY(i) - activePointerPositions[pointerId] = Offset(px, py) - val (x, y) = mapToDevice(px, py) - onInjectTouch( - UiMotionActions.MOVE, - pointerId.toLong(), - x, - y, - event.getPressure(i).coerceIn(0f, 1f), - 1 - ) - } - } - - MotionEvent.ACTION_POINTER_UP -> { - val index = event.actionIndex - val px = event.getX(index) - val py = event.getY(index) - val (x, y) = mapToDevice(px, py) - val pointerId = event.getPointerId(index) - onInjectTouch(UiMotionActions.POINTER_UP, pointerId.toLong(), x, y, 0f, 1) - activePointerIds -= pointerId - activePointerPositions.remove(pointerId) - activeTouchCount = activePointerIds.size - syncActivePointersFromEvent(skipPointerId = pointerId) - } - - MotionEvent.ACTION_UP -> { - val (x, y) = mapToDevice(event.x, event.y) - val pointerId = event.getPointerId(0) - onInjectTouch(UiMotionActions.UP, pointerId.toLong(), x, y, 0f, 1) - activePointerIds.clear() - activePointerPositions.clear() - activeTouchCount = 0 - } - - MotionEvent.ACTION_CANCEL -> { - for (pointerId in activePointerIds) { - val pos = activePointerPositions[pointerId] ?: Offset.Zero - val (x, y) = mapToDevice(pos.x, pos.y) - onInjectTouch(UiMotionActions.CANCEL, pointerId.toLong(), x, y, 0f, 0) - } - activePointerIds.clear() - activePointerPositions.clear() - activeTouchCount = 0 - } + for (i in 0 until event.pointerCount) { + val pointerId = event.getPointerId(i) + if (!activePointerIds.contains(pointerId)) continue + if (pointerId == endedPointerId) continue + if (pointerId in justPressed) continue + val raw = eventPositions[pointerId] ?: continue + val pressure = eventPressures[pointerId] ?: 0f + activePointerPositions[pointerId] = raw + val (x, y) = mapToDevice(raw.x, raw.y) + activePointerDevicePositions[pointerId] = x to y + onInjectTouch(UiMotionActions.MOVE, pointerId.toLong(), x, y, pressure, 0) } + + if (endedPointerId != null) { + val endPos = eventPositions[endedPointerId] + if (endPos != null) { + activePointerPositions[endedPointerId] = endPos + } + releasePointer(endedPointerId, reason = "event") + } + + activeTouchCount = activePointerIds.size + refreshTouchDebug() true } .onSizeChanged { touchAreaSize = it }, @@ -815,7 +856,7 @@ fun FullscreenControlScreen( modifier = Modifier .align(Alignment.TopStart) .padding(start = UiSpacing.CardContent, top = UiSpacing.CardContent) - .background(Color.Black.copy(alpha = 0.5f), RoundedCornerShape(25)) + .background(Color.Black.copy(alpha = 0.5f)) .padding(horizontal = UiSpacing.CardContent, vertical = UiSpacing.Medium), ) { Column(verticalArrangement = Arrangement.spacedBy(UiSpacing.Tiny)) { @@ -825,14 +866,19 @@ fun FullscreenControlScreen( fontSize = 13.sp, fontWeight = FontWeight.SemiBold, ) + @SuppressLint("DefaultLocale") + Text( + text = "FPS: ${String.format("%.1f", currentFps.coerceAtLeast(0f))}", + color = Color.White, + fontSize = 13.sp, + ) Text( text = "触点: $activeTouchCount", color = Color.White, fontSize = 13.sp, ) - @SuppressLint("DefaultLocale") - Text( - text = "FPS: ${String.format("%.1f", currentFps.coerceAtLeast(0f))}", + if (activeTouchDebug.isNotEmpty()) Text( + text = activeTouchDebug, color = Color.White, fontSize = 13.sp, )