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, | |
) | |
], | |
) | |
], | |
), | |
), | |
), | |
), | |
); | |
} | |
} |
@aqua30 thank you for your reply. I'll check the options and come back to you in case I need assistance. Thank yo very much
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@tOlliDev Hey! Sorry for delayed response.
Though I replicated this from a fintech app and for a defined set of cards but you can extend it to multiple cards with some math calculations for each card.
But for multiple cards, I would suggest to go with ListView or ScrollView. I don't have the code for it yet but you can try and let me know if you need help in code.