Files
xianyan/lib/core/utils/ui/page_transitions.dart
Developer f91be94e9c refactor: 完成项目架构重构,统一模块导入路径
- 清理大量废弃的 barrel 导出文件,移除冗余的中间导出层
- 修复所有相对路径导入错误,统一调整为扁平化模块引用
- 更新多平台 pubspec 版本号与依赖库版本
- 补充后端功能问题管理后台与脚本工具
- 调整部分页面的快捷方式文案适配新功能
- 更新部分翻译覆盖率与API文档
2026-06-12 08:53:57 +08:00

285 lines
9.5 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/// ============================================================
/// 闲言APP — iOS 风格页面转场动画
/// 创建时间: 2026-04-20
/// 更新时间: 2026-06-12
// 作用: 统一页面切换动画,集成 heroine + stupid_simple_sheet
// 上次更新: 修复SnapshotWidget在paint阶段触发markNeedsPaint断言错误
/// ============================================================
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:stupid_simple_sheet/stupid_simple_sheet.dart';
/// iOS 风格右滑入转场 PageBuilder (CupertinoPage 原生侧滑返回)
///
/// 使用 CupertinoPage 替代 CustomTransitionPage
/// 原生支持 iOS 侧滑返回手势swipe-back gesture
/// 转场动画与系统一致:从右侧滑入 + 侧滑返回。
CupertinoPage<void> iosSlideTransition({
required GoRouterState state,
required Widget child,
}) {
return CupertinoPage<void>(
key: state.pageKey,
child: child,
);
}
/// iOS 风格底部弹起转场 PageBuilder (Modal)
CustomTransitionPage<void> iosModalTransition({
required GoRouterState state,
required Widget child,
}) {
return CustomTransitionPage<void>(
key: state.pageKey,
child: child,
barrierDismissible: true,
barrierColor: Colors.black54,
opaque: false,
transitionDuration: const Duration(milliseconds: 350),
reverseTransitionDuration: const Duration(milliseconds: 350),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
final slideAnimation =
Tween<Offset>(
begin: const Offset(0.0, 1.0),
end: Offset.zero,
).animate(
CurvedAnimation(
parent: animation,
curve: Curves.easeOutCubic,
reverseCurve: Curves.easeInCubic,
),
);
return SlideTransition(position: slideAnimation, child: child);
},
);
}
/// 液态玻璃底部面板转场 (StupidSimpleGlassSheet)
///
/// iOS 26 风格液态玻璃 Sheet支持拖拽关闭 + 弹簧动画 + 背景模糊。
/// 适用于会员页、设置面板等 Modal 场景。
CustomTransitionPage<void> glassSheetTransition({
required GoRouterState state,
required Widget child,
SheetSnappingConfig snappingConfig = SheetSnappingConfig.full,
bool blurBehindBarrier = true,
}) {
return CustomTransitionPage<void>(
key: state.pageKey,
child: child,
opaque: false,
barrierDismissible: true,
barrierColor: const Color.from(alpha: 0.15, red: 0, green: 0, blue: 0),
transitionDuration: const Duration(milliseconds: 350),
reverseTransitionDuration: const Duration(milliseconds: 350),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return AnimatedBuilder(
animation: animation,
builder: (context, _) {
final value = animation.value;
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisAlignment: MainAxisAlignment.end,
children: [
Flexible(
child: FractionalTranslation(
translation: Offset(0, 1 - value),
child: ClipRRect(
borderRadius: const BorderRadius.vertical(
top: Radius.circular(36),
),
child: child,
),
),
),
],
);
},
);
},
);
}
/// iOS 风格淡入淡出转场
CustomTransitionPage<void> iosFadeTransition({
required GoRouterState state,
required Widget child,
}) {
return CustomTransitionPage<void>(
key: state.pageKey,
child: child,
transitionDuration: const Duration(milliseconds: 250),
reverseTransitionDuration: const Duration(milliseconds: 200),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
final fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(parent: animation, curve: Curves.easeOut));
final scaleAnimation = Tween<double>(
begin: 0.96,
end: 1.0,
).animate(CurvedAnimation(parent: animation, curve: Curves.easeOut));
return FadeTransition(
opacity: fadeAnimation,
child: ScaleTransition(scale: scaleAnimation, child: child),
);
},
);
}
/// Heroine 共享元素转场 PageBuilder
///
/// 使用 Heroine 弹簧动画替代 Hero支持 FadeShuttleBuilder。
/// 适用于有共享元素(如句子卡片→编辑器)的页面转场。
CustomTransitionPage<void> heroineTransition({
required GoRouterState state,
required Widget child,
}) {
return CustomTransitionPage<void>(
key: state.pageKey,
child: child,
transitionDuration: const Duration(milliseconds: 500),
reverseTransitionDuration: const Duration(milliseconds: 500),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return AnimatedBuilder(
animation: animation,
builder: (context, _) {
final value = Curves.easeOutCubic.transform(animation.value);
return Opacity(
opacity: value,
child: Transform.translate(
offset: Offset(0, 20 * (1 - value)),
child: child,
),
);
},
);
},
);
}
/// 命令式显示液态玻璃底部面板
///
/// 使用 _ScaledGlassSheetRoute 实现真正的 iOS 26 液态玻璃 Sheet
/// 支持弹簧动画、拖拽关闭、背景模糊、多级吸附、下层页面缩放效果。
Future<T?> showGlassSheet<T>({
required BuildContext context,
required WidgetBuilder builder,
SheetSnappingConfig snappingConfig = SheetSnappingConfig.full,
bool blurBehindBarrier = true,
bool barrierDismissible = true,
bool useRootNavigator = true,
Color barrierColor = Colors.black54,
Color backgroundColor = CupertinoColors.systemBackground,
ShapeBorder? shape,
}) {
return Navigator.of(context, rootNavigator: useRootNavigator).push<T>(
_ScaledGlassSheetRoute<T>(
blurBehindBarrier: blurBehindBarrier,
snappingConfig: snappingConfig,
barrierColor: barrierColor,
backgroundColor: backgroundColor,
shape: shape ?? StupidSimpleGlassSheetRoute.glassShape,
child: Builder(builder: builder),
),
);
}
/// 带缩放效果的液态玻璃底部面板路由
///
/// 继承 StupidSimpleGlassSheetRoute重写 delegatedTransition
/// 在 Sheet 弹出时对下层页面应用缩放 + 下移 + 圆角 + 暗化效果,
/// 还原 iOS 原生 Sheet 弹出时背景页面被"压制"的视觉体验。
///
/// 注意: 使用 RouteSnapshotMode.never 禁用 SnapshotWidget 快照,
/// 避免包内部 _updateSnapshotState() 在 paint 阶段修改
/// allowSnapshotting 触发 markNeedsPaint 断言错误。
/// 底层页面通过 RepaintBoundary 隔离动画重绘,性能影响可忽略。
class _ScaledGlassSheetRoute<T> extends StupidSimpleGlassSheetRoute<T> {
_ScaledGlassSheetRoute({
required super.child,
super.settings,
super.motion,
super.clearBarrierImmediately,
super.backgroundColor,
super.callNavigatorUserGestureMethods,
super.snappingConfig,
super.draggable,
super.originateAboveBottomViewInset,
super.shape,
super.blurBehindBarrier,
Color barrierColor =
const Color.from(alpha: 0.15, red: 0, green: 0, blue: 0),
}) : super(
backgroundSnapshotMode: RouteSnapshotMode.never,
barrierColor: barrierColor,
);
static const double _scaleFactor = 0.0835;
static const double _slideFraction = 0.05;
@override
DelegatedTransitionBuilder? get delegatedTransition =>
(context, animation, secondaryAnimation, canSnapshot, child) {
return AnimatedBuilder(
animation: secondaryAnimation,
builder: (context, snapshotChild) {
final t = Curves.easeOutCubic.transform(
secondaryAnimation.value.clamp(0.0, 1.0),
);
final scale = 1.0 - _scaleFactor * t;
final slideOffset =
_slideFraction * t * MediaQuery.sizeOf(context).height;
final cornerRadius = 36.0 * t;
final overlayOpacity = 0.1 * t;
return Transform.translate(
offset: Offset(0, slideOffset),
child: Transform.scale(
scale: scale,
alignment: Alignment.topCenter,
filterQuality: FilterQuality.medium,
child: ClipRRect(
borderRadius: BorderRadius.circular(cornerRadius),
child: Stack(
children: [
snapshotChild!,
IgnorePointer(
child: Opacity(
opacity: overlayOpacity,
child: const DecoratedBox(
decoration: BoxDecoration(color: Color(0xFF000000)),
child: SizedBox.expand(),
),
),
),
],
),
),
),
);
},
child: child,
);
};
}
/// 命令式显示 iOS 18 风格底部面板
Future<T?> showCupertinoSheet<T>({
required BuildContext context,
required WidgetBuilder builder,
SheetSnappingConfig snappingConfig = SheetSnappingConfig.full,
}) {
return Navigator.of(context).push<T>(
StupidSimpleCupertinoSheetRoute<T>(
snappingConfig: snappingConfig,
child: Builder(builder: builder),
),
);
}