import 'dart:async'; import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; /// 远端触摸控制层(对应 Android 端 RemoteTouchView)。 /// /// 捕获指针(触摸/鼠标)事件,将其转换为相对于控件区域的百分比坐标 /// (0.0 ~ 1.0),以便在不同分辨率的设备间换算。 /// /// - 短时间内位移很小的抬起 -> 单击(TOUCH) /// - 否则 -> 滑动(SWIPE) /// - 按住不动超过阈值 -> 长按(LONG_PRESS) /// 同时捕获物理按键事件(KEY),用于外接键盘 / 遥控器场景。 /// /// 该控件完全基于 Flutter 框架实现,无需任何原生代码,可运行于 /// Android、iOS 等平台。 class RemoteTouchView extends StatefulWidget { final void Function(double x, double y) onTouch; final void Function( double x1, double y1, double x2, double y2, int durationMs, ) onSwipe; final void Function(int keyCode, int action) onKey; final void Function(double x, double y) onLongPress; final void Function(int action, double x, double y) onMotionEvent; const RemoteTouchView({ super.key, required this.onTouch, required this.onSwipe, required this.onKey, required this.onLongPress, required this.onMotionEvent, }); @override State createState() => _RemoteTouchViewState(); } class _RemoteTouchViewState extends State { Offset? _startPosition; Offset? _currentPosition; DateTime? _startTime; bool _isLongPressed = false; Timer? _longPressTimer; // 缓存 RenderBox 信息以优化坐标换算开销 RenderBox? _cachedRenderBox; Size? _cachedSize; // 采样率限制:控制 ACTION_MOVE 的发送频率(例如每 16ms 发送一次,约 60fps) int _lastMoveTimestamp = 0; static const int _sampleIntervalMs = 16; /// 长按触发阈值,对应 Android GestureDetector 默认的 LONG_PRESS_TIMEOUT(400ms)。 static const _longPressTimeout = Duration(milliseconds: 400); /// 手指移动超过该像素阈值则视为滚动,取消长按(对应 Android touch slop)。 static const _touchSlop = 10.0; double _toRelativeX(Offset local, Size size) => (local.dx / size.width).clamp(0.0, 1.0); double _toRelativeY(Offset local, Size size) => (local.dy / size.height).clamp(0.0, 1.0); void _cancelLongPress() { _longPressTimer?.cancel(); _longPressTimer = null; } @override void dispose() { _cancelLongPress(); super.dispose(); } @override Widget build(BuildContext context) { return Focus( autofocus: true, onKeyEvent: (node, event) { if (event is KeyDownEvent) { widget.onKey(event.logicalKey.keyId, 0); return KeyEventResult.handled; } else if (event is KeyUpEvent) { widget.onKey(event.logicalKey.keyId, 1); return KeyEventResult.handled; } return KeyEventResult.ignored; }, child: Listener( behavior: HitTestBehavior.opaque, onPointerDown: (event) { _cancelLongPress(); _isLongPressed = false; _startPosition = event.localPosition; _currentPosition = event.localPosition; _startTime = DateTime.now(); _lastMoveTimestamp = 0; // 重置采样计时 // 开始触摸时缓存 RenderBox 和 Size _cachedRenderBox = context.findRenderObject() as RenderBox?; _cachedSize = _cachedRenderBox?.size; if (_cachedSize != null) { final rx = _toRelativeX(event.localPosition, _cachedSize!); final ry = _toRelativeY(event.localPosition, _cachedSize!); widget.onMotionEvent(0, rx, ry); // ACTION_DOWN } _longPressTimer = Timer(_longPressTimeout, () { if (_currentPosition == null || _cachedSize == null) return; _isLongPressed = true; final x = _toRelativeX(_currentPosition!, _cachedSize!); final y = _toRelativeY(_currentPosition!, _cachedSize!); widget.onLongPress(x, y); }); }, onPointerMove: (event) { _currentPosition = event.localPosition; if (_cachedSize != null) { final now = DateTime.now().millisecondsSinceEpoch; // 采样率限制逻辑 if (now - _lastMoveTimestamp >= _sampleIntervalMs) { _lastMoveTimestamp = now; final rx = _toRelativeX(event.localPosition, _cachedSize!); final ry = _toRelativeY(event.localPosition, _cachedSize!); widget.onMotionEvent(2, rx, ry); // ACTION_MOVE } } if (_longPressTimer != null && _startPosition != null) { if ((event.localPosition - _startPosition!).distance > _touchSlop) { _cancelLongPress(); } } }, onPointerUp: (event) { _cancelLongPress(); if (_cachedSize != null) { final rx = _toRelativeX(event.localPosition, _cachedSize!); final ry = _toRelativeY(event.localPosition, _cachedSize!); widget.onMotionEvent(1, rx, ry); // ACTION_UP if (!_isLongPressed && _startPosition != null && _startTime != null) { final startX = _toRelativeX(_startPosition!, _cachedSize!); final startY = _toRelativeY(_startPosition!, _cachedSize!); final duration = DateTime.now().difference(_startTime!).inMilliseconds; final dx = (rx - startX).abs(); final dy = (ry - startY).abs(); if (duration < 200 && dx < 0.02 && dy < 0.02) { widget.onTouch(startX, startY); } else { widget.onSwipe(startX, startY, rx, ry, max(duration, 1)); } } } // 清理状态 _startPosition = null; _currentPosition = null; _startTime = null; _cachedRenderBox = null; _cachedSize = null; }, child: const ColoredBox( color: Colors.transparent, child: SizedBox.expand(), ), ), ); } }