- 清理大量废弃的 barrel 导出文件,移除冗余的中间导出层 - 修复所有相对路径导入错误,统一调整为扁平化模块引用 - 更新多平台 pubspec 版本号与依赖库版本 - 补充后端功能问题管理后台与脚本工具 - 调整部分页面的快捷方式文案适配新功能 - 更新部分翻译覆盖率与API文档
285 lines
9.5 KiB
Dart
285 lines
9.5 KiB
Dart
/// ============================================================
|
||
/// 闲言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),
|
||
),
|
||
);
|
||
}
|