Hero 애니메이션과 페이지 전환 — CustomPageRoute
Hero 애니메이션과 페이지 전환 — CustomPageRoute
화면 전환 시 특정 위젯이 자연스럽게 이동하는 Hero 애니메이션은 사용자 경험을 크게 향상시킵니다. 인스타그램의 사진 확대, 쇼핑몰의 상품 상세 진입 등에서 자주 볼 수 있는 패턴입니다.
Hero 애니메이션 기본
같은 tag를 가진 Hero 위젯이 두 화면에 있으면, 화면 전환 시 자동으로 이동 애니메이션이 적용됩니다.
// 목록 화면
class ProductListScreen extends StatelessWidget {
const ProductListScreen({super.key});
@override
Widget build(BuildContext context) {
return GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
),
itemCount: products.length,
itemBuilder: (context, index) {
final product = products[index];
return GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => ProductDetailScreen(product: product),
),
);
},
child: Hero(
// 고유한 tag가 핵심!
tag: 'product-image-${product.id}',
child: Image.network(
product.imageUrl,
fit: BoxFit.cover,
),
),
);
},
);
}
}
// 상세 화면
class ProductDetailScreen extends StatelessWidget {
final Product product;
const ProductDetailScreen({super.key, required this.product});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
// 같은 tag의 Hero 위젯
Hero(
tag: 'product-image-${product.id}',
child: Image.network(
product.imageUrl,
width: double.infinity,
height: 300,
fit: BoxFit.cover,
),
),
Padding(
padding: const EdgeInsets.all(16),
child: Text(
product.name,
style: const TextStyle(fontSize: 24),
),
),
],
),
);
}
}
Hero의 주의사항
tag는 화면 내에서 고유 해야 합니다 (중복 tag는 에러)- Hero 위젯은 같은 타입의 자식을 가져야 자연스럽습니다
- 텍스트 Hero는
Material위젯으로 감싸면 전환 시 깜빡임을 방지할 수 있습니다
// 텍스트 Hero
Hero(
tag: 'product-title-${product.id}',
child: Material(
color: Colors.transparent,
child: Text(
product.name,
style: const TextStyle(fontSize: 24),
),
),
)
CustomPageRoute — 커스텀 전환 효과
페이드 전환
class FadePageRoute<T> extends PageRouteBuilder<T> {
final Widget page;
FadePageRoute({required this.page})
: super(
pageBuilder: (context, animation, secondaryAnimation) => page,
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeTransition(
opacity: animation,
child: child,
);
},
transitionDuration: const Duration(milliseconds: 300),
);
}
// 사용
Navigator.push(
context,
FadePageRoute(page: const DetailScreen()),
);
슬라이드 전환
class SlidePageRoute<T> extends PageRouteBuilder<T> {
final Widget page;
SlidePageRoute({required this.page})
: super(
pageBuilder: (context, animation, secondaryAnimation) => page,
transitionsBuilder: (context, animation, secondaryAnimation, child) {
// 오른쪽에서 슬라이드
final offsetAnimation = Tween<Offset>(
begin: const Offset(1.0, 0.0),
end: Offset.zero,
).animate(CurvedAnimation(
parent: animation,
curve: Curves.easeInOut,
));
return SlideTransition(
position: offsetAnimation,
child: child,
);
},
);
}
스케일 + 페이드 조합
class ScaleFadePageRoute<T> extends PageRouteBuilder<T> {
final Widget page;
ScaleFadePageRoute({required this.page})
: super(
pageBuilder: (context, animation, secondaryAnimation) => page,
transitionsBuilder: (context, animation, secondaryAnimation, child) {
final curved = CurvedAnimation(
parent: animation,
curve: Curves.easeOutCubic,
);
return FadeTransition(
opacity: curved,
child: ScaleTransition(
scale: Tween(begin: 0.8, end: 1.0).animate(curved),
child: child,
),
);
},
transitionDuration: const Duration(milliseconds: 400),
);
}
아래에서 위로 슬라이드 (바텀시트 스타일)
class BottomSlideRoute<T> extends PageRouteBuilder<T> {
final Widget page;
BottomSlideRoute({required this.page})
: super(
opaque: false,
barrierColor: Colors.black54,
pageBuilder: (context, animation, secondaryAnimation) => page,
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, 1),
end: Offset.zero,
).animate(CurvedAnimation(
parent: animation,
curve: Curves.easeOut,
)),
child: child,
);
},
);
}
GoRouter에서 커스텀 전환
GoRoute(
path: '/detail/:id',
pageBuilder: (context, state) {
return CustomTransitionPage(
key: state.pageKey,
child: DetailScreen(id: state.pathParameters['id']!),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeTransition(
opacity: CurvedAnimation(
parent: animation,
curve: Curves.easeInOut,
),
child: child,
);
},
);
},
)
전환 애니메이션 선택 가이드
| 전환 효과 | 적합한 상황 |
|---|---|
| 오른쪽 슬라이드 | 일반적인 화면 이동 (기본) |
| 페이드 | 같은 레벨의 화면 전환 |
| 스케일 | 모달, 팝업 느낌 |
| 아래에서 위로 | 바텀시트, 모달 화면 |
| Hero | 요소 간 연결감 강조 |
정리
- Hero 애니메이션은 같은
tag만 지정하면 자동으로 동작합니다 tag는 화면 내에서 고유해야 하며, 보통 ID를 포함시킵니다PageRouteBuilder로 페이드, 슬라이드, 스케일 등 커스텀 전환을 구현합니다- 전환 효과는 UX 맥락에 맞게 선택하세요
- GoRouter에서는
CustomTransitionPage를 사용합니다
댓글 로딩 중...