refactor: touch implementation, ratio switching
- 重写了触控实现 - 串流过程中自动切换横竖屏 - 禁用了全屏控制页中的预测性返回手势
This commit is contained in:
@@ -16,11 +16,11 @@
|
|||||||
|
|
||||||
- 可替换 scrcpy-server
|
- 可替换 scrcpy-server
|
||||||
- 利用 mDNS 服务实现自动连接启用无线调试的设备、自动发现等待配对设备的IP与端口
|
- 利用 mDNS 服务实现自动连接启用无线调试的设备、自动发现等待配对设备的IP与端口
|
||||||
|
- 自动横竖屏切换(算吗
|
||||||
|
|
||||||
## 已知问题
|
## 已知问题 / TODO
|
||||||
|
|
||||||
- 多指触控抬起后滞留
|
- 组件排序动画一坨
|
||||||
- 快速离开再进入全屏会导致视频流关键帧丢失
|
|
||||||
|
|
||||||
## 构建
|
## 构建
|
||||||
|
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ android {
|
|||||||
applicationId = "io.github.miuzarte.scrcpyforandroid"
|
applicationId = "io.github.miuzarte.scrcpyforandroid"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 3
|
versionCode = 4
|
||||||
versionName = "0.0.3"
|
versionName = "0.0.4"
|
||||||
|
|
||||||
externalNativeBuild {
|
externalNativeBuild {
|
||||||
cmake {
|
cmake {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
|
android:configChanges="keyboardHidden|orientation|screenSize|smallestScreenSize"
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|||||||
@@ -33,6 +33,9 @@ class NativeCoreFacade(private val appContext: Context) {
|
|||||||
private val mainHandler = Handler(Looper.getMainLooper())
|
private val mainHandler = Handler(Looper.getMainLooper())
|
||||||
private val bootstrapLock = Any()
|
private val bootstrapLock = Any()
|
||||||
private val bootstrapPackets = ArrayDeque<CachedPacket>()
|
private val bootstrapPackets = ArrayDeque<CachedPacket>()
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
private var latestConfigPacket: CachedPacket? = null
|
||||||
private var packetCount: Long = 0
|
private var packetCount: Long = 0
|
||||||
|
|
||||||
@Volatile
|
@Volatile
|
||||||
@@ -181,6 +184,7 @@ class NativeCoreFacade(private val appContext: Context) {
|
|||||||
releaseAllDecoders()
|
releaseAllDecoders()
|
||||||
synchronized(bootstrapLock) {
|
synchronized(bootstrapLock) {
|
||||||
bootstrapPackets.clear()
|
bootstrapPackets.clear()
|
||||||
|
latestConfigPacket = null
|
||||||
}
|
}
|
||||||
if (!request.noVideo) {
|
if (!request.noVideo) {
|
||||||
surfaceMap.forEach { (tag, surface) ->
|
surfaceMap.forEach { (tag, surface) ->
|
||||||
@@ -221,6 +225,7 @@ class NativeCoreFacade(private val appContext: Context) {
|
|||||||
releaseAllDecoders()
|
releaseAllDecoders()
|
||||||
synchronized(bootstrapLock) {
|
synchronized(bootstrapLock) {
|
||||||
bootstrapPackets.clear()
|
bootstrapPackets.clear()
|
||||||
|
latestConfigPacket = null
|
||||||
}
|
}
|
||||||
currentSessionInfo = null
|
currentSessionInfo = null
|
||||||
sessionManager.clearVideoConsumer()
|
sessionManager.clearVideoConsumer()
|
||||||
@@ -578,6 +583,20 @@ class NativeCoreFacade(private val appContext: Context) {
|
|||||||
isKeyFrame = packet.isKeyFrame,
|
isKeyFrame = packet.isKeyFrame,
|
||||||
)
|
)
|
||||||
synchronized(bootstrapLock) {
|
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) {
|
while (bootstrapPackets.size >= MAX_BOOTSTRAP_PACKETS) {
|
||||||
bootstrapPackets.removeFirst()
|
bootstrapPackets.removeFirst()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -161,8 +161,8 @@ internal class AdbPairingKey(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private const val ANDROID_PUBKEY_MODULUS_SIZE = 2048 / 8
|
private const val ANDROID_PUBKEY_MODULUS_SIZE = 2048 / 8 // 256
|
||||||
private const val ANDROID_PUBKEY_MODULUS_SIZE_WORDS = ANDROID_PUBKEY_MODULUS_SIZE / 4
|
private const val ANDROID_PUBKEY_MODULUS_SIZE_WORDS = ANDROID_PUBKEY_MODULUS_SIZE / 4 // 64
|
||||||
private const val RSA_PUBLIC_KEY_SIZE = 524
|
private const val RSA_PUBLIC_KEY_SIZE = 524
|
||||||
|
|
||||||
private fun BigInteger.toAdbEncoded(): IntArray {
|
private fun BigInteger.toAdbEncoded(): IntArray {
|
||||||
|
|||||||
@@ -190,21 +190,8 @@ internal class DirectAdbConnection(
|
|||||||
) : AutoCloseable {
|
) : AutoCloseable {
|
||||||
|
|
||||||
private val sha1DigestInfoPrefix = byteArrayOf(
|
private val sha1DigestInfoPrefix = byteArrayOf(
|
||||||
0x30,
|
0x30, 0x21, 0x30, 0x09, 0x06, 0x05, 0x2B, 0x0E,
|
||||||
0x21,
|
0x03, 0x02, 0x1A, 0x05, 0x00, 0x04, 0x14,
|
||||||
0x30,
|
|
||||||
0x09,
|
|
||||||
0x06,
|
|
||||||
0x05,
|
|
||||||
0x2B,
|
|
||||||
0x0E,
|
|
||||||
0x03,
|
|
||||||
0x02,
|
|
||||||
0x1A,
|
|
||||||
0x05,
|
|
||||||
0x00,
|
|
||||||
0x04,
|
|
||||||
0x14,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
private val socket = Socket()
|
private val socket = Socket()
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package io.github.miuzarte.scrcpyforandroid.pages
|
package io.github.miuzarte.scrcpyforandroid.pages
|
||||||
|
|
||||||
import android.app.Activity
|
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.Box
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
@@ -42,8 +42,13 @@ fun FullscreenControlPage(
|
|||||||
virtualButtonsInMore: List<String>,
|
virtualButtonsInMore: List<String>,
|
||||||
showDebugInfo: Boolean,
|
showDebugInfo: Boolean,
|
||||||
showVirtualButtons: Boolean,
|
showVirtualButtons: Boolean,
|
||||||
|
onVideoSizeChanged: (width: Int, height: Int) -> Unit,
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
) {
|
) {
|
||||||
|
// 全屏控制页不使用预测性返回手势,
|
||||||
|
// 省得处理解码的问题
|
||||||
|
BackHandler(enabled = true, onBack = onDismiss)
|
||||||
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val haptics = rememberAppHaptics()
|
val haptics = rememberAppHaptics()
|
||||||
val activity = remember(context) { context as? Activity }
|
val activity = remember(context) { context as? Activity }
|
||||||
@@ -56,9 +61,6 @@ fun FullscreenControlPage(
|
|||||||
moreActions = virtualButtonLayout.second,
|
moreActions = virtualButtonLayout.second,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val initialOrientation = remember(activity) {
|
|
||||||
activity?.requestedOrientation ?: ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
|
||||||
}
|
|
||||||
var session by remember(launch) {
|
var session by remember(launch) {
|
||||||
mutableStateOf(
|
mutableStateOf(
|
||||||
ScrcpySessionInfo(
|
ScrcpySessionInfo(
|
||||||
@@ -72,18 +74,6 @@ fun FullscreenControlPage(
|
|||||||
}
|
}
|
||||||
var currentFps by remember { mutableFloatStateOf(0f) }
|
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) {
|
DisposableEffect(activity) {
|
||||||
val window = activity?.window
|
val window = activity?.window
|
||||||
if (window != null) {
|
if (window != null) {
|
||||||
@@ -107,6 +97,7 @@ fun FullscreenControlPage(
|
|||||||
DisposableEffect(nativeCore) {
|
DisposableEffect(nativeCore) {
|
||||||
val listener: (Int, Int) -> Unit = { w, h ->
|
val listener: (Int, Int) -> Unit = { w, h ->
|
||||||
session = session.copy(width = w, height = h)
|
session = session.copy(width = w, height = h)
|
||||||
|
onVideoSizeChanged(w, h)
|
||||||
}
|
}
|
||||||
nativeCore.addVideoSizeListener(listener)
|
nativeCore.addVideoSizeListener(listener)
|
||||||
onDispose {
|
onDispose {
|
||||||
@@ -151,6 +142,7 @@ fun FullscreenControlPage(
|
|||||||
screenWidth = session.width,
|
screenWidth = session.width,
|
||||||
screenHeight = session.height,
|
screenHeight = session.height,
|
||||||
pressure = pressure,
|
pressure = pressure,
|
||||||
|
actionButton = 0,
|
||||||
buttons = buttons,
|
buttons = buttons,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package io.github.miuzarte.scrcpyforandroid.pages
|
package io.github.miuzarte.scrcpyforandroid.pages
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.pm.ActivityInfo
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.activity.compose.PredictiveBackHandler
|
import androidx.activity.compose.PredictiveBackHandler
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
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.MoreVert
|
||||||
import androidx.compose.material.icons.filled.Settings
|
import androidx.compose.material.icons.filled.Settings
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
@@ -86,6 +89,10 @@ private sealed interface RootScreen : NavKey {
|
|||||||
@Composable
|
@Composable
|
||||||
fun MainPage() {
|
fun MainPage() {
|
||||||
val context = LocalContext.current
|
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 nativeCore = remember(context) { NativeCoreFacade.get(context.applicationContext) }
|
||||||
val initialSettings = remember(context) { loadMainSettings(context) }
|
val initialSettings = remember(context) { loadMainSettings(context) }
|
||||||
val initialDeviceSettings = remember(context) { loadDevicePageSettings(context) }
|
val initialDeviceSettings = remember(context) { loadDevicePageSettings(context) }
|
||||||
@@ -182,9 +189,26 @@ fun MainPage() {
|
|||||||
var openReorderDevicesAction by remember { mutableStateOf<(() -> Unit)?>(null) }
|
var openReorderDevicesAction by remember { mutableStateOf<(() -> Unit)?>(null) }
|
||||||
var canClearLogs by remember { mutableStateOf(false) }
|
var canClearLogs by remember { mutableStateOf(false) }
|
||||||
var showDeviceMenu by rememberSaveable { mutableStateOf(false) }
|
var showDeviceMenu by rememberSaveable { mutableStateOf(false) }
|
||||||
|
var fullscreenOrientation by rememberSaveable {
|
||||||
|
mutableIntStateOf(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT)
|
||||||
|
}
|
||||||
val themeMode = resolveThemeMode(themeBaseIndex, monetEnabled)
|
val themeMode = resolveThemeMode(themeBaseIndex, monetEnabled)
|
||||||
val themeController = remember(themeMode) { ThemeController(colorSchemeMode = themeMode) }
|
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(
|
LaunchedEffect(
|
||||||
audioEnabled,
|
audioEnabled,
|
||||||
audioCodec,
|
audioCodec,
|
||||||
@@ -232,9 +256,6 @@ fun MainPage() {
|
|||||||
nativeCore.setAdbKeyName(adbKeyName.ifBlank { AppDefaults.ADB_KEY_NAME })
|
nativeCore.setAdbKeyName(adbKeyName.ifBlank { AppDefaults.ADB_KEY_NAME })
|
||||||
}
|
}
|
||||||
|
|
||||||
val canNavigateBack =
|
|
||||||
rootBackStack.size > 1 || pagerState.currentPage != MainTabDestination.Device.ordinal
|
|
||||||
|
|
||||||
fun popRoot() {
|
fun popRoot() {
|
||||||
if (rootBackStack.size > 1) {
|
if (rootBackStack.size > 1) {
|
||||||
rootBackStack.removeAt(rootBackStack.lastIndex)
|
rootBackStack.removeAt(rootBackStack.lastIndex)
|
||||||
@@ -257,6 +278,9 @@ fun MainPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val canNavigateBack = rootBackStack.size > 1 ||
|
||||||
|
pagerState.currentPage != MainTabDestination.Device.ordinal
|
||||||
|
|
||||||
PredictiveBackHandler(enabled = canNavigateBack) { progress ->
|
PredictiveBackHandler(enabled = canNavigateBack) { progress ->
|
||||||
try {
|
try {
|
||||||
progress.collect { }
|
progress.collect { }
|
||||||
@@ -467,6 +491,12 @@ fun MainPage() {
|
|||||||
},
|
},
|
||||||
onOpenAdvancedPage = { rootBackStack.add(RootScreen.Advanced) },
|
onOpenAdvancedPage = { rootBackStack.add(RootScreen.Advanced) },
|
||||||
onOpenFullscreenPage = { session ->
|
onOpenFullscreenPage = { session ->
|
||||||
|
fullscreenOrientation =
|
||||||
|
if (session.width >= session.height) {
|
||||||
|
ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
|
||||||
|
} else {
|
||||||
|
ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||||
|
}
|
||||||
rootBackStack.add(
|
rootBackStack.add(
|
||||||
RootScreen.Fullscreen(
|
RootScreen.Fullscreen(
|
||||||
launch = FullscreenControlLaunch(
|
launch = FullscreenControlLaunch(
|
||||||
@@ -511,6 +541,10 @@ fun MainPage() {
|
|||||||
onDevicePreviewCardHeightDpChange = {
|
onDevicePreviewCardHeightDpChange = {
|
||||||
devicePreviewCardHeightDp = it.coerceAtLeast(120)
|
devicePreviewCardHeightDp = it.coerceAtLeast(120)
|
||||||
},
|
},
|
||||||
|
showFullscreenVirtualButtons = showFullscreenVirtualButtons,
|
||||||
|
onShowFullscreenVirtualButtonsChange = {
|
||||||
|
showFullscreenVirtualButtons = it
|
||||||
|
},
|
||||||
customServerUri = customServerUri,
|
customServerUri = customServerUri,
|
||||||
onPickServer = {
|
onPickServer = {
|
||||||
picker.launch(
|
picker.launch(
|
||||||
@@ -692,6 +726,13 @@ fun MainPage() {
|
|||||||
virtualButtonsInMore = virtualButtonsInMore,
|
virtualButtonsInMore = virtualButtonsInMore,
|
||||||
showDebugInfo = fullscreenDebugInfoEnabled,
|
showDebugInfo = fullscreenDebugInfoEnabled,
|
||||||
showVirtualButtons = showFullscreenVirtualButtons,
|
showVirtualButtons = showFullscreenVirtualButtons,
|
||||||
|
onVideoSizeChanged = { width, height ->
|
||||||
|
fullscreenOrientation = if (width >= height) {
|
||||||
|
ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
|
||||||
|
} else {
|
||||||
|
ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||||
|
}
|
||||||
|
},
|
||||||
onDismiss = { popRoot() },
|
onDismiss = { popRoot() },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,6 +65,8 @@ fun SettingsScreen(
|
|||||||
onKeepScreenOnWhenStreamingEnabledChange: (Boolean) -> Unit,
|
onKeepScreenOnWhenStreamingEnabledChange: (Boolean) -> Unit,
|
||||||
devicePreviewCardHeightDp: Int,
|
devicePreviewCardHeightDp: Int,
|
||||||
onDevicePreviewCardHeightDpChange: (Int) -> Unit,
|
onDevicePreviewCardHeightDpChange: (Int) -> Unit,
|
||||||
|
showFullscreenVirtualButtons: Boolean,
|
||||||
|
onShowFullscreenVirtualButtonsChange: (Boolean) -> Unit,
|
||||||
customServerUri: String?,
|
customServerUri: String?,
|
||||||
onPickServer: () -> Unit,
|
onPickServer: () -> Unit,
|
||||||
onClearServer: () -> Unit,
|
onClearServer: () -> Unit,
|
||||||
@@ -138,6 +140,12 @@ fun SettingsScreen(
|
|||||||
?.let { onDevicePreviewCardHeightDpChange(it.coerceAtLeast(120)) }
|
?.let { onDevicePreviewCardHeightDpChange(it.coerceAtLeast(120)) }
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
SuperSwitch(
|
||||||
|
title = "全屏显示虚拟按钮",
|
||||||
|
summary = "在全屏控制页底部显示返回键、主页键等虚拟按钮",
|
||||||
|
checked = showFullscreenVirtualButtons,
|
||||||
|
onCheckedChange = onShowFullscreenVirtualButtonsChange,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
SectionSmallTitle(text = "scrcpy-server")
|
SectionSmallTitle(text = "scrcpy-server")
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.foundation.text.KeyboardActions
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
@@ -114,6 +113,8 @@ private object UiMotionActions {
|
|||||||
const val POINTER_UP = 6
|
const val POINTER_UP = 6
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private const val FULLSCREEN_TOUCH_LOG_TAG = "FullscreenTouch"
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun StatusCard(
|
internal fun StatusCard(
|
||||||
statusLine: String,
|
statusLine: String,
|
||||||
@@ -647,7 +648,11 @@ fun FullscreenControlScreen(
|
|||||||
var touchAreaSize by remember { mutableStateOf(IntSize.Zero) }
|
var touchAreaSize by remember { mutableStateOf(IntSize.Zero) }
|
||||||
val activePointerIds = remember { linkedSetOf<Int>() }
|
val activePointerIds = remember { linkedSetOf<Int>() }
|
||||||
val activePointerPositions = remember { linkedMapOf<Int, Offset>() }
|
val activePointerPositions = remember { linkedMapOf<Int, Offset>() }
|
||||||
|
val activePointerDevicePositions = remember { linkedMapOf<Int, Pair<Int, Int>>() }
|
||||||
|
val pointerLabels = remember { linkedMapOf<Int, Int>() }
|
||||||
|
var nextPointerLabel by remember { mutableIntStateOf(1) }
|
||||||
var activeTouchCount by remember { mutableIntStateOf(0) }
|
var activeTouchCount by remember { mutableIntStateOf(0) }
|
||||||
|
var activeTouchDebug by remember { mutableStateOf("") }
|
||||||
BackHandler(enabled = enableBackHandler, onBack = onDismiss)
|
BackHandler(enabled = enableBackHandler, onBack = onDismiss)
|
||||||
|
|
||||||
BoxWithConstraints(
|
BoxWithConstraints(
|
||||||
@@ -659,125 +664,161 @@ fun FullscreenControlScreen(
|
|||||||
return@pointerInteropFilter true
|
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<Int, Int> {
|
fun mapToDevice(rawX: Float, rawY: Float): Pair<Int, Int> {
|
||||||
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))
|
.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))
|
.coerceIn(0, (session.height - 1).coerceAtLeast(0))
|
||||||
return x to y
|
return x to y
|
||||||
}
|
}
|
||||||
|
|
||||||
fun syncActivePointersFromEvent(skipPointerId: Int? = null) {
|
fun pointerLabel(pointerId: Int): Int {
|
||||||
for (i in 0 until event.pointerCount) {
|
val existing = pointerLabels[pointerId]
|
||||||
val pointerId = event.getPointerId(i)
|
if (existing != null) {
|
||||||
if (!activePointerIds.contains(pointerId) || pointerId == skipPointerId) continue
|
return existing
|
||||||
val px = event.getX(i)
|
}
|
||||||
val py = event.getY(i)
|
val assigned = nextPointerLabel
|
||||||
activePointerPositions[pointerId] = Offset(px, py)
|
nextPointerLabel += 1
|
||||||
val (x, y) = mapToDevice(px, py)
|
pointerLabels[pointerId] = assigned
|
||||||
onInjectTouch(
|
return assigned
|
||||||
UiMotionActions.MOVE,
|
}
|
||||||
pointerId.toLong(),
|
|
||||||
x,
|
fun refreshTouchDebug() {
|
||||||
y,
|
if (activePointerIds.isEmpty()) {
|
||||||
event.getPressure(i).coerceIn(0f, 1f),
|
activeTouchDebug = ""
|
||||||
1
|
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<Int>(event.pointerCount)
|
||||||
|
val eventPositions = HashMap<Int, Offset>(event.pointerCount)
|
||||||
|
val eventPressures = HashMap<Int, Float>(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<Int>()
|
||||||
|
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) {
|
for (i in 0 until event.pointerCount) {
|
||||||
MotionEvent.ACTION_DOWN -> {
|
val pointerId = event.getPointerId(i)
|
||||||
activePointerIds.clear()
|
if (!activePointerIds.contains(pointerId)) continue
|
||||||
activePointerPositions.clear()
|
if (pointerId == endedPointerId) continue
|
||||||
activeTouchCount = 0
|
if (pointerId in justPressed) continue
|
||||||
val (x, y) = mapToDevice(event.x, event.y)
|
val raw = eventPositions[pointerId] ?: continue
|
||||||
val pointerId = event.getPointerId(0)
|
val pressure = eventPressures[pointerId] ?: 0f
|
||||||
activePointerIds += pointerId
|
activePointerPositions[pointerId] = raw
|
||||||
activePointerPositions[pointerId] = Offset(event.x, event.y)
|
val (x, y) = mapToDevice(raw.x, raw.y)
|
||||||
activeTouchCount = activePointerIds.size
|
activePointerDevicePositions[pointerId] = x to y
|
||||||
onInjectTouch(
|
onInjectTouch(UiMotionActions.MOVE, pointerId.toLong(), x, y, pressure, 0)
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (endedPointerId != null) {
|
||||||
|
val endPos = eventPositions[endedPointerId]
|
||||||
|
if (endPos != null) {
|
||||||
|
activePointerPositions[endedPointerId] = endPos
|
||||||
|
}
|
||||||
|
releasePointer(endedPointerId, reason = "event")
|
||||||
|
}
|
||||||
|
|
||||||
|
activeTouchCount = activePointerIds.size
|
||||||
|
refreshTouchDebug()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
.onSizeChanged { touchAreaSize = it },
|
.onSizeChanged { touchAreaSize = it },
|
||||||
@@ -815,7 +856,7 @@ fun FullscreenControlScreen(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.TopStart)
|
.align(Alignment.TopStart)
|
||||||
.padding(start = UiSpacing.CardContent, top = UiSpacing.CardContent)
|
.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),
|
.padding(horizontal = UiSpacing.CardContent, vertical = UiSpacing.Medium),
|
||||||
) {
|
) {
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(UiSpacing.Tiny)) {
|
Column(verticalArrangement = Arrangement.spacedBy(UiSpacing.Tiny)) {
|
||||||
@@ -825,14 +866,19 @@ fun FullscreenControlScreen(
|
|||||||
fontSize = 13.sp,
|
fontSize = 13.sp,
|
||||||
fontWeight = FontWeight.SemiBold,
|
fontWeight = FontWeight.SemiBold,
|
||||||
)
|
)
|
||||||
|
@SuppressLint("DefaultLocale")
|
||||||
|
Text(
|
||||||
|
text = "FPS: ${String.format("%.1f", currentFps.coerceAtLeast(0f))}",
|
||||||
|
color = Color.White,
|
||||||
|
fontSize = 13.sp,
|
||||||
|
)
|
||||||
Text(
|
Text(
|
||||||
text = "触点: $activeTouchCount",
|
text = "触点: $activeTouchCount",
|
||||||
color = Color.White,
|
color = Color.White,
|
||||||
fontSize = 13.sp,
|
fontSize = 13.sp,
|
||||||
)
|
)
|
||||||
@SuppressLint("DefaultLocale")
|
if (activeTouchDebug.isNotEmpty()) Text(
|
||||||
Text(
|
text = activeTouchDebug,
|
||||||
text = "FPS: ${String.format("%.1f", currentFps.coerceAtLeast(0f))}",
|
|
||||||
color = Color.White,
|
color = Color.White,
|
||||||
fontSize = 13.sp,
|
fontSize = 13.sp,
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user