343 lines
11 KiB
Dart
343 lines
11 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'dart:math' as math;
|
|
import '../utils/reduce_motion.dart';
|
|
|
|
/// 슬라이드 + 페이드 전환
|
|
class SlidePageRoute<T> extends PageRouteBuilder<T> {
|
|
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<T> extends PageRouteBuilder<T> {
|
|
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<T> extends PageRouteBuilder<T> {
|
|
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<T> extends PageRouteBuilder<T> {
|
|
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<T> extends PageRouteBuilder<T> {
|
|
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<T> extends PageRouteBuilder<T> {
|
|
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<T> extends PageRouteBuilder<T> {
|
|
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,
|
|
}
|