169 lines
5.0 KiB
Dart
169 lines
5.0 KiB
Dart
|
|
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(),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|