Files

169 lines
5.0 KiB
Dart
Raw Permalink Normal View History

2026-07-14 11:11:36 +08:00
import 'dart:async';
import 'package:flutter/material.dart';
import '../../../../core/infra/player_engine/player_engine.dart';
enum _DragMode { none, brightness, volume, seek }
class AndroidGestureLayer extends StatefulWidget {
const AndroidGestureLayer({
super.key,
required this.enabled,
required this.player,
required this.onToggleControls,
required this.onDoubleTapCenter,
required this.onDoubleTapLeft,
required this.onDoubleTapRight,
required this.onBrightnessChange,
required this.onVolumeChange,
required this.onVerticalDragEnd,
required this.onHorizontalSeekUpdate,
required this.onHorizontalSeekStart,
required this.onHorizontalSeekEnd,
required this.onLongPressStart,
required this.onLongPressEnd,
});
final bool enabled;
final PlayerEngine player;
final VoidCallback onToggleControls;
final VoidCallback onDoubleTapCenter;
final VoidCallback onDoubleTapLeft;
final VoidCallback onDoubleTapRight;
final ValueChanged<double> onBrightnessChange;
final ValueChanged<double> onVolumeChange;
final VoidCallback onVerticalDragEnd;
final ValueChanged<Duration> onHorizontalSeekUpdate;
final VoidCallback onHorizontalSeekStart;
final VoidCallback onHorizontalSeekEnd;
final ValueChanged<bool> onLongPressStart;
final VoidCallback onLongPressEnd;
@override
State<AndroidGestureLayer> createState() => _AndroidGestureLayerState();
}
class _AndroidGestureLayerState extends State<AndroidGestureLayer> {
_DragMode _dragMode = _DragMode.none;
Offset _panStartPos = Offset.zero;
double _cumulativeDx = 0;
double _cumulativeDy = 0;
Timer? _doubleTapTimer;
Offset _lastTapDownPos = Offset.zero;
void _handleTapDown(TapDownDetails details) {
_lastTapDownPos = details.localPosition;
}
void _handleTapUp(TapUpDetails details) {
final pos = _lastTapDownPos;
if (_doubleTapTimer != null) {
_doubleTapTimer!.cancel();
_doubleTapTimer = null;
widget.onToggleControls();
_executeDoubleTap(pos);
} else {
widget.onToggleControls();
_doubleTapTimer = Timer(const Duration(milliseconds: 300), () {
_doubleTapTimer = null;
});
}
}
void _executeDoubleTap(Offset pos) {
final width = context.size?.width ?? 0;
final third = width / 3;
if (pos.dx < third) {
widget.onDoubleTapLeft();
} else if (pos.dx > third * 2) {
widget.onDoubleTapRight();
} else {
widget.onDoubleTapCenter();
}
}
void _handlePanStart(DragStartDetails details) {
_panStartPos = details.localPosition;
_dragMode = _DragMode.none;
_cumulativeDx = 0;
_cumulativeDy = 0;
}
void _handlePanUpdate(DragUpdateDetails details) {
_cumulativeDx += details.delta.dx;
_cumulativeDy += details.delta.dy;
if (_dragMode == _DragMode.none) {
final absDx = _cumulativeDx.abs();
final absDy = _cumulativeDy.abs();
if (absDx < 20 && absDy < 20) return;
if (absDx > absDy * 1.5) {
_dragMode = _DragMode.seek;
widget.onHorizontalSeekStart();
} else if (absDy > absDx * 1.5) {
final screenWidth = context.size?.width ?? 1;
_dragMode = _panStartPos.dx < screenWidth / 2
? _DragMode.brightness
: _DragMode.volume;
} else {
return;
}
}
if (_dragMode == _DragMode.seek) {
final screenWidth = context.size?.width ?? 1;
final totalDuration = widget.player.state.duration;
final seekDelta = totalDuration * (_cumulativeDx / screenWidth) * 0.5;
widget.onHorizontalSeekUpdate(seekDelta);
} else if (_dragMode == _DragMode.brightness) {
final screenHeight = context.size?.height ?? 1;
final delta = -details.delta.dy / (screenHeight * 0.7);
widget.onBrightnessChange(delta);
} else if (_dragMode == _DragMode.volume) {
final screenHeight = context.size?.height ?? 1;
final delta = -details.delta.dy / (screenHeight * 1.5);
widget.onVolumeChange(delta);
}
}
void _handlePanEnd(DragEndDetails details) {
if (_dragMode == _DragMode.seek) {
widget.onHorizontalSeekEnd();
} else if (_dragMode == _DragMode.brightness ||
_dragMode == _DragMode.volume) {
widget.onVerticalDragEnd();
}
_dragMode = _DragMode.none;
}
@override
void dispose() {
_doubleTapTimer?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
if (!widget.enabled) return const SizedBox.expand();
return GestureDetector(
behavior: HitTestBehavior.translucent,
onTapDown: _handleTapDown,
onTapUp: _handleTapUp,
onPanStart: _handlePanStart,
onPanUpdate: _handlePanUpdate,
onPanEnd: _handlePanEnd,
onLongPressStart: (details) {
final screenWidth = context.size?.width ?? 1;
final isLeftSide = details.localPosition.dx < screenWidth / 2;
widget.onLongPressStart(isLeftSide);
},
onLongPressEnd: (_) => widget.onLongPressEnd(),
child: const SizedBox.expand(),
);
}
}