Skip to content

Instantly share code, notes, and snippets.

@aqua30
Created March 21, 2024 18:57
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save aqua30/58bc16b02cf9878ff751230fc9fd21cf to your computer and use it in GitHub Desktop.
Save aqua30/58bc16b02cf9878ff751230fc9fd21cf to your computer and use it in GitHub Desktop.
Card Stacking UI in Flutter with Animation
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;
}
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,
),
)
],
)
),
);
}
}
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