优化交互体验增加动画效果
This commit is contained in:
@@ -8,6 +8,8 @@ import 'package:recording_tool/gen/assets.gen.dart';
|
||||
class RecordDialog extends StatelessWidget {
|
||||
const RecordDialog({super.key, required this.title, required this.actions});
|
||||
|
||||
static const _transitionDuration = Duration(milliseconds: 280);
|
||||
|
||||
final String title;
|
||||
final List<RecordDialogAction> actions;
|
||||
|
||||
@@ -18,8 +20,8 @@ class RecordDialog extends StatelessWidget {
|
||||
VoidCallback? onPressed,
|
||||
bool barrierDismissible = true,
|
||||
}) {
|
||||
return showDialog<void>(
|
||||
context: context,
|
||||
return _present(
|
||||
context,
|
||||
barrierDismissible: barrierDismissible,
|
||||
builder: (dialogContext) {
|
||||
return RecordDialog(
|
||||
@@ -47,8 +49,8 @@ class RecordDialog extends StatelessWidget {
|
||||
VoidCallback? onRightPressed,
|
||||
bool barrierDismissible = false,
|
||||
}) {
|
||||
return showDialog<void>(
|
||||
context: context,
|
||||
return _present(
|
||||
context,
|
||||
barrierDismissible: barrierDismissible,
|
||||
builder: (dialogContext) {
|
||||
return RecordDialog(
|
||||
@@ -74,6 +76,51 @@ class RecordDialog extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
static Future<void> _present(
|
||||
BuildContext context, {
|
||||
required Widget Function(BuildContext dialogContext) builder,
|
||||
required bool barrierDismissible,
|
||||
}) {
|
||||
return showGeneralDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: barrierDismissible,
|
||||
barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel,
|
||||
barrierColor: Colors.black54,
|
||||
transitionDuration: _transitionDuration,
|
||||
pageBuilder: (dialogContext, animation, secondaryAnimation) {
|
||||
return builder(dialogContext);
|
||||
},
|
||||
transitionBuilder: _buildTransition,
|
||||
);
|
||||
}
|
||||
|
||||
static Widget _buildTransition(
|
||||
BuildContext context,
|
||||
Animation<double> animation,
|
||||
Animation<double> secondaryAnimation,
|
||||
Widget child,
|
||||
) {
|
||||
final curved = CurvedAnimation(
|
||||
parent: animation,
|
||||
curve: Curves.easeOutCubic,
|
||||
reverseCurve: Curves.easeInCubic,
|
||||
);
|
||||
|
||||
return FadeTransition(
|
||||
opacity: curved,
|
||||
child: SlideTransition(
|
||||
position: Tween<Offset>(
|
||||
begin: const Offset(0, 0.08),
|
||||
end: Offset.zero,
|
||||
).animate(curved),
|
||||
child: ScaleTransition(
|
||||
scale: Tween<double>(begin: 0.92, end: 1).animate(curved),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final actionWidgets = actions
|
||||
|
||||
@@ -263,7 +263,8 @@ class _RecordingPageState extends ConsumerState<RecordingPage> {
|
||||
if (!state.isPreviewReady && state.errorMessage == null)
|
||||
const RecordingLoadingOverlayWidget(message: '正在启动相机…'),
|
||||
const RecordTimerWidget(),
|
||||
RecordingHudWidget(
|
||||
RepaintBoundary(
|
||||
child: RecordingHudWidget(
|
||||
state: state,
|
||||
showClipboardHint: showClipboardInfo,
|
||||
clipboardAddress: clipboard.address.trim(),
|
||||
@@ -275,7 +276,9 @@ class _RecordingPageState extends ConsumerState<RecordingPage> {
|
||||
.read(recordingViewModelProvider)
|
||||
.session;
|
||||
if (latest.gallerySaveFailed) {
|
||||
AppToast.show(latest.errorMessage ?? '保存到相册失败,请开启相册权限');
|
||||
AppToast.show(
|
||||
latest.errorMessage ?? '保存到相册失败,请开启相册权限',
|
||||
);
|
||||
return;
|
||||
}
|
||||
await _showRecordingSavedDialogIfNeeded();
|
||||
@@ -292,6 +295,7 @@ class _RecordingPageState extends ConsumerState<RecordingPage> {
|
||||
viewModel.setTouchLocked(!state.isTouchLocked);
|
||||
},
|
||||
),
|
||||
),
|
||||
if (state.isTouchLocked && state.isRecording)
|
||||
RecordingTouchLockOverlayWidget(
|
||||
enabled: true,
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// 录制页内容切换时的统一过渡动画。
|
||||
class RecordContentTransition {
|
||||
RecordContentTransition._();
|
||||
|
||||
static const duration = Duration(milliseconds: 600);
|
||||
|
||||
static Widget builder(Widget child, Animation<double> animation) {
|
||||
final curved = CurvedAnimation(
|
||||
parent: animation,
|
||||
curve: Curves.easeOutCubic,
|
||||
reverseCurve: Curves.easeInCubic,
|
||||
);
|
||||
|
||||
return FadeTransition(
|
||||
opacity: curved,
|
||||
child: SlideTransition(
|
||||
position: Tween<Offset>(
|
||||
begin: const Offset(0, 0.12),
|
||||
end: Offset.zero,
|
||||
).animate(curved),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static Widget stackLayoutBuilder(
|
||||
Widget? currentChild,
|
||||
List<Widget> previousChildren,
|
||||
) {
|
||||
return Stack(
|
||||
alignment: Alignment.center,
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
...previousChildren,
|
||||
if (currentChild != null) currentChild,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
static Widget bottomStackLayoutBuilder(
|
||||
Widget? currentChild,
|
||||
List<Widget> previousChildren,
|
||||
) {
|
||||
return Stack(
|
||||
alignment: Alignment.bottomLeft,
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
...previousChildren,
|
||||
if (currentChild != null) currentChild,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:recording_tool/core/utils/date_time_formatter.dart';
|
||||
import 'package:recording_tool/features/recording/widgets/record_content_transition.dart';
|
||||
|
||||
/// 左下角实时时钟与剪贴板地址
|
||||
class ClipboardAddressClockChipWidget extends StatefulWidget {
|
||||
@@ -48,14 +49,32 @@ class _ClipboardAddressClockChipWidgetState
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
return AnimatedSize(
|
||||
duration: RecordContentTransition.duration,
|
||||
curve: Curves.easeOutCubic,
|
||||
alignment: Alignment.topLeft,
|
||||
clipBehavior: Clip.none,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(_nowText, style: _textStyle),
|
||||
if (widget.address.isNotEmpty)
|
||||
Text(widget.address, style: _textStyle),
|
||||
AnimatedSwitcher(
|
||||
duration: RecordContentTransition.duration,
|
||||
switchInCurve: Curves.easeOutCubic,
|
||||
switchOutCurve: Curves.easeInCubic,
|
||||
layoutBuilder: RecordContentTransition.bottomStackLayoutBuilder,
|
||||
transitionBuilder: RecordContentTransition.builder,
|
||||
child: widget.address.isNotEmpty
|
||||
? Text(
|
||||
widget.address,
|
||||
key: ValueKey(widget.address),
|
||||
style: _textStyle,
|
||||
)
|
||||
: const SizedBox.shrink(key: ValueKey('clipboard-address-empty')),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:recording_tool/features/recording/widgets/record_content_transition.dart';
|
||||
import 'package:recording_tool/gen/assets.gen.dart';
|
||||
import 'package:recording_tool/shared/widgets/app_toast.dart';
|
||||
|
||||
@@ -27,6 +28,27 @@ class RecordHeaderWidget extends StatelessWidget {
|
||||
|
||||
bool get _showEventTitle => hasValidClipboardInfo;
|
||||
|
||||
Widget _buildHeaderContent() {
|
||||
if (_showEventTitle) {
|
||||
return _HeaderEventTitleRow(
|
||||
key: ValueKey('title-${eventTitle ?? ''}'),
|
||||
title: eventTitle ?? '',
|
||||
isRecording: isRecording,
|
||||
onClearEventInfo: onClearEventInfo,
|
||||
);
|
||||
}
|
||||
|
||||
if (_showPasteButtons) {
|
||||
return _HeaderPasteActions(
|
||||
key: const ValueKey('paste-actions'),
|
||||
onMockCopy: _mockCopyEventInfo,
|
||||
onPasteEventInfo: onPasteEventInfo,
|
||||
);
|
||||
}
|
||||
|
||||
return const SizedBox.shrink(key: ValueKey('header-empty'));
|
||||
}
|
||||
|
||||
void _mockCopyEventInfo() {
|
||||
const strTemp =
|
||||
'{"title":"蔡依婷vs夏志豪 空中格斗赛 初中组","address":"黑龙江省鹤岗市11111","filename":"蔡依婷_夏志豪_测试循环赛-7_空中格斗赛"}';
|
||||
@@ -52,18 +74,14 @@ class RecordHeaderWidget extends StatelessWidget {
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
Expanded(
|
||||
child: _showEventTitle
|
||||
? _HeaderEventTitleRow(
|
||||
title: eventTitle ?? '',
|
||||
isRecording: isRecording,
|
||||
onClearEventInfo: onClearEventInfo,
|
||||
)
|
||||
: _showPasteButtons
|
||||
? _HeaderPasteActions(
|
||||
onMockCopy: _mockCopyEventInfo,
|
||||
onPasteEventInfo: onPasteEventInfo,
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
child: AnimatedSwitcher(
|
||||
duration: RecordContentTransition.duration,
|
||||
switchInCurve: Curves.easeOutCubic,
|
||||
switchOutCurve: Curves.easeInCubic,
|
||||
layoutBuilder: RecordContentTransition.stackLayoutBuilder,
|
||||
transitionBuilder: RecordContentTransition.builder,
|
||||
child: _buildHeaderContent(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -75,6 +93,7 @@ class RecordHeaderWidget extends StatelessWidget {
|
||||
|
||||
class _HeaderEventTitleRow extends StatelessWidget {
|
||||
const _HeaderEventTitleRow({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.isRecording,
|
||||
required this.onClearEventInfo,
|
||||
@@ -95,8 +114,14 @@ class _HeaderEventTitleRow extends StatelessWidget {
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: AnimatedSwitcher(
|
||||
duration: RecordContentTransition.duration,
|
||||
switchInCurve: Curves.easeOutCubic,
|
||||
switchOutCurve: Curves.easeInCubic,
|
||||
transitionBuilder: RecordContentTransition.builder,
|
||||
child: Text(
|
||||
title,
|
||||
key: ValueKey(title),
|
||||
style: _overlayTextStyle.copyWith(
|
||||
fontSize: 12.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
@@ -106,8 +131,15 @@ class _HeaderEventTitleRow extends StatelessWidget {
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
if (!isRecording)
|
||||
IconButton(
|
||||
),
|
||||
AnimatedSwitcher(
|
||||
duration: RecordContentTransition.duration,
|
||||
switchInCurve: Curves.easeOutCubic,
|
||||
switchOutCurve: Curves.easeInCubic,
|
||||
transitionBuilder: RecordContentTransition.builder,
|
||||
child: !isRecording
|
||||
? IconButton(
|
||||
key: const ValueKey('clear-event-info'),
|
||||
onPressed: onClearEventInfo,
|
||||
icon: Assets.images.imageDelete.image(
|
||||
width: 15.r,
|
||||
@@ -118,6 +150,8 @@ class _HeaderEventTitleRow extends StatelessWidget {
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: BoxConstraints(minWidth: 40.r, minHeight: 40.r),
|
||||
tooltip: '删除',
|
||||
)
|
||||
: const SizedBox.shrink(key: ValueKey('clear-event-info-hidden')),
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -126,6 +160,7 @@ class _HeaderEventTitleRow extends StatelessWidget {
|
||||
|
||||
class _HeaderPasteActions extends StatelessWidget {
|
||||
const _HeaderPasteActions({
|
||||
super.key,
|
||||
required this.onMockCopy,
|
||||
required this.onPasteEventInfo,
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
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/record_content_transition.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';
|
||||
@@ -84,11 +85,22 @@ class RecordingHudWidget extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
if (showClipboardHint)
|
||||
Positioned(
|
||||
left: _overlayInfoLeft,
|
||||
bottom: _overlayInfoBottom,
|
||||
child: ClipboardAddressClockChipWidget(address: clipboardAddress),
|
||||
child: AnimatedSwitcher(
|
||||
duration: RecordContentTransition.duration,
|
||||
switchInCurve: Curves.easeOutCubic,
|
||||
switchOutCurve: Curves.easeInCubic,
|
||||
layoutBuilder: RecordContentTransition.bottomStackLayoutBuilder,
|
||||
transitionBuilder: RecordContentTransition.builder,
|
||||
child: showClipboardHint
|
||||
? ClipboardAddressClockChipWidget(
|
||||
key: const ValueKey('clipboard-info'),
|
||||
address: clipboardAddress,
|
||||
)
|
||||
: const SizedBox.shrink(key: ValueKey('clipboard-info-hidden')),
|
||||
),
|
||||
),
|
||||
if (state.isRecording)
|
||||
Positioned(
|
||||
|
||||
Reference in New Issue
Block a user