refactor: touch implementation, ratio switching
- 重写了触控实现 - 串流过程中自动切换横竖屏 - 禁用了全屏控制页中的预测性返回手势
This commit is contained in:
@@ -16,11 +16,11 @@
|
||||
|
||||
- 可替换 scrcpy-server
|
||||
- 利用 mDNS 服务实现自动连接启用无线调试的设备、自动发现等待配对设备的IP与端口
|
||||
- 自动横竖屏切换(算吗
|
||||
|
||||
## 已知问题
|
||||
## 已知问题 / TODO
|
||||
|
||||
- 多指触控抬起后滞留
|
||||
- 快速离开再进入全屏会导致视频流关键帧丢失
|
||||
- 组件排序动画一坨
|
||||
|
||||
## 构建
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:configChanges="keyboardHidden|orientation|screenSize|smallestScreenSize"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<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 bootstrapLock = Any()
|
||||
private val bootstrapPackets = ArrayDeque<CachedPacket>()
|
||||
|
||||
@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()
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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<String>,
|
||||
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,
|
||||
)
|
||||
},
|
||||
|
||||
@@ -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() },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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<Int>() }
|
||||
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 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<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))
|
||||
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<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) {
|
||||
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,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user