feat(android): 相对鼠标模式 + 灵敏度 + 长按右键 + 修复表单 IME 残留
相对模式(默认):手指滑动按增量移动远端指针,DOWN 不发绝对移动——轻点落在 远端指针当前位置,不再「手指落下指针跳到脚下」。换算基准为视图全宽=桌面全宽 (灵敏度 1.0),慢速滑动用小数累积取整,逐次 toInt() 会静默吞掉位移。 长按右键(Q5=A):>=500ms 且位移 <12dp;触发后抑制本次 ACTION_UP 的左键, 否则一次长按会「先右键再左键」把菜单立刻点掉。超过 slop 立即转拖拽并取消计时, 所以拖窗口不会弹菜单。双指右键路径保持不变。 真机暴露并修复(N19):切到远控面板后软键盘仍绑在已隐藏的表单 EditText 上 (dumpsys 里 mServedView 指向 EditText、mInputShown=true),键盘盖住面板底部 按钮栏,**所有按钮点不动**——3b 新增的模式/灵敏度按钮把它暴露出来。改为对每个 表单输入框显式 hideSoftInputFromWindow。 新增右键计数器:长按后紧跟的 ACTION_UP 会覆盖一行式诊断文本,验收脚本读不到 「长按右键」;计数器持久且能区分长按/双指来源。
This commit is contained in:
parent
3773a0851c
commit
7c3b6b3e93
@ -11,8 +11,8 @@ android {
|
|||||||
applicationId = "com.verify.remote"
|
applicationId = "com.verify.remote"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 34
|
targetSdk = 34
|
||||||
versionCode = 2
|
versionCode = 3
|
||||||
versionName = "0.1-phase3a"
|
versionName = "0.1-phase3b"
|
||||||
// 阶段 1 只测真机 arm64;x86_64 留给模拟器回归
|
// 阶段 1 只测真机 arm64;x86_64 留给模拟器回归
|
||||||
ndk {
|
ndk {
|
||||||
abiFilters += listOf("arm64-v8a")
|
abiFilters += listOf("arm64-v8a")
|
||||||
|
|||||||
@ -45,6 +45,10 @@ class MainActivity : Activity(), RemoteSurfaceView.Listener {
|
|||||||
private lateinit var codeInput: EditText
|
private lateinit var codeInput: EditText
|
||||||
private lateinit var connectButton: Button
|
private lateinit var connectButton: Button
|
||||||
private lateinit var disconnectButton: Button
|
private lateinit var disconnectButton: Button
|
||||||
|
private lateinit var modeButton: Button
|
||||||
|
private lateinit var sensUpButton: Button
|
||||||
|
private lateinit var sensDownButton: Button
|
||||||
|
private lateinit var sensLabel: TextView
|
||||||
private lateinit var connectStatus: TextView
|
private lateinit var connectStatus: TextView
|
||||||
|
|
||||||
@Volatile
|
@Volatile
|
||||||
@ -97,6 +101,9 @@ class MainActivity : Activity(), RemoteSurfaceView.Listener {
|
|||||||
orientation = LinearLayout.VERTICAL
|
orientation = LinearLayout.VERTICAL
|
||||||
setBackgroundColor(0xFF000000.toInt())
|
setBackgroundColor(0xFF000000.toInt())
|
||||||
}
|
}
|
||||||
|
// 3b:模式与灵敏度先于面板构造恢复——buildRemotePanel 里的按钮文案要读它们
|
||||||
|
surfaceView.relativeMode = prefs().getBoolean(PREF_RELATIVE, true)
|
||||||
|
surfaceView.sensitivity = prefs().getFloat(PREF_SENSITIVITY, RelPointer.DEFAULT_SENSITIVITY)
|
||||||
connectPanel = buildConnectPanel()
|
connectPanel = buildConnectPanel()
|
||||||
remotePanel = buildRemotePanel()
|
remotePanel = buildRemotePanel()
|
||||||
val lp = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, 0, 1f)
|
val lp = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, 0, 1f)
|
||||||
@ -208,6 +215,32 @@ class MainActivity : Activity(), RemoteSurfaceView.Listener {
|
|||||||
text = "键盘"
|
text = "键盘"
|
||||||
setOnClickListener { showKeyboard() }
|
setOnClickListener { showKeyboard() }
|
||||||
}
|
}
|
||||||
|
// 协议 v4:指针模式切换(Q3=A:默认相对,可切绝对)
|
||||||
|
modeButton = Button(this).apply {
|
||||||
|
text = modeLabel()
|
||||||
|
setOnClickListener {
|
||||||
|
surfaceView.relativeMode = !surfaceView.relativeMode
|
||||||
|
prefs().edit().putBoolean(PREF_RELATIVE, surfaceView.relativeMode).apply()
|
||||||
|
text = modeLabel()
|
||||||
|
lastTouchDbg = if (surfaceView.relativeMode) "模式:相对" else "模式:绝对"
|
||||||
|
refreshStatus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 灵敏度(Q4=A:0.25–3.0,默认 1.0)。仅相对模式生效,绝对模式下调了也不影响。
|
||||||
|
sensDownButton = Button(this).apply {
|
||||||
|
text = "−"
|
||||||
|
setOnClickListener { adjustSensitivity(-SENS_STEP) }
|
||||||
|
}
|
||||||
|
sensUpButton = Button(this).apply {
|
||||||
|
text = "+"
|
||||||
|
setOnClickListener { adjustSensitivity(+SENS_STEP) }
|
||||||
|
}
|
||||||
|
sensLabel = TextView(this).apply {
|
||||||
|
text = sensText()
|
||||||
|
textSize = 12f
|
||||||
|
setTextColor(0xFFFFFFFF.toInt())
|
||||||
|
gravity = Gravity.CENTER
|
||||||
|
}
|
||||||
disconnectButton = Button(this).apply {
|
disconnectButton = Button(this).apply {
|
||||||
text = "断开"
|
text = "断开"
|
||||||
// 连接等待期它显示「取消」并走同一个 cancelConnect():两个按钮的
|
// 连接等待期它显示「取消」并走同一个 cancelConnect():两个按钮的
|
||||||
@ -217,8 +250,15 @@ class MainActivity : Activity(), RemoteSurfaceView.Listener {
|
|||||||
val bar = LinearLayout(this).apply {
|
val bar = LinearLayout(this).apply {
|
||||||
orientation = LinearLayout.HORIZONTAL
|
orientation = LinearLayout.HORIZONTAL
|
||||||
addView(keyboardButton, LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f))
|
addView(keyboardButton, LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f))
|
||||||
|
addView(modeButton, LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f))
|
||||||
addView(disconnectButton, LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f))
|
addView(disconnectButton, LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f))
|
||||||
}
|
}
|
||||||
|
val sensBar = LinearLayout(this).apply {
|
||||||
|
orientation = LinearLayout.HORIZONTAL
|
||||||
|
addView(sensDownButton, LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT))
|
||||||
|
addView(sensLabel, LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f))
|
||||||
|
addView(sensUpButton, LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT))
|
||||||
|
}
|
||||||
return LinearLayout(this).apply {
|
return LinearLayout(this).apply {
|
||||||
orientation = LinearLayout.VERTICAL
|
orientation = LinearLayout.VERTICAL
|
||||||
visibility = View.GONE
|
visibility = View.GONE
|
||||||
@ -226,10 +266,26 @@ class MainActivity : Activity(), RemoteSurfaceView.Listener {
|
|||||||
// 固定高度而非 WRAP_CONTENT:与 weight=1 的兄弟共存时,
|
// 固定高度而非 WRAP_CONTENT:与 weight=1 的兄弟共存时,
|
||||||
// WRAP_CONTENT 可能被压成 0(实测 bounds=[0,0][0,0],状态栏不可见)
|
// WRAP_CONTENT 可能被压成 0(实测 bounds=[0,0][0,0],状态栏不可见)
|
||||||
addView(statusView, LayoutParams(LayoutParams.MATCH_PARENT, dp(64)))
|
addView(statusView, LayoutParams(LayoutParams.MATCH_PARENT, dp(64)))
|
||||||
|
addView(sensBar, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT))
|
||||||
addView(bar, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT))
|
addView(bar, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun prefs() = getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||||
|
|
||||||
|
private fun modeLabel() = if (surfaceView.relativeMode) "模式:相对" else "模式:绝对"
|
||||||
|
|
||||||
|
private fun sensText() = "灵敏度 ${"%.2f".format(surfaceView.sensitivity)}"
|
||||||
|
|
||||||
|
/** 灵敏度按 0.25 步进调整并落盘(绝对模式下调了也不影响,切回相对即生效) */
|
||||||
|
private fun adjustSensitivity(delta: Float) {
|
||||||
|
surfaceView.sensitivity = surfaceView.sensitivity + delta
|
||||||
|
prefs().edit().putFloat(PREF_SENSITIVITY, surfaceView.sensitivity).apply()
|
||||||
|
sensLabel.text = sensText()
|
||||||
|
lastTouchDbg = "灵敏度 ${"%.2f".format(surfaceView.sensitivity)}"
|
||||||
|
refreshStatus()
|
||||||
|
}
|
||||||
|
|
||||||
private fun showConnectState(msg: String) {
|
private fun showConnectState(msg: String) {
|
||||||
connecting = false
|
connecting = false
|
||||||
remotePanelVisible(false)
|
remotePanelVisible(false)
|
||||||
@ -239,6 +295,36 @@ class MainActivity : Activity(), RemoteSurfaceView.Listener {
|
|||||||
private fun showRemoteState() {
|
private fun showRemoteState() {
|
||||||
remotePanelVisible(true)
|
remotePanelVisible(true)
|
||||||
connectPanel.visibility = View.GONE
|
connectPanel.visibility = View.GONE
|
||||||
|
// 3b/N17(Q6=A):表单被隐藏时必须主动清焦点。否则 EditText 仍持有焦点,
|
||||||
|
// 远控面板上的按钮第一下 tap 只被用来「丢焦点」而不触发点击——真机上表现
|
||||||
|
// 为「按钮要点两次」(3a 的 D8 已实测到 4 轮全丢 tap)。
|
||||||
|
clearFormFocus()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 收走表单输入框的焦点并隐藏软键盘。
|
||||||
|
*
|
||||||
|
* 为什么放在切面板时而不是按钮点击时:点按钮时焦点已经丢了,补救太晚;
|
||||||
|
* 面板可见的那一刻才是「表单不再需要输入」的准确时机。
|
||||||
|
*
|
||||||
|
* **3b 真机暴露的加强(N19)**:只 `clearFocus()` 不够。实测远控面板已经显示、
|
||||||
|
* 表单已 GONE 之后,`dumpsys input_method` 里 `mServedView` **仍指向表单的
|
||||||
|
* EditText**、`mInputShown=true`——软键盘继续挂在屏幕底部,把面板底部的按钮栏
|
||||||
|
* (键盘/模式/断开,y=2256..2324)整个盖住,**所有按钮点不动**。
|
||||||
|
* 因此必须显式对每个表单输入框 `hideSoftInputFromWindow`,并确认 IME 真的收起。
|
||||||
|
*/
|
||||||
|
private fun clearFormFocus() {
|
||||||
|
val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||||
|
// 逐个表单输入框收键盘:currentFocus 可能已经不是它们(隐藏视图的焦点
|
||||||
|
// 状态不可靠),而 IME 的 mServedView 恰恰还绑在它们身上。
|
||||||
|
for (v in listOf(hostInput, portInput, codeInput)) {
|
||||||
|
v.clearFocus()
|
||||||
|
imm.hideSoftInputFromWindow(v.windowToken, 0)
|
||||||
|
}
|
||||||
|
currentFocus?.let { if (it !== surfaceView) it.clearFocus() }
|
||||||
|
keyboardVisible = false
|
||||||
|
// 布局变化会再触发 updateKeyboardVisibility(),用真实可见区域复核一次
|
||||||
|
window.decorView.post { updateKeyboardVisibility() }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun remotePanelVisible(show: Boolean) {
|
private fun remotePanelVisible(show: Boolean) {
|
||||||
@ -276,6 +362,12 @@ class MainActivity : Activity(), RemoteSurfaceView.Listener {
|
|||||||
if (h != 0L) RustBridge.nativeSendMouseMove(h, x, y)
|
if (h != 0L) RustBridge.nativeSendMouseMove(h, x, y)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 协议 v4:相对位移(被控端桌面物理像素)。零位移由 Rust 侧拦掉,这里不重复判断。 */
|
||||||
|
override fun sendMouseRel(dx: Int, dy: Int) {
|
||||||
|
val h = handle
|
||||||
|
if (h != 0L) RustBridge.nativeSendMouseRel(h, dx, dy)
|
||||||
|
}
|
||||||
|
|
||||||
override fun sendMouseButton(button: Int, down: Boolean) {
|
override fun sendMouseButton(button: Int, down: Boolean) {
|
||||||
val h = handle
|
val h = handle
|
||||||
if (h != 0L) RustBridge.nativeSendMouseButton(h, button, if (down) 1 else 0)
|
if (h != 0L) RustBridge.nativeSendMouseButton(h, button, if (down) 1 else 0)
|
||||||
@ -301,6 +393,22 @@ class MainActivity : Activity(), RemoteSurfaceView.Listener {
|
|||||||
|
|
||||||
override fun isReadyForInput(): Boolean = handle != 0L
|
override fun isReadyForInput(): Boolean = handle != 0L
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 3b:右键注入计数(长按 / 双指各一路)。持久计数而不是覆盖式诊断行——
|
||||||
|
* 长按之后的 ACTION_UP 会把 lastTouchDbg 改写成「长按结束」,验收脚本读不到
|
||||||
|
* 「长按右键」。计数不会丢,且能区分两种来源。
|
||||||
|
*/
|
||||||
|
@Volatile
|
||||||
|
private var rightClicks = 0
|
||||||
|
@Volatile
|
||||||
|
private var rightClickSource = "-"
|
||||||
|
|
||||||
|
override fun onRightClickInjected(source: String) {
|
||||||
|
rightClicks++
|
||||||
|
rightClickSource = source
|
||||||
|
runOnUiThread { refreshStatus() }
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 软键盘是否可见(自测,不依赖 InputMethodManager 的间接信号)。
|
* 软键盘是否可见(自测,不依赖 InputMethodManager 的间接信号)。
|
||||||
*
|
*
|
||||||
@ -492,6 +600,9 @@ class MainActivity : Activity(), RemoteSurfaceView.Listener {
|
|||||||
handle = h
|
handle = h
|
||||||
remoteW = w
|
remoteW = w
|
||||||
remoteH = ht
|
remoteH = ht
|
||||||
|
// 相对模式的换算基准:视图像素 → 桌面像素必须知道远端真实尺寸
|
||||||
|
surfaceView.remoteWidth = w
|
||||||
|
surfaceView.remoteHeight = ht
|
||||||
connectButton.text = "断开"
|
connectButton.text = "断开"
|
||||||
disconnectButton.text = "断开"
|
disconnectButton.text = "断开"
|
||||||
// 只有认证成功的会话才值得记住;验证码不落盘(用户裁决)
|
// 只有认证成功的会话才值得记住;验证码不落盘(用户裁决)
|
||||||
@ -623,6 +734,9 @@ class MainActivity : Activity(), RemoteSurfaceView.Listener {
|
|||||||
if (handle == h) handle = 0L
|
if (handle == h) handle = 0L
|
||||||
remoteW = 0
|
remoteW = 0
|
||||||
remoteH = 0
|
remoteH = 0
|
||||||
|
// 断开后相对换算失去基准:不清零会让下一次会话在拿到新尺寸前按旧尺寸换算
|
||||||
|
surfaceView.remoteWidth = 0
|
||||||
|
surfaceView.remoteHeight = 0
|
||||||
lastFrames = 0
|
lastFrames = 0
|
||||||
lastRtt = 0
|
lastRtt = 0
|
||||||
lastFps = 0
|
lastFps = 0
|
||||||
@ -647,6 +761,8 @@ class MainActivity : Activity(), RemoteSurfaceView.Listener {
|
|||||||
decodeThread = null
|
decodeThread = null
|
||||||
remoteW = 0
|
remoteW = 0
|
||||||
remoteH = 0
|
remoteH = 0
|
||||||
|
surfaceView.remoteWidth = 0
|
||||||
|
surfaceView.remoteHeight = 0
|
||||||
lastFrames = 0
|
lastFrames = 0
|
||||||
lastRtt = 0
|
lastRtt = 0
|
||||||
lastFps = 0
|
lastFps = 0
|
||||||
@ -677,7 +793,9 @@ class MainActivity : Activity(), RemoteSurfaceView.Listener {
|
|||||||
}
|
}
|
||||||
statusView.text = buildString {
|
statusView.text = buildString {
|
||||||
append("已连接 v$PROTO_LABEL ${remoteW}x$remoteH fps=$lastFps RTT=${lastRtt}ms 帧=$lastFrames\n")
|
append("已连接 v$PROTO_LABEL ${remoteW}x$remoteH fps=$lastFps RTT=${lastRtt}ms 帧=$lastFrames\n")
|
||||||
append("输入: $lastTouchDbg")
|
append("${if (surfaceView.relativeMode) "相对" else "绝对"} | 输入: $lastTouchDbg")
|
||||||
|
// 右键计数只在发生过时出现,避免常态占宽(状态栏只有 3 行)
|
||||||
|
if (rightClicks > 0) append(" | 右键=$rightClicks($rightClickSource)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -686,12 +804,17 @@ class MainActivity : Activity(), RemoteSurfaceView.Listener {
|
|||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "MainActivity"
|
private const val TAG = "MainActivity"
|
||||||
private const val PREFS = "connect"
|
private const val PREFS = "connect"
|
||||||
|
/** 3b:指针模式与灵敏度落盘(验证码仍不落盘,见 2b 裁决) */
|
||||||
|
private const val PREF_RELATIVE = "relative_mode"
|
||||||
|
private const val PREF_SENSITIVITY = "sensitivity"
|
||||||
|
private const val SENS_STEP = 0.25f
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 与协议版本保持一致:远控画面状态栏显示当前握手版本(方案 R5 要求)。
|
* 与协议版本保持一致:远控画面状态栏显示当前握手版本(方案 R5 要求)。
|
||||||
* 值随 `common::PROTO_VERSION` 人工同步——Android 侧不做线格式解析,
|
* 值随 `common::PROTO_VERSION` 人工同步——Android 侧不做线格式解析,
|
||||||
* 版本由 Rust 握手校验,这里只作显示与排障用。
|
* 版本由 Rust 握手校验,这里只作显示与排障用。
|
||||||
|
* 3b:3 → 4(协议 v4 相对鼠标)。
|
||||||
*/
|
*/
|
||||||
private const val PROTO_LABEL = "3"
|
private const val PROTO_LABEL = "4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,119 @@
|
|||||||
|
package com.verify.remote
|
||||||
|
|
||||||
|
import kotlin.math.abs
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 相对鼠标的**纯逻辑**:把「手指在视图上移动了多少像素」换算成「远端指针该走多少桌面像素」,
|
||||||
|
* 并做小数累积取整。抽成纯 Kotlin 是为了能在 JVM 单测里验证(V-B1 的判据依赖它)。
|
||||||
|
*
|
||||||
|
* 为什么不直接发 `deltaViewPx`:
|
||||||
|
* - 视图尺寸 ≠ 桌面尺寸(letterbox 后视图是 1080x607,桌面是 1920x1080);
|
||||||
|
* - `sensitivity != 1.0` 时还要缩放。
|
||||||
|
* 两步合并成一次乘法,避免分两次取整(每多一次取整就多一个累积误差源)。
|
||||||
|
*
|
||||||
|
* 为什么不直接 `toInt()` 就发:
|
||||||
|
* 手指慢速滑动时每次 `ACTION_MOVE` 的增量可能只有 0.3 个桌面像素,`toInt()` 逐次截断会让
|
||||||
|
* **位移被静默吞掉**(V-B1「慢速小幅滑动不丢位移」直接不成立)。必须把余数累积到下一次。
|
||||||
|
*/
|
||||||
|
object RelPointer {
|
||||||
|
|
||||||
|
/** 灵敏度允许范围(Q4=A:默认 1.0,可调 0.25–3.0) */
|
||||||
|
const val MIN_SENSITIVITY = 0.25f
|
||||||
|
const val MAX_SENSITIVITY = 3.0f
|
||||||
|
const val DEFAULT_SENSITIVITY = 1.0f
|
||||||
|
|
||||||
|
fun clampSensitivity(v: Float): Float = v.coerceIn(MIN_SENSITIVITY, MAX_SENSITIVITY)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 视图像素增量 → 桌面像素增量(**浮点,未取整**)。
|
||||||
|
*
|
||||||
|
* `sensitivity = 1.0` 的语义:手指扫过视图整个宽度 = 指针走完整个桌面宽度。
|
||||||
|
* 这样相对模式与绝对模式的手感一致(绝对模式下 10% 视图宽 = 10% 桌面宽)。
|
||||||
|
*
|
||||||
|
* 任一维度非法(<=0,例如视图尚未测量、远端尺寸未取到)时返回 0:
|
||||||
|
* 宁可不移动,也不要按错误比例乱飞。
|
||||||
|
*/
|
||||||
|
fun toDesktopDelta(
|
||||||
|
viewDeltaPx: Float,
|
||||||
|
viewExtentPx: Int,
|
||||||
|
remoteExtentPx: Int,
|
||||||
|
sensitivity: Float,
|
||||||
|
): Float {
|
||||||
|
if (viewExtentPx <= 0 || remoteExtentPx <= 0) return 0f
|
||||||
|
if (viewDeltaPx == 0f) return 0f
|
||||||
|
val scale = remoteExtentPx.toFloat() / viewExtentPx.toFloat()
|
||||||
|
return viewDeltaPx * scale * clampSensitivity(sensitivity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 小数累积器:把连续的浮点位移攒成整数像素再发送,余数留到下一次。
|
||||||
|
*
|
||||||
|
* 保证:连续输入之和与单独一次输入等价(不丢位移);每次发出的都是整数(协议是 i16)。
|
||||||
|
* 撞边等外部原因丢弃的位移**不回补**——累积器只负责「取整」,不负责「补偿」,
|
||||||
|
* 否则撞边后回滑会出现一段"空转"(V-B1 第三条判据要求撞边后立即响应)。
|
||||||
|
*/
|
||||||
|
class RelAccumulator {
|
||||||
|
private var accX = 0f
|
||||||
|
private var accY = 0f
|
||||||
|
|
||||||
|
/** 会话/手势结束时清空,避免上一段手势的余数漏到下一段 */
|
||||||
|
fun reset() {
|
||||||
|
accX = 0f
|
||||||
|
accY = 0f
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 累积一段位移,返回**本次可以发送的整数增量**;不足 1 像素时返回 null(不发)。
|
||||||
|
*
|
||||||
|
* X/Y 独立累积:斜向慢速滑动时两个轴各自攒够才发,不会因为一个轴没到 1 就把另一个轴也拖住。
|
||||||
|
*/
|
||||||
|
fun take(dxDesktop: Float, dyDesktop: Float): Pair<Int, Int>? {
|
||||||
|
accX += dxDesktop
|
||||||
|
accY += dyDesktop
|
||||||
|
// roundToInt 而非 toInt:-0.7 应取 -1 而不是 0(向零截断会让反向慢滑丢位移)
|
||||||
|
val ix = accX.roundToInt()
|
||||||
|
val iy = accY.roundToInt()
|
||||||
|
if (ix == 0 && iy == 0) return null
|
||||||
|
accX -= ix
|
||||||
|
accY -= iy
|
||||||
|
// 协议是 i16;单次事件不可能接近这个范围,但显式 clamp 让越界行为有界
|
||||||
|
return ix.coerceIn(-32768, 32767) to iy.coerceIn(-32768, 32767)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 长按右键的判定(Q5=A:≥500ms 且位移 < slop)。
|
||||||
|
*
|
||||||
|
* 抽成纯逻辑的原因:真实触发依赖 Handler 计时与 MotionEvent,JVM 里不可构造;
|
||||||
|
* 但「什么条件下该弹右键」是纯判断,必须能被单测钉住(尤其"拖窗口不弹菜单"这条)。
|
||||||
|
*/
|
||||||
|
object LongPressPolicy {
|
||||||
|
|
||||||
|
/** 长按触发阈值(毫秒) */
|
||||||
|
const val LONG_PRESS_MS = 500L
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否应触发长按右键。
|
||||||
|
*
|
||||||
|
* @param elapsedMs 从 ACTION_DOWN 起经过的时间
|
||||||
|
* @param movedPx 期间的最大位移(欧氏距离)
|
||||||
|
* @param slopPx 拖动阈值(12dp),超过即视为拖拽而非长按
|
||||||
|
* @param twoFinger 是否出现过第二根手指(双指手势有自己的右键路径,不能叠加)
|
||||||
|
*
|
||||||
|
* 三条都必须满足:够久、没怎么动、不是双指。
|
||||||
|
* 「拖窗口不弹菜单」靠 movedPx 这条——超过 slop 时 [RemoteSurfaceView] 会切到 DRAG
|
||||||
|
* 并取消计时任务,所以这里通常压根不会被调用;但作为纯逻辑仍要自洽。
|
||||||
|
*/
|
||||||
|
fun shouldFire(
|
||||||
|
elapsedMs: Long,
|
||||||
|
movedPx: Float,
|
||||||
|
slopPx: Float,
|
||||||
|
twoFinger: Boolean,
|
||||||
|
): Boolean {
|
||||||
|
if (twoFinger) return false
|
||||||
|
if (elapsedMs < LONG_PRESS_MS) return false
|
||||||
|
return abs(movedPx) < slopPx
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,6 +1,8 @@
|
|||||||
package com.verify.remote
|
package com.verify.remote
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
import android.text.InputType
|
import android.text.InputType
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import android.view.SurfaceView
|
import android.view.SurfaceView
|
||||||
@ -21,10 +23,15 @@ import kotlin.math.hypot
|
|||||||
* 实现为本项目独立编写):
|
* 实现为本项目独立编写):
|
||||||
* - 单指轻点 = 左键单击
|
* - 单指轻点 = 左键单击
|
||||||
* - 单指按下并移动 = 左键拖拽(窗口拖动、文字选中)
|
* - 单指按下并移动 = 左键拖拽(窗口拖动、文字选中)
|
||||||
|
* - 单指长按不动 = 右键(协议 v4 起;双指轻点仍可用)
|
||||||
* - 双指轻点 = 右键
|
* - 双指轻点 = 右键
|
||||||
* - 双指上下滑动 = 滚轮
|
* - 双指上下滑动 = 滚轮
|
||||||
*
|
*
|
||||||
* 刻意不做「手指即光标」的绝对定位:手指会完全挡住目标,桌面精细操作不可用。
|
* **两种指针模式(协议 v4)**:
|
||||||
|
* - 相对模式(默认):手指滑动 = 远端指针按**增量**移动,手指只扫一小段就能走很远,
|
||||||
|
* 且不遮挡目标。DOWN 不发送绝对移动——轻点落在**远端指针当前位置**。
|
||||||
|
* - 绝对模式:手指位置直接映射到远端坐标(2a/3a 行为,DOWN 先发绝对 `MouseMove`)。
|
||||||
|
* 两种模式由 [relativeMode] 切换;R7 绝对触摸回归判据依赖绝对模式仍可用。
|
||||||
*
|
*
|
||||||
* **IME 接线(阶段 2b)**:本视图是文本编辑器(`onCheckIsTextEditor`),
|
* **IME 接线(阶段 2b)**:本视图是文本编辑器(`onCheckIsTextEditor`),
|
||||||
* 软键盘的提交经 `RemoteInputConnection` 上行。`isFocusableInTouchMode` 必须为
|
* 软键盘的提交经 `RemoteInputConnection` 上行。`isFocusableInTouchMode` 必须为
|
||||||
@ -36,6 +43,8 @@ class RemoteSurfaceView(context: Context) : SurfaceView(context) {
|
|||||||
/** 由 Activity 实现,转发到 RustBridge */
|
/** 由 Activity 实现,转发到 RustBridge */
|
||||||
interface Listener {
|
interface Listener {
|
||||||
fun sendMouseMove(x: Int, y: Int)
|
fun sendMouseMove(x: Int, y: Int)
|
||||||
|
/** 协议 v4:相对位移(被控端桌面物理像素) */
|
||||||
|
fun sendMouseRel(dx: Int, dy: Int)
|
||||||
fun sendMouseButton(button: Int, down: Boolean)
|
fun sendMouseButton(button: Int, down: Boolean)
|
||||||
fun sendWheel(dy: Int)
|
fun sendWheel(dy: Int)
|
||||||
/** 协议 v3:一段 UTF-16 code unit(IME 提交结果) */
|
/** 协议 v3:一段 UTF-16 code unit(IME 提交结果) */
|
||||||
@ -44,10 +53,41 @@ class RemoteSurfaceView(context: Context) : SurfaceView(context) {
|
|||||||
fun sendKey(key: WinKey, down: Boolean)
|
fun sendKey(key: WinKey, down: Boolean)
|
||||||
/** 连接态:只有已连接才声明自己是文本编辑器,避免未连接时误弹键盘 */
|
/** 连接态:只有已连接才声明自己是文本编辑器,避免未连接时误弹键盘 */
|
||||||
fun isReadyForInput(): Boolean
|
fun isReadyForInput(): Boolean
|
||||||
|
/**
|
||||||
|
* 右键注入发生了(3b)。用于把「长按是否真的发了右键」变成**可持久观测**的状态。
|
||||||
|
*
|
||||||
|
* 为什么需要它:`onDebug` 是覆盖式的一行文本,长按后紧跟着的 ACTION_UP 会
|
||||||
|
* 立刻把它改写成「长按结束」——真机验收脚本因此看不到「长按右键」,
|
||||||
|
* 判据被后续事件冲掉。计数器不会丢。
|
||||||
|
*/
|
||||||
|
fun onRightClickInjected(source: String)
|
||||||
}
|
}
|
||||||
|
|
||||||
var listener: Listener? = null
|
var listener: Listener? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 相对指针模式(协议 v4)。默认 true:桌面远控的通行形态,手指不遮目标。
|
||||||
|
* 切换时清空累积器——上一模式的余数不该漏进新模式。
|
||||||
|
*/
|
||||||
|
var relativeMode: Boolean = true
|
||||||
|
set(value) {
|
||||||
|
if (field != value) {
|
||||||
|
field = value
|
||||||
|
relAcc.reset()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 灵敏度(仅相对模式生效)。默认 1.0 = 手指扫过视图全宽,指针走完桌面全宽。 */
|
||||||
|
var sensitivity: Float = RelPointer.DEFAULT_SENSITIVITY
|
||||||
|
set(value) {
|
||||||
|
field = RelPointer.clampSensitivity(value)
|
||||||
|
relAcc.reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 远端桌面像素尺寸(相对模式换算用);未连接时为 0,此时相对移动不发送 */
|
||||||
|
var remoteWidth: Int = 0
|
||||||
|
var remoteHeight: Int = 0
|
||||||
|
|
||||||
/** 视频宽高比(w/h)。>0 时视图按此比例测量。 */
|
/** 视频宽高比(w/h)。>0 时视图按此比例测量。 */
|
||||||
var videoAspectRatio = 0f
|
var videoAspectRatio = 0f
|
||||||
set(value) {
|
set(value) {
|
||||||
@ -70,6 +110,14 @@ class RemoteSurfaceView(context: Context) : SurfaceView(context) {
|
|||||||
private var twoFingerMoved = false
|
private var twoFingerMoved = false
|
||||||
private var wheelAccum = 0f
|
private var wheelAccum = 0f
|
||||||
|
|
||||||
|
/** 相对模式的小数累积器(慢速滑动不丢位移) */
|
||||||
|
private val relAcc = RelAccumulator()
|
||||||
|
|
||||||
|
/** 长按右键:计时任务 + 是否已消费本次手势的 UP(防「先右键再左键」) */
|
||||||
|
private val ui = Handler(Looper.getMainLooper())
|
||||||
|
private var longPressFired = false
|
||||||
|
private var longPressTwoFinger = false
|
||||||
|
|
||||||
/** 判定「拖拽 vs 点击」的位移阈值:12dp */
|
/** 判定「拖拽 vs 点击」的位移阈值:12dp */
|
||||||
private val dragSlopPx = resources.displayMetrics.density * 12f
|
private val dragSlopPx = resources.displayMetrics.density * 12f
|
||||||
|
|
||||||
@ -79,6 +127,32 @@ class RemoteSurfaceView(context: Context) : SurfaceView(context) {
|
|||||||
isFocusableInTouchMode = true
|
isFocusableInTouchMode = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 长按计时任务(Q5=A:≥500ms 且位移 < slop)。
|
||||||
|
* 命名刻意避开 `cancelLongPress`——`View` 已有同名方法,会被要求 override。
|
||||||
|
*/
|
||||||
|
private val longPressTask = Runnable {
|
||||||
|
val l = listener ?: return@Runnable
|
||||||
|
if (mode != Mode.TAP) return@Runnable // 已转拖拽/双指
|
||||||
|
val elapsed = System.currentTimeMillis() - downTime
|
||||||
|
val movedPx = hypot((lastX - downX).toDouble(), (lastY - downY).toDouble()).toFloat()
|
||||||
|
if (!LongPressPolicy.shouldFire(elapsed, movedPx, dragSlopPx, longPressTwoFinger)) {
|
||||||
|
return@Runnable
|
||||||
|
}
|
||||||
|
// 相对模式:在远端指针**当前位置**点右键,不发绝对移动
|
||||||
|
if (!relativeMode) l.sendMouseMove(normX(lastX), normY(lastY))
|
||||||
|
l.sendMouseButton(BTN_RIGHT, true)
|
||||||
|
l.sendMouseButton(BTN_RIGHT, false)
|
||||||
|
longPressFired = true
|
||||||
|
performClick()
|
||||||
|
onDebug?.invoke("长按右键")
|
||||||
|
listener?.onRightClickInjected("长按")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun cancelLongPressTimer() {
|
||||||
|
ui.removeCallbacks(longPressTask)
|
||||||
|
}
|
||||||
|
|
||||||
// ---- IME 通道 ----
|
// ---- IME 通道 ----
|
||||||
|
|
||||||
override fun onCheckIsTextEditor(): Boolean = listener?.isReadyForInput() == true
|
override fun onCheckIsTextEditor(): Boolean = listener?.isReadyForInput() == true
|
||||||
@ -135,6 +209,25 @@ class RemoteSurfaceView(context: Context) : SurfaceView(context) {
|
|||||||
private fun normX(x: Float) = (x / width * 65535).toInt().coerceIn(0, 65535)
|
private fun normX(x: Float) = (x / width * 65535).toInt().coerceIn(0, 65535)
|
||||||
private fun normY(y: Float) = (y / height * 65535).toInt().coerceIn(0, 65535)
|
private fun normY(y: Float) = (y / height * 65535).toInt().coerceIn(0, 65535)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 相对模式下把视图像素增量发出去(经累积取整)。
|
||||||
|
*
|
||||||
|
* 为什么 TAP/DRAG 都走这一个函数:两个分支都必须用**同一个**累积器,
|
||||||
|
* 分成两套会让 TAP→DRAG 切换时丢一次余数。
|
||||||
|
*/
|
||||||
|
private fun sendRelative(l: Listener, fromX: Float, fromY: Float, toX: Float, toY: Float) {
|
||||||
|
val dx = RelPointer.toDesktopDelta(toX - fromX, width, remoteWidth, sensitivity)
|
||||||
|
val dy = RelPointer.toDesktopDelta(toY - fromY, height, remoteHeight, sensitivity)
|
||||||
|
if (dx == 0f && dy == 0f) return
|
||||||
|
val d = relAcc.take(dx, dy) ?: return
|
||||||
|
l.sendMouseRel(d.first, d.second)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 绝对模式下按当前手指位置发归一化坐标 */
|
||||||
|
private fun sendAbsolute(l: Listener, x: Float, y: Float) {
|
||||||
|
l.sendMouseMove(normX(x), normY(y))
|
||||||
|
}
|
||||||
|
|
||||||
private fun moved(fromX: Float, fromY: Float, toX: Float, toY: Float) =
|
private fun moved(fromX: Float, fromY: Float, toX: Float, toY: Float) =
|
||||||
hypot((toX - fromX).toDouble(), (toY - fromY).toDouble()) > dragSlopPx
|
hypot((toX - fromX).toDouble(), (toY - fromY).toDouble()) > dragSlopPx
|
||||||
|
|
||||||
@ -166,8 +259,15 @@ class RemoteSurfaceView(context: Context) : SurfaceView(context) {
|
|||||||
lastX = event.x; lastY = event.y
|
lastX = event.x; lastY = event.y
|
||||||
downTime = System.currentTimeMillis()
|
downTime = System.currentTimeMillis()
|
||||||
mode = Mode.TAP
|
mode = Mode.TAP
|
||||||
// 先把远端指针移到位,点击/拖拽都以此为基准
|
longPressFired = false
|
||||||
l.sendMouseMove(normX(event.x), normY(event.y))
|
longPressTwoFinger = false
|
||||||
|
relAcc.reset()
|
||||||
|
// 相对模式:**不发**绝对移动。手指落下不该让远端指针跳到自己脚下
|
||||||
|
// (那正是相对鼠标要解决的问题);轻点落在指针当前位置。
|
||||||
|
if (!relativeMode) sendAbsolute(l, event.x, event.y)
|
||||||
|
// 长按右键计时(Q5=A:500ms 且不动)
|
||||||
|
cancelLongPressTimer()
|
||||||
|
ui.postDelayed(longPressTask, LongPressPolicy.LONG_PRESS_MS)
|
||||||
onDebug?.invoke("按下")
|
onDebug?.invoke("按下")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -176,6 +276,8 @@ class RemoteSurfaceView(context: Context) : SurfaceView(context) {
|
|||||||
// 必须先抬起,否则远端会留下一个被按住未释放的左键。
|
// 必须先抬起,否则远端会留下一个被按住未释放的左键。
|
||||||
if (mode == Mode.DRAG) l.sendMouseButton(BTN_LEFT, false)
|
if (mode == Mode.DRAG) l.sendMouseButton(BTN_LEFT, false)
|
||||||
mode = Mode.TWO_FINGER
|
mode = Mode.TWO_FINGER
|
||||||
|
longPressTwoFinger = true
|
||||||
|
cancelLongPressTimer()
|
||||||
twoFingerMoved = false
|
twoFingerMoved = false
|
||||||
wheelAccum = 0f
|
wheelAccum = 0f
|
||||||
twoFingerStartY = midY(event)
|
twoFingerStartY = midY(event)
|
||||||
@ -186,16 +288,28 @@ class RemoteSurfaceView(context: Context) : SurfaceView(context) {
|
|||||||
|
|
||||||
MotionEvent.ACTION_MOVE -> when (mode) {
|
MotionEvent.ACTION_MOVE -> when (mode) {
|
||||||
Mode.TAP -> if (moved(downX, downY, event.x, event.y)) {
|
Mode.TAP -> if (moved(downX, downY, event.x, event.y)) {
|
||||||
|
// 超过 slop = 拖拽,长按不再可能(先取消计时,避免拖到一半弹菜单)
|
||||||
|
cancelLongPressTimer()
|
||||||
mode = Mode.DRAG
|
mode = Mode.DRAG
|
||||||
l.sendMouseButton(BTN_LEFT, true)
|
l.sendMouseButton(BTN_LEFT, true)
|
||||||
|
if (relativeMode) {
|
||||||
|
// 相对拖拽:从落点开始按增量移动(不先跳到手指位置)
|
||||||
|
relAcc.reset()
|
||||||
|
sendRelative(l, downX, downY, event.x, event.y)
|
||||||
|
} else {
|
||||||
|
sendAbsolute(l, event.x, event.y)
|
||||||
|
}
|
||||||
lastX = event.x; lastY = event.y
|
lastX = event.x; lastY = event.y
|
||||||
l.sendMouseMove(normX(event.x), normY(event.y))
|
|
||||||
onDebug?.invoke("拖拽中")
|
onDebug?.invoke("拖拽中")
|
||||||
}
|
}
|
||||||
|
|
||||||
Mode.DRAG -> {
|
Mode.DRAG -> {
|
||||||
|
if (relativeMode) {
|
||||||
|
sendRelative(l, lastX, lastY, event.x, event.y)
|
||||||
|
} else {
|
||||||
|
sendAbsolute(l, event.x, event.y)
|
||||||
|
}
|
||||||
lastX = event.x; lastY = event.y
|
lastX = event.x; lastY = event.y
|
||||||
l.sendMouseMove(normX(event.x), normY(event.y))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Mode.TWO_FINGER -> {
|
Mode.TWO_FINGER -> {
|
||||||
@ -226,13 +340,22 @@ class RemoteSurfaceView(context: Context) : SurfaceView(context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
MotionEvent.ACTION_UP -> {
|
MotionEvent.ACTION_UP -> {
|
||||||
|
cancelLongPressTimer()
|
||||||
when (mode) {
|
when (mode) {
|
||||||
Mode.TAP -> {
|
Mode.TAP -> {
|
||||||
// 轻点 = 左键单击(位置已在 DOWN 时发送)
|
if (longPressFired) {
|
||||||
l.sendMouseButton(BTN_LEFT, true)
|
// 长按已弹右键:本次 UP 不能再发左键单击,
|
||||||
l.sendMouseButton(BTN_LEFT, false)
|
// 否则一次长按会「先右键再左键」,右键菜单立刻被左键点掉
|
||||||
performClick()
|
onDebug?.invoke("长按结束")
|
||||||
onDebug?.invoke("单击")
|
} else {
|
||||||
|
// 轻点 = 左键单击。绝对模式位置已在 DOWN 时发送;
|
||||||
|
// 相对模式点击**远端指针当前位置**,不发任何移动
|
||||||
|
if (!relativeMode) sendAbsolute(l, lastX, lastY)
|
||||||
|
l.sendMouseButton(BTN_LEFT, true)
|
||||||
|
l.sendMouseButton(BTN_LEFT, false)
|
||||||
|
performClick()
|
||||||
|
onDebug?.invoke("单击")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Mode.DRAG -> {
|
Mode.DRAG -> {
|
||||||
l.sendMouseButton(BTN_LEFT, false)
|
l.sendMouseButton(BTN_LEFT, false)
|
||||||
@ -244,11 +367,14 @@ class RemoteSurfaceView(context: Context) : SurfaceView(context) {
|
|||||||
Mode.IDLE -> {}
|
Mode.IDLE -> {}
|
||||||
}
|
}
|
||||||
mode = Mode.IDLE
|
mode = Mode.IDLE
|
||||||
|
longPressFired = false
|
||||||
}
|
}
|
||||||
|
|
||||||
MotionEvent.ACTION_CANCEL -> {
|
MotionEvent.ACTION_CANCEL -> {
|
||||||
|
cancelLongPressTimer()
|
||||||
if (mode == Mode.DRAG) l.sendMouseButton(BTN_LEFT, false)
|
if (mode == Mode.DRAG) l.sendMouseButton(BTN_LEFT, false)
|
||||||
mode = Mode.IDLE
|
mode = Mode.IDLE
|
||||||
|
longPressFired = false
|
||||||
onDebug?.invoke("手势取消")
|
onDebug?.invoke("手势取消")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -259,10 +385,13 @@ class RemoteSurfaceView(context: Context) : SurfaceView(context) {
|
|||||||
private fun maybeRightClick(l: Listener): Boolean {
|
private fun maybeRightClick(l: Listener): Boolean {
|
||||||
if (twoFingerMoved) return false
|
if (twoFingerMoved) return false
|
||||||
if (System.currentTimeMillis() - twoFingerDownTime > TWO_FINGER_TAP_MAX_MS) return false
|
if (System.currentTimeMillis() - twoFingerDownTime > TWO_FINGER_TAP_MAX_MS) return false
|
||||||
l.sendMouseMove(normX(lastX), normY(lastY))
|
// 双指右键沿用「先定位再点」:双指落点本身就是用户的定位意图,
|
||||||
|
// 与单指相对模式「不动指针」的语义不同(2a 起就是这个行为,回归判据依赖它)
|
||||||
|
sendAbsolute(l, lastX, lastY)
|
||||||
l.sendMouseButton(BTN_RIGHT, true)
|
l.sendMouseButton(BTN_RIGHT, true)
|
||||||
l.sendMouseButton(BTN_RIGHT, false)
|
l.sendMouseButton(BTN_RIGHT, false)
|
||||||
performClick()
|
performClick()
|
||||||
|
listener?.onRightClickInjected("双指")
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -36,6 +36,11 @@ object RustBridge {
|
|||||||
external fun nativeStats(handle: Long): Long
|
external fun nativeStats(handle: Long): Long
|
||||||
external fun nativeDesktopSize(handle: Long): Long
|
external fun nativeDesktopSize(handle: Long): Long
|
||||||
external fun nativeSendMouseMove(handle: Long, x: Int, y: Int)
|
external fun nativeSendMouseMove(handle: Long, x: Int, y: Int)
|
||||||
|
/**
|
||||||
|
* 协议 v4:相对移动(单位 = 被控端桌面物理像素)。
|
||||||
|
* Rust 侧把 Int clamp 到 i16,越界不会回绕;零位移在 viewer-core 被拦掉。
|
||||||
|
*/
|
||||||
|
external fun nativeSendMouseRel(handle: Long, dx: Int, dy: Int)
|
||||||
external fun nativeSendMouseButton(handle: Long, button: Int, down: Int)
|
external fun nativeSendMouseButton(handle: Long, button: Int, down: Int)
|
||||||
external fun nativeSendWheel(handle: Long, dy: Int)
|
external fun nativeSendWheel(handle: Long, dy: Int)
|
||||||
external fun nativeSendKey(handle: Long, scancode: Int, extended: Int, down: Int)
|
external fun nativeSendKey(handle: Long, scancode: Int, extended: Int, down: Int)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user