Files
VibeCoding/webrtc_controller_flutter/lib/widgets/remote_touch_view.dart

188 lines
6.9 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<RemoteTouchView> createState() => _RemoteTouchViewState();
}
class _RemoteTouchViewState extends State<RemoteTouchView> {
Offset? _startPosition;
Offset? _currentPosition;
DateTime? _startTime;
bool _isLongPressed = false;
Timer? _longPressTimer;
/// 长按触发阈值,对应 Android GestureDetector 默认的 LONG_PRESS_TIMEOUT400ms
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(),
),
),
);
}
}