import 'package:flutter/material.dart'; import 'dart:math' as math; import '../utils/reduce_motion.dart'; /// 슬라이드 + 페이드 전환 class SlidePageRoute extends PageRouteBuilder { final Widget page; final AxisDirection direction; SlidePageRoute({ required this.page, this.direction = AxisDirection.right, }) : super( pageBuilder: (context, animation, secondaryAnimation) => page, transitionDuration: ReduceMotion.platform() ? Duration.zero : const Duration(milliseconds: 300), reverseTransitionDuration: ReduceMotion.platform() ? Duration.zero : const Duration(milliseconds: 300), transitionsBuilder: (context, animation, secondaryAnimation, child) { Offset begin; switch (direction) { case AxisDirection.right: begin = const Offset(-1.0, 0.0); break; case AxisDirection.left: begin = const Offset(1.0, 0.0); break; case AxisDirection.up: begin = const Offset(0.0, 1.0); break; case AxisDirection.down: begin = const Offset(0.0, -1.0); break; } const end = Offset.zero; const curve = Curves.easeOutCubic; var tween = Tween(begin: begin, end: end).chain( CurveTween(curve: curve), ); var offsetAnimation = animation.drive(tween); var fadeTween = Tween(begin: 0.0, end: 1.0).chain( CurveTween(curve: curve), ); var fadeAnimation = animation.drive(fadeTween); return SlideTransition( position: offsetAnimation, child: FadeTransition( opacity: fadeAnimation, child: child, ), ); }, ); } /// 스케일 + 페이드 전환 class ScalePageRoute extends PageRouteBuilder { final Widget page; final Alignment alignment; ScalePageRoute({ required this.page, this.alignment = Alignment.center, }) : super( pageBuilder: (context, animation, secondaryAnimation) => page, transitionDuration: ReduceMotion.platform() ? Duration.zero : const Duration(milliseconds: 400), reverseTransitionDuration: ReduceMotion.platform() ? Duration.zero : const Duration(milliseconds: 400), transitionsBuilder: (context, animation, secondaryAnimation, child) { const curve = Curves.elasticOut; var scaleTween = Tween(begin: 0.0, end: 1.0).chain( CurveTween(curve: curve), ); var scaleAnimation = animation.drive(scaleTween); var fadeTween = Tween(begin: 0.0, end: 1.0).chain( CurveTween(curve: Curves.easeIn), ); var fadeAnimation = animation.drive(fadeTween); return ScaleTransition( scale: scaleAnimation, alignment: alignment, child: FadeTransition( opacity: fadeAnimation, child: child, ), ); }, ); } /// 회전 + 스케일 전환 class RotatePageRoute extends PageRouteBuilder { final Widget page; RotatePageRoute({required this.page}) : super( pageBuilder: (context, animation, secondaryAnimation) => page, transitionDuration: ReduceMotion.platform() ? Duration.zero : const Duration(milliseconds: 500), reverseTransitionDuration: ReduceMotion.platform() ? Duration.zero : const Duration(milliseconds: 500), transitionsBuilder: (context, animation, secondaryAnimation, child) { const curve = Curves.easeInOut; var rotateTween = Tween(begin: -0.5, end: 0.0).chain( CurveTween(curve: curve), ); var rotateAnimation = animation.drive(rotateTween); var scaleTween = Tween(begin: 0.0, end: 1.0).chain( CurveTween(curve: curve), ); var scaleAnimation = animation.drive(scaleTween); return Transform( alignment: Alignment.center, transform: Matrix4.identity() ..setEntry(3, 2, 0.001) ..rotateZ(rotateAnimation.value), child: Transform.scale( scale: scaleAnimation.value, child: child, ), ); }, ); } /// 3D 플립 전환 class FlipPageRoute extends PageRouteBuilder { final Widget page; final bool horizontal; FlipPageRoute({ required this.page, this.horizontal = true, }) : super( pageBuilder: (context, animation, secondaryAnimation) => page, transitionDuration: ReduceMotion.platform() ? Duration.zero : const Duration(milliseconds: 800), reverseTransitionDuration: ReduceMotion.platform() ? Duration.zero : const Duration(milliseconds: 800), transitionsBuilder: (context, animation, secondaryAnimation, child) { final isAnimatingForward = animation.status == AnimationStatus.forward; final flipAnimation = Tween( begin: 0.0, end: isAnimatingForward ? -math.pi : math.pi, ).animate(CurvedAnimation( parent: animation, curve: Curves.easeInOut, )); return AnimatedBuilder( animation: flipAnimation, builder: (context, child) { final isShowingFront = flipAnimation.value.abs() < math.pi / 2; return Transform( alignment: Alignment.center, transform: Matrix4.identity() ..setEntry(3, 2, 0.001) ..rotateY(horizontal ? flipAnimation.value : 0) ..rotateX(horizontal ? 0 : flipAnimation.value), child: isShowingFront ? Container() : Transform( alignment: Alignment.center, transform: Matrix4.identity() ..rotateY(horizontal ? math.pi : 0) ..rotateX(horizontal ? 0 : math.pi), child: child, ), ); }, child: child, ); }, ); } /// 컨테이너 트랜스폼 (Material Design) class ContainerTransformPageRoute extends PageRouteBuilder { final Widget page; final Widget startWidget; final BorderRadius? borderRadius; ContainerTransformPageRoute({ required this.page, required this.startWidget, this.borderRadius, }) : super( pageBuilder: (context, animation, secondaryAnimation) => page, transitionDuration: ReduceMotion.platform() ? Duration.zero : const Duration(milliseconds: 500), reverseTransitionDuration: ReduceMotion.platform() ? Duration.zero : const Duration(milliseconds: 500), transitionsBuilder: (context, animation, secondaryAnimation, child) { return Stack( children: [ // 배경 페이드 FadeTransition( opacity: animation, child: Container( color: Theme.of(context) .colorScheme .scrim .withValues(alpha: 0.3), ), ), // 컨테이너 확장 애니메이션 AnimatedBuilder( animation: animation, builder: (context, _) { final progress = animation.value; final scale = 0.5 + (0.5 * progress); final radius = borderRadius?.topLeft.x ?? 0; final currentRadius = radius * (1 - progress); return Transform.scale( scale: scale, child: ClipRRect( borderRadius: BorderRadius.circular(currentRadius), child: progress < 0.5 ? startWidget : child, ), ); }, child: child, ), ], ); }, ); } /// 커스텀 Hero 애니메이션 class CustomHeroPageRoute extends PageRouteBuilder { final Widget page; final String heroTag; CustomHeroPageRoute({ required this.page, required this.heroTag, }) : super( pageBuilder: (context, animation, secondaryAnimation) => page, transitionDuration: const Duration(milliseconds: 500), reverseTransitionDuration: const Duration(milliseconds: 500), transitionsBuilder: (context, animation, secondaryAnimation, child) { return FadeTransition( opacity: CurvedAnimation( parent: animation, curve: const Interval(0.5, 1.0), ), child: child, ); }, ); } /// 공유 축 전환 (Material Design) class SharedAxisPageRoute extends PageRouteBuilder { final Widget page; final SharedAxisTransitionType transitionType; SharedAxisPageRoute({ required this.page, required this.transitionType, }) : super( pageBuilder: (context, animation, secondaryAnimation) => page, transitionDuration: ReduceMotion.platform() ? Duration.zero : const Duration(milliseconds: 300), reverseTransitionDuration: ReduceMotion.platform() ? Duration.zero : const Duration(milliseconds: 300), transitionsBuilder: (context, animation, secondaryAnimation, child) { late final Offset begin; late final Offset end; switch (transitionType) { case SharedAxisTransitionType.horizontal: begin = const Offset(1.0, 0.0); end = Offset.zero; break; case SharedAxisTransitionType.vertical: begin = const Offset(0.0, 1.0); end = Offset.zero; break; case SharedAxisTransitionType.scaled: begin = Offset.zero; end = Offset.zero; break; } final slideTween = Tween(begin: begin, end: end); final fadeTween = Tween(begin: 0.0, end: 1.0); final scaleTween = transitionType == SharedAxisTransitionType.scaled ? Tween(begin: 0.8, end: 1.0) : Tween(begin: 1.0, end: 1.0); final slideAnimation = animation.drive(slideTween); final fadeAnimation = animation.drive(fadeTween); final scaleAnimation = animation.drive(scaleTween); return SlideTransition( position: slideAnimation, child: FadeTransition( opacity: fadeAnimation, child: ScaleTransition( scale: scaleAnimation, child: child, ), ), ); }, ); } enum SharedAxisTransitionType { horizontal, vertical, scaled, }