diff --git a/lib/features/recording/widgets/widget_recording_button.dart b/lib/features/recording/widgets/widget_recording_button.dart new file mode 100644 index 0000000..2fd3b3c --- /dev/null +++ b/lib/features/recording/widgets/widget_recording_button.dart @@ -0,0 +1,63 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +/// 录制控制按钮:白圈 + 红色内芯,录制中为圆角矩形。 +class RecordingControlButton extends StatelessWidget { + const RecordingControlButton({ + super.key, + required this.isRecording, + required this.onTap, + this.enabled = true, + this.size, + }); + + final bool isRecording; + final VoidCallback? onTap; + final bool enabled; + final double? size; + + @override + Widget build(BuildContext context) { + final buttonSize = size ?? 70.r; + final borderWidth = 4.r; + final idleInnerSize = 62.r; + final recordingInnerSize = 22.r; + final recordingCornerRadius = 6.r; + + final innerSize = isRecording ? recordingInnerSize : idleInnerSize; + final borderRadius = isRecording + ? recordingCornerRadius + : idleInnerSize / 2; + + return GestureDetector( + onTap: enabled ? onTap : null, + child: SizedBox( + width: buttonSize, + height: buttonSize, + child: Stack( + alignment: Alignment.center, + children: [ + Container( + width: buttonSize, + height: buttonSize, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all(color: Colors.white, width: borderWidth), + ), + ), + AnimatedContainer( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOutCubic, + width: innerSize, + height: innerSize, + decoration: BoxDecoration( + color: Colors.red, + borderRadius: BorderRadius.circular(borderRadius), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/recording/widgets/widget_recording_hud.dart b/lib/features/recording/widgets/widget_recording_hud.dart index 188c502..143a372 100644 --- a/lib/features/recording/widgets/widget_recording_hud.dart +++ b/lib/features/recording/widgets/widget_recording_hud.dart @@ -4,6 +4,7 @@ import 'package:permission_handler/permission_handler.dart'; import 'package:recording_tool/core/utils/rate_limiter.dart'; import 'package:recording_tool/features/recording/model/model_recording_session.dart'; import 'package:recording_tool/features/recording/widgets/widget_clipboard_address_clock_chip.dart'; +import 'package:recording_tool/features/recording/widgets/widget_recording_button.dart'; import 'package:recording_tool/features/recording/widgets/widget_recording_setup_hints.dart'; /// 录制页 HUD 层(状态提示、录制控制) @@ -112,44 +113,31 @@ class RecordingHudWidget extends StatelessWidget { right: 0, bottom: _recordButtonBottom, child: Center( - child: GestureDetector( - onTap: state.isStartingRecording - ? null - : () async { - if (state.isRecording) { - RateLimit.instance.debounce( - key: 'recording.session.stop', - value: null, - duration: Duration(milliseconds: 300), - onCallback: (_) async { - await onStop(); - }, - ); - } else { - RateLimit.instance.debounce( - key: 'recording.session.start', - value: null, - duration: Duration(milliseconds: 300), - onCallback: (_) async { - await onStart(); - }, - ); - } + child: RecordingControlButton( + isRecording: state.isRecording, + enabled: !state.isStartingRecording, + size: _recordButtonSize, + onTap: () { + if (state.isRecording) { + RateLimit.instance.debounce( + key: 'recording.session.stop', + value: null, + duration: Duration(milliseconds: 300), + onCallback: (_) async { + await onStop(); }, - child: Container( - width: _recordButtonSize, - height: _recordButtonSize, - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all(color: Colors.white, width: 4.r), - color: state.isRecording ? Colors.white : Colors.red, - ), - child: Icon( - state.isRecording ? Icons.stop : Icons.fiber_manual_record, - color: state.isRecording ? Colors.red : Colors.white, - size: 32.r, - ), - ), + ); + } else { + RateLimit.instance.debounce( + key: 'recording.session.start', + value: null, + duration: Duration(milliseconds: 300), + onCallback: (_) async { + await onStart(); + }, + ); + } + }, ), ), ), diff --git a/lib/features/recording/widgets/widget_recording_touch_lock_overlay.dart b/lib/features/recording/widgets/widget_recording_touch_lock_overlay.dart index ae44c5a..18e1c90 100644 --- a/lib/features/recording/widgets/widget_recording_touch_lock_overlay.dart +++ b/lib/features/recording/widgets/widget_recording_touch_lock_overlay.dart @@ -24,6 +24,7 @@ class _RecordingTouchLockOverlayWidgetState extends State { Timer? _holdTimer; bool _isHolding = false; + int? _remainingSeconds; @override void didUpdateWidget(RecordingTouchLockOverlayWidget oldWidget) { @@ -35,25 +36,48 @@ class _RecordingTouchLockOverlayWidgetState @override void dispose() { - _cancelHold(); + _holdTimer?.cancel(); + _holdTimer = null; super.dispose(); } void _cancelHold() { _holdTimer?.cancel(); _holdTimer = null; - _isHolding = false; + if (!_isHolding && _remainingSeconds == null) return; + setState(() { + _isHolding = false; + _remainingSeconds = null; + }); } void _startHold() { if (!widget.enabled) return; - setState(() => _isHolding = true); + final totalSeconds = widget.unlockHoldDuration.inSeconds; _holdTimer?.cancel(); - _holdTimer = Timer(widget.unlockHoldDuration, () { - if (!mounted) return; - _cancelHold(); - widget.onUnlocked(); - setState(() {}); + setState(() { + _isHolding = true; + _remainingSeconds = totalSeconds; + }); + + var elapsed = 0; + _holdTimer = Timer.periodic(const Duration(seconds: 1), (timer) { + elapsed += 1; + if (!mounted) { + timer.cancel(); + return; + } + if (elapsed >= totalSeconds) { + timer.cancel(); + _holdTimer = null; + widget.onUnlocked(); + setState(() { + _isHolding = false; + _remainingSeconds = null; + }); + return; + } + setState(() => _remainingSeconds = totalSeconds - elapsed); }); } @@ -85,12 +109,57 @@ class _RecordingTouchLockOverlayWidgetState horizontal: 16.r, vertical: 8.r, ), - child: Text( - _isHolding - ? '保持按住 ${widget.unlockHoldDuration.inSeconds}s 解锁…' - : '防误触已开启,按住 ${widget.unlockHoldDuration.inSeconds}s 解锁', - style: TextStyle(color: Colors.white, fontSize: 10.sp), - ), + child: _isHolding && _remainingSeconds != null + ? Builder( + builder: (context) { + final remainingSeconds = _remainingSeconds!; + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + AnimatedSwitcher( + duration: const Duration(milliseconds: 280), + switchInCurve: Curves.easeOut, + switchOutCurve: Curves.easeIn, + transitionBuilder: (child, animation) { + return ScaleTransition( + scale: Tween(begin: 0.6, end: 1) + .animate(animation), + child: FadeTransition( + opacity: animation, + child: child, + ), + ); + }, + child: Text( + '${remainingSeconds}s', + key: ValueKey(remainingSeconds), + style: TextStyle( + color: Colors.white, + fontSize: 18.sp, + fontWeight: FontWeight.w600, + height: 1.1, + ), + ), + ), + SizedBox(height: 2.r), + Text( + '保持按住解锁', + style: TextStyle( + color: Colors.white70, + fontSize: 10.sp, + ), + ), + ], + ); + }, + ) + : Text( + '防误触已开启,按住 ${widget.unlockHoldDuration.inSeconds}s 解锁', + style: TextStyle( + color: Colors.white, + fontSize: 10.sp, + ), + ), ), ), ),