更换 录制按钮 UI

This commit is contained in:
2026-06-05 18:29:49 +08:00
parent 1e08b70c39
commit 26098114d2
3 changed files with 171 additions and 51 deletions

View File

@@ -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),
),
),
],
),
),
);
}
}

View File

@@ -4,6 +4,7 @@ import 'package:permission_handler/permission_handler.dart';
import 'package:recording_tool/core/utils/rate_limiter.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/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_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'; import 'package:recording_tool/features/recording/widgets/widget_recording_setup_hints.dart';
/// 录制页 HUD 层(状态提示、录制控制) /// 录制页 HUD 层(状态提示、录制控制)
@@ -112,44 +113,31 @@ class RecordingHudWidget extends StatelessWidget {
right: 0, right: 0,
bottom: _recordButtonBottom, bottom: _recordButtonBottom,
child: Center( child: Center(
child: GestureDetector( child: RecordingControlButton(
onTap: state.isStartingRecording isRecording: state.isRecording,
? null enabled: !state.isStartingRecording,
: () async { size: _recordButtonSize,
if (state.isRecording) { onTap: () {
RateLimit.instance.debounce<void>( if (state.isRecording) {
key: 'recording.session.stop', RateLimit.instance.debounce<void>(
value: null, key: 'recording.session.stop',
duration: Duration(milliseconds: 300), value: null,
onCallback: (_) async { duration: Duration(milliseconds: 300),
await onStop(); onCallback: (_) async {
}, await onStop();
);
} else {
RateLimit.instance.debounce<void>(
key: 'recording.session.start',
value: null,
duration: Duration(milliseconds: 300),
onCallback: (_) async {
await onStart();
},
);
}
}, },
child: Container( );
width: _recordButtonSize, } else {
height: _recordButtonSize, RateLimit.instance.debounce<void>(
decoration: BoxDecoration( key: 'recording.session.start',
shape: BoxShape.circle, value: null,
border: Border.all(color: Colors.white, width: 4.r), duration: Duration(milliseconds: 300),
color: state.isRecording ? Colors.white : Colors.red, onCallback: (_) async {
), await onStart();
child: Icon( },
state.isRecording ? Icons.stop : Icons.fiber_manual_record, );
color: state.isRecording ? Colors.red : Colors.white, }
size: 32.r, },
),
),
), ),
), ),
), ),

View File

@@ -24,6 +24,7 @@ class _RecordingTouchLockOverlayWidgetState
extends State<RecordingTouchLockOverlayWidget> { extends State<RecordingTouchLockOverlayWidget> {
Timer? _holdTimer; Timer? _holdTimer;
bool _isHolding = false; bool _isHolding = false;
int? _remainingSeconds;
@override @override
void didUpdateWidget(RecordingTouchLockOverlayWidget oldWidget) { void didUpdateWidget(RecordingTouchLockOverlayWidget oldWidget) {
@@ -35,25 +36,48 @@ class _RecordingTouchLockOverlayWidgetState
@override @override
void dispose() { void dispose() {
_cancelHold(); _holdTimer?.cancel();
_holdTimer = null;
super.dispose(); super.dispose();
} }
void _cancelHold() { void _cancelHold() {
_holdTimer?.cancel(); _holdTimer?.cancel();
_holdTimer = null; _holdTimer = null;
_isHolding = false; if (!_isHolding && _remainingSeconds == null) return;
setState(() {
_isHolding = false;
_remainingSeconds = null;
});
} }
void _startHold() { void _startHold() {
if (!widget.enabled) return; if (!widget.enabled) return;
setState(() => _isHolding = true); final totalSeconds = widget.unlockHoldDuration.inSeconds;
_holdTimer?.cancel(); _holdTimer?.cancel();
_holdTimer = Timer(widget.unlockHoldDuration, () { setState(() {
if (!mounted) return; _isHolding = true;
_cancelHold(); _remainingSeconds = totalSeconds;
widget.onUnlocked(); });
setState(() {});
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, horizontal: 16.r,
vertical: 8.r, vertical: 8.r,
), ),
child: Text( child: _isHolding && _remainingSeconds != null
_isHolding ? Builder(
? '保持按住 ${widget.unlockHoldDuration.inSeconds}s 解锁…' builder: (context) {
: '防误触已开启,按住 ${widget.unlockHoldDuration.inSeconds}s 解锁', final remainingSeconds = _remainingSeconds!;
style: TextStyle(color: Colors.white, fontSize: 10.sp), 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<double>(begin: 0.6, end: 1)
.animate(animation),
child: FadeTransition(
opacity: animation,
child: child,
),
);
},
child: Text(
'${remainingSeconds}s',
key: ValueKey<int>(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,
),
),
), ),
), ),
), ),