Created
March 21, 2024 18:57
-
-
Save aqua30/58bc16b02cf9878ff751230fc9fd21cf to your computer and use it in GitHub Desktop.
Card Stacking UI in Flutter with Animation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'package:flutter/material.dart'; | |
import 'package:ui_animations/cards/expanded_card.dart'; | |
import 'cards/summary_card.dart'; | |
class CardStackAnimation extends StatefulWidget { | |
const CardStackAnimation({super.key}); | |
@override | |
State<StatefulWidget> createState() => _CardStackAnimation(); | |
} | |
double topOffset = 100; | |
double expandedCardTop = topOffset; | |
double expandedHeight = 360; | |
double card1Top = expandedCardTop; | |
double card2Top = expandedCardTop + 60; | |
double card3Top = expandedCardTop + expandedHeight; | |
bool animateHeight = false; | |
int selectedCard = 1; | |
class _CardStackAnimation extends State<CardStackAnimation> { | |
final double sideMargin = 16; | |
@override | |
Widget build(BuildContext context) { | |
return Container( | |
color: const Color(0XFFf6f5ff), | |
alignment: Alignment.topCenter, | |
child: Stack( | |
children: [ | |
/** | |
* Summary card 1 | |
* */ | |
Positioned( | |
top: expandedCardTop.forFirstStackCard, | |
left: sideMargin, | |
right: sideMargin, | |
child: SummaryCard( | |
leadingIcon: Icons.food_bank, | |
title: 'Savings PRO', | |
subTitle: 'Tap to see more', | |
onClick: () { | |
_update(1, topOffset); | |
}, | |
) | |
), | |
/** | |
* Summary card 3 | |
* */ | |
Positioned( | |
top: 460, | |
left: sideMargin, | |
right: sideMargin, | |
child: SummaryCard( | |
alignment: Alignment.bottomCenter, | |
leadingIcon: Icons.attach_money, | |
title: 'Loans', | |
subTitle: 'Get up to 5 Lakhs', | |
onClick: () { | |
_update(3, 220); | |
}, | |
) | |
), | |
/** | |
* Summary card 2 | |
* */ | |
Positioned( | |
top: expandedCardTop.forSecondStackCard, | |
left: sideMargin, | |
right: sideMargin, | |
child: SummaryCard( | |
alignment: expandedCardTop == 220 ? Alignment.topCenter : Alignment.bottomCenter, | |
leadingIcon: Icons.credit_card, | |
title: 'Credit Card', | |
subTitle: 'Limit upto 3 Lakhs', | |
onClick: () { | |
_update(2, 160); | |
}, | |
) | |
), | |
/** | |
* Expanded card | |
* */ | |
AnimatedPositioned( | |
duration: Duration(milliseconds: animateHeight ? 10 : 250), | |
onEnd: () { | |
setState(() { | |
animateHeight = false; | |
}); | |
}, | |
curve: Curves.fastEaseInToSlowEaseOut, | |
height: 360, | |
left: sideMargin, | |
right: sideMargin, | |
top: animateHeight ? (expandedCardTop + 20) : expandedCardTop, | |
child: ExpandedCard( | |
title: _majorCardTitle, | |
icon: _majorCardIcon, | |
), | |
), | |
], | |
) | |
); | |
} | |
String get _majorCardTitle { | |
return selectedCard == 1 | |
? 'Savings PRO' | |
: (selectedCard == 2 ? 'Credit Card' : 'Loans'); | |
} | |
IconData get _majorCardIcon { | |
return selectedCard == 1 | |
? Icons.food_bank | |
: (selectedCard == 2 ? Icons.credit_card : Icons.attach_money); | |
} | |
_update(int cardIdx, double cardTop) { | |
setState(() { | |
selectedCard = cardIdx; | |
expandedCardTop = cardTop; | |
animateHeight = true; | |
}); | |
} | |
} | |
extension CardTopExt on double { | |
double get forFirstStackCard => this == topOffset ? 280 : topOffset; | |
double get forSecondStackCard => this == 220 ? 160 : 400; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'package:flutter/material.dart'; | |
class ExpandedCard extends StatelessWidget { | |
final IconData icon; | |
final String title; | |
const ExpandedCard({super.key, required this.icon, required this.title}); | |
@override | |
Widget build(BuildContext context) { | |
return Material( | |
color: Colors.transparent, | |
child: Container( | |
clipBehavior: Clip.antiAlias, | |
decoration: BoxDecoration( | |
color: Colors.white, | |
borderRadius: const BorderRadius.all(Radius.circular(20)), | |
boxShadow: [ | |
BoxShadow( | |
offset: const Offset(2, -2), | |
spreadRadius: 4, | |
blurRadius: 10, | |
color: Colors.black45.withAlpha(50)), | |
BoxShadow( | |
offset: const Offset(2, 2), | |
spreadRadius: 4, | |
blurRadius: 10, | |
color: Colors.black45.withAlpha(50)) | |
]), | |
width: 300, | |
height: 360, | |
alignment: Alignment.center, | |
child: Column( | |
crossAxisAlignment: CrossAxisAlignment.start, | |
mainAxisAlignment: MainAxisAlignment.start, | |
children: [ | |
const SizedBox( | |
height: 16 | |
), | |
Padding( | |
padding: const EdgeInsets.all(16.0), | |
child: SizedBox( | |
width: double.infinity, | |
child: Text( | |
title, | |
textAlign: TextAlign.start, | |
style: const TextStyle( | |
fontSize: 24, | |
color: Colors.black45, | |
fontWeight: FontWeight.bold, | |
), | |
), | |
), | |
), | |
Center( | |
child: Icon( | |
icon, | |
color: Colors.red, | |
size: 50, | |
), | |
) | |
], | |
) | |
), | |
); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'package:flutter/material.dart'; | |
class SummaryCard extends StatelessWidget { | |
final VoidCallback onClick; | |
final IconData leadingIcon; | |
final String title; | |
final String subTitle; | |
final Alignment alignment; | |
const SummaryCard( | |
{super.key, | |
required this.onClick, | |
required this.leadingIcon, | |
required this.title, | |
this.alignment = Alignment.topCenter, | |
required this.subTitle}); | |
@override | |
Widget build(BuildContext context) { | |
const textStyle = TextStyle(fontSize: 14, color: Colors.black45); | |
return Material( | |
color: Colors.transparent, | |
child: GestureDetector( | |
onTap: onClick, | |
child: Container( | |
decoration: BoxDecoration( | |
color: Colors.white, | |
borderRadius: const BorderRadius.all(Radius.circular(20)), | |
boxShadow: [ | |
BoxShadow( | |
offset: const Offset(2, -2), | |
spreadRadius: 2, | |
blurRadius: 10, | |
color: Colors.black45.withAlpha(50)), | |
BoxShadow( | |
offset: const Offset(2, 2), | |
spreadRadius: 2, | |
blurRadius: 10, | |
color: Colors.black45.withAlpha(50)) | |
]), | |
height: 120, | |
alignment: alignment, | |
clipBehavior: Clip.hardEdge, | |
child: SizedBox( | |
height: 60, | |
child: Row( | |
mainAxisAlignment: MainAxisAlignment.start, | |
crossAxisAlignment: CrossAxisAlignment.center, | |
children: [ | |
const SizedBox( | |
width: 16, | |
), | |
Icon( | |
leadingIcon, | |
color: Colors.red, | |
), | |
const SizedBox( | |
width: 16, | |
), | |
Column( | |
mainAxisAlignment: MainAxisAlignment.center, | |
crossAxisAlignment: CrossAxisAlignment.start, | |
children: [ | |
Text( | |
title, | |
style: textStyle, | |
), | |
Text( | |
subTitle, | |
style: textStyle, | |
) | |
], | |
) | |
], | |
), | |
), | |
), | |
), | |
); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment