录制按钮增加动画
This commit is contained in:
@@ -275,25 +275,23 @@ class _RecordingPageState extends ConsumerState<RecordingPage> {
|
||||
if (!state.isPreviewReady && state.errorMessage == null)
|
||||
const RecordingLoadingOverlayWidget(message: '正在启动相机…'),
|
||||
const RecordTimerWidget(),
|
||||
RepaintBoundary(
|
||||
child: RecordingHudWidget(
|
||||
state: state,
|
||||
showClipboardHint: showClipboardInfo,
|
||||
clipboardAddress: clipboard.address.trim(),
|
||||
onStart: _onStartRecording,
|
||||
onStop: _stopRecordingAndShowResult,
|
||||
onOpenDnd: () async {
|
||||
await viewModel.openDndSettings();
|
||||
await viewModel.refreshDndAccess();
|
||||
},
|
||||
onOpenBattery: () async {
|
||||
await viewModel.openBatterySettings();
|
||||
await viewModel.refreshBatteryOptimization();
|
||||
},
|
||||
onToggleTouchLock: () {
|
||||
viewModel.setTouchLocked(!state.isTouchLocked);
|
||||
},
|
||||
),
|
||||
RecordingHudWidget(
|
||||
state: state,
|
||||
showClipboardHint: showClipboardInfo,
|
||||
clipboardAddress: clipboard.address.trim(),
|
||||
onStart: _onStartRecording,
|
||||
onStop: _stopRecordingAndShowResult,
|
||||
onOpenDnd: () async {
|
||||
await viewModel.openDndSettings();
|
||||
await viewModel.refreshDndAccess();
|
||||
},
|
||||
onOpenBattery: () async {
|
||||
await viewModel.openBatterySettings();
|
||||
await viewModel.refreshBatteryOptimization();
|
||||
},
|
||||
onToggleTouchLock: () {
|
||||
viewModel.setTouchLocked(!state.isTouchLocked);
|
||||
},
|
||||
),
|
||||
if (state.isTouchLocked && state.isRecording)
|
||||
RecordingTouchLockOverlayWidget(
|
||||
|
||||
@@ -24,7 +24,9 @@ class _RecordTimerWidgetState extends ConsumerState<RecordTimerWidget> {
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Center(
|
||||
child: Container(
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 380),
|
||||
curve: Curves.easeOutCubic,
|
||||
padding: EdgeInsets.symmetric(horizontal: 5.r, vertical: 0.r),
|
||||
decoration: BoxDecoration(
|
||||
color: isRecording ? Colors.red : Colors.transparent,
|
||||
|
||||
@@ -1,62 +1,164 @@
|
||||
import 'dart:ui' show lerpDouble;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
|
||||
/// 录制控制按钮:白圈 + 红色内芯,录制中为圆角矩形。
|
||||
class RecordingControlButton extends StatelessWidget {
|
||||
class RecordingControlButton extends StatefulWidget {
|
||||
const RecordingControlButton({
|
||||
super.key,
|
||||
required this.isRecording,
|
||||
required this.onTap,
|
||||
this.isStartingRecording = false,
|
||||
this.enabled = true,
|
||||
this.size,
|
||||
});
|
||||
|
||||
final bool isRecording;
|
||||
final bool isStartingRecording;
|
||||
final VoidCallback? onTap;
|
||||
final bool enabled;
|
||||
final double? size;
|
||||
|
||||
@override
|
||||
State<RecordingControlButton> createState() => _RecordingControlButtonState();
|
||||
}
|
||||
|
||||
class _RecordingControlButtonState extends State<RecordingControlButton>
|
||||
with TickerProviderStateMixin {
|
||||
static const _morphDuration = Duration(milliseconds: 380);
|
||||
static const _pressDownDuration = Duration(milliseconds: 120);
|
||||
static const _pressUpDuration = Duration(milliseconds: 180);
|
||||
|
||||
late final AnimationController _morphController;
|
||||
late final AnimationController _pressController;
|
||||
late final CurvedAnimation _morphAnimation;
|
||||
late final Animation<double> _pressScale;
|
||||
|
||||
bool get _targetIsRecording =>
|
||||
widget.isRecording || widget.isStartingRecording;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_morphController = AnimationController(
|
||||
vsync: this,
|
||||
duration: _morphDuration,
|
||||
value: _targetIsRecording ? 1 : 0,
|
||||
);
|
||||
_morphAnimation = CurvedAnimation(
|
||||
parent: _morphController,
|
||||
curve: Curves.easeOutCubic,
|
||||
reverseCurve: Curves.easeInCubic,
|
||||
);
|
||||
_pressController = AnimationController(
|
||||
vsync: this,
|
||||
duration: _pressDownDuration,
|
||||
);
|
||||
_pressScale = Tween<double>(begin: 1, end: 0.94).animate(
|
||||
CurvedAnimation(
|
||||
parent: _pressController,
|
||||
curve: Curves.easeOut,
|
||||
reverseCurve: Curves.easeOutBack,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant RecordingControlButton oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
final oldTarget =
|
||||
oldWidget.isRecording || oldWidget.isStartingRecording;
|
||||
final newTarget = _targetIsRecording;
|
||||
if (oldTarget != newTarget) {
|
||||
if (newTarget) {
|
||||
_morphController.forward();
|
||||
} else {
|
||||
_morphController.reverse();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_morphAnimation.dispose();
|
||||
_morphController.dispose();
|
||||
_pressController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _handlePressDown() {
|
||||
if (!widget.enabled) return;
|
||||
_pressController.duration = _pressDownDuration;
|
||||
_pressController.forward();
|
||||
}
|
||||
|
||||
void _handlePressUp() {
|
||||
if (!widget.enabled) return;
|
||||
_pressController.duration = _pressUpDuration;
|
||||
_pressController.reverse();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final buttonSize = size ?? 70.r;
|
||||
final buttonSize = widget.size ?? 70.r;
|
||||
final borderWidth = 4.r;
|
||||
final idleInnerSize = 62.r;
|
||||
final recordingInnerSize = 22.r;
|
||||
final idleCornerRadius = idleInnerSize / 2;
|
||||
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(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTapDown: (_) => _handlePressDown(),
|
||||
onTapUp: (_) => _handlePressUp(),
|
||||
onTapCancel: _handlePressUp,
|
||||
onTap: widget.enabled ? widget.onTap : null,
|
||||
child: AnimatedBuilder(
|
||||
animation: Listenable.merge([_morphController, _pressController]),
|
||||
builder: (context, child) {
|
||||
final morph = _morphAnimation.value;
|
||||
|
||||
final innerSize = lerpDouble(
|
||||
idleInnerSize,
|
||||
recordingInnerSize,
|
||||
morph,
|
||||
)!;
|
||||
final cornerRadius = lerpDouble(
|
||||
idleCornerRadius,
|
||||
recordingCornerRadius,
|
||||
morph,
|
||||
)!;
|
||||
|
||||
return Transform.scale(
|
||||
scale: _pressScale.value,
|
||||
child: SizedBox(
|
||||
width: buttonSize,
|
||||
height: buttonSize,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.white, width: borderWidth),
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: buttonSize,
|
||||
height: buttonSize,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.white, width: borderWidth),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
width: innerSize,
|
||||
height: innerSize,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red,
|
||||
borderRadius: BorderRadius.circular(cornerRadius),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 500),
|
||||
curve: Curves.ease,
|
||||
width: innerSize,
|
||||
height: innerSize,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red,
|
||||
borderRadius: BorderRadius.circular(borderRadius),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -127,6 +127,7 @@ class RecordingHudWidget extends StatelessWidget {
|
||||
child: Center(
|
||||
child: RecordingControlButton(
|
||||
isRecording: state.isRecording,
|
||||
isStartingRecording: state.isStartingRecording,
|
||||
enabled: !state.isStartingRecording,
|
||||
size: _recordButtonSize,
|
||||
onTap: () {
|
||||
|
||||
Reference in New Issue
Block a user