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; const RemoteTouchView({ super.key, required this.onTouch, required this.onSwipe, required this.onKey, required this.onLongPress, }); @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) { 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(); _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); widget.onLongPress(x, y); }); }, onPointerMove: (event) { _currentPosition = event.localPosition; 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(); // 长按已触发则不再派发单击 / 滑动事件(对应 Android 端 isLongPressed 逻辑)。 if (_isLongPressed) { _startPosition = null; _currentPosition = null; _startTime = null; return; } final size = context.size; 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) { widget.onTouch(startX, startY); } else { widget.onSwipe(startX, startY, endX, endY, max(duration, 1)); } _startPosition = null; _currentPosition = null; _startTime = null; }, child: const ColoredBox( color: Colors.transparent, child: SizedBox.expand(), ), ), ); } }