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; /// 长按触发阈值,对应 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) { debugPrint('RemoteTouchView: KeyDown - ${event.logicalKey.debugName} (${event.logicalKey.keyId})'); widget.onKey(event.logicalKey.keyId, 0); return KeyEventResult.handled; } else if (event is KeyUpEvent) { debugPrint('RemoteTouchView: KeyUp - ${event.logicalKey.debugName} (${event.logicalKey.keyId})'); 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(); final size = context.size; if (size != null) { final rx = _toRelativeX(event.localPosition, size); final ry = _toRelativeY(event.localPosition, size); debugPrint('RemoteTouchView: PointerDown - relX: ${rx.toStringAsFixed(3)}, relY: ${ry.toStringAsFixed(3)}'); widget.onMotionEvent(0, rx, ry); } _longPressTimer = Timer(_longPressTimeout, () { final size = context.size; if (_currentPosition == null || size == null) return; _isLongPressed = true; final x = _toRelativeX(_currentPosition!, size); final y = _toRelativeY(_currentPosition!, size); debugPrint('RemoteTouchView: LongPress - relX: ${x.toStringAsFixed(3)}, relY: ${y.toStringAsFixed(3)}'); widget.onLongPress(x, y); }); }, onPointerMove: (event) { _currentPosition = event.localPosition; final size = context.size; if (size != null) { final rx = _toRelativeX(event.localPosition, size); final ry = _toRelativeY(event.localPosition, size); // Move 事件较多,可以考虑控制日志频率,这里直接打印 debugPrint('RemoteTouchView: PointerMove - relX: ${rx.toStringAsFixed(3)}, relY: ${ry.toStringAsFixed(3)}'); widget.onMotionEvent(2, rx, ry); } if (_longPressTimer != null && _startPosition != null) { final dx = (event.localPosition.dx - _startPosition!.dx).abs(); final dy = (event.localPosition.dy - _startPosition!.dy).abs(); if (dx > _touchSlop || dy > _touchSlop) { _cancelLongPress(); } } }, onPointerUp: (event) { _cancelLongPress(); final size = context.size; if (size != null) { final rx = _toRelativeX(event.localPosition, size); final ry = _toRelativeY(event.localPosition, size); debugPrint('RemoteTouchView: PointerUp - relX: ${rx.toStringAsFixed(3)}, relY: ${ry.toStringAsFixed(3)}'); widget.onMotionEvent(1, rx, ry); } // 长按已触发则不再派发单击 / 滑动事件(对应 Android 端 isLongPressed 逻辑)。 if (_isLongPressed) { _startPosition = null; _currentPosition = null; _startTime = null; return; } if (_startPosition == null || size == null || _startTime == null) { return; } final startX = _toRelativeX(_startPosition!, size); final startY = _toRelativeY(_startPosition!, size); final endX = _toRelativeX(event.localPosition, size); final endY = _toRelativeY(event.localPosition, size); final duration = DateTime.now().difference(_startTime!).inMilliseconds; final dx = (endX - startX).abs(); final dy = (endY - startY).abs(); if (duration < 200 && dx < 0.02 && dy < 0.02) { debugPrint('RemoteTouchView: Detected Touch - x: ${startX.toStringAsFixed(3)}, y: ${startY.toStringAsFixed(3)}'); widget.onTouch(startX, startY); } else { debugPrint('RemoteTouchView: Detected Swipe - from (${startX.toStringAsFixed(3)}, ${startY.toStringAsFixed(3)}) to (${endX.toStringAsFixed(3)}, ${endY.toStringAsFixed(3)}), duration: ${max(duration, 1)}ms'); widget.onSwipe(startX, startY, endX, endY, max(duration, 1)); } _startPosition = null; _currentPosition = null; _startTime = null; }, child: const ColoredBox( color: Colors.transparent, child: SizedBox.expand(), ), ), ); } }