animated_date_header 1.0.1
animated_date_header: ^1.0.1 copied to clipboard
A Flutter widget that animates day number and month name transitions with blur, slide, scale, and bounce effects. Just pass a DateTime — the widget handles per-letter staggered animations automatically.
import 'package:flutter/material.dart';
import 'package:animated_date_header/animated_date_header.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Animated Date Header — Examples',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const ExampleHome(),
);
}
}
class ExampleHome extends StatelessWidget {
const ExampleHome({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Animated Date Header')),
body: ListView(
children: [
_tile(
context,
title: 'Basic Usage',
subtitle: 'Minimal setup — just pass a date',
page: const BasicExample(),
),
_tile(
context,
title: 'Custom Styles',
subtitle: 'Large colorful header with custom fonts',
page: const StyledExample(),
),
_tile(
context,
title: 'Full App Bar',
subtitle: 'Date header with navigation & action buttons',
page: const AppBarExample(),
),
_tile(
context,
title: 'Standalone AnimatedDigit',
subtitle: 'Use AnimatedDigit on its own for any text',
page: const DigitExample(),
),
],
),
);
}
Widget _tile(
BuildContext context, {
required String title,
required String subtitle,
required Widget page,
}) {
return ListTile(
title: Text(title),
subtitle: Text(subtitle),
trailing: const Icon(Icons.chevron_right),
onTap: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => page),
),
);
}
}
// ---------------------------------------------------------------------------
// Example 1: Basic Usage
// ---------------------------------------------------------------------------
class BasicExample extends StatefulWidget {
const BasicExample({super.key});
@override
State<BasicExample> createState() => _BasicExampleState();
}
class _BasicExampleState extends State<BasicExample> {
DateTime _date = DateTime.now();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Basic Usage')),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
AnimatedDateHeader(date: _date),
const SizedBox(height: 40),
Row(
mainAxisSize: MainAxisSize.min,
children: [
FilledButton.tonal(
onPressed: () => setState(() {
_date = _date.subtract(const Duration(days: 1));
}),
child: const Text('Previous Day'),
),
const SizedBox(width: 12),
FilledButton.tonal(
onPressed: () => setState(() {
_date = _date.add(const Duration(days: 1));
}),
child: const Text('Next Day'),
),
],
),
const SizedBox(height: 12),
Row(
mainAxisSize: MainAxisSize.min,
children: [
FilledButton.tonal(
onPressed: () => setState(() {
_date = DateTime(_date.year, _date.month - 1, _date.day);
}),
child: const Text('Previous Month'),
),
const SizedBox(width: 12),
FilledButton.tonal(
onPressed: () => setState(() {
_date = DateTime(_date.year, _date.month + 1, _date.day);
}),
child: const Text('Next Month'),
),
],
),
],
),
),
);
}
}
// ---------------------------------------------------------------------------
// Example 2: Custom Styles
// ---------------------------------------------------------------------------
class StyledExample extends StatefulWidget {
const StyledExample({super.key});
@override
State<StyledExample> createState() => _StyledExampleState();
}
class _StyledExampleState extends State<StyledExample> {
DateTime _date = DateTime.now();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Custom Styles')),
backgroundColor: const Color(0xFF1A1A2E),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
AnimatedDateHeader(
date: _date,
style: const AnimatedDateHeaderStyle(
dayStyle: TextStyle(
fontSize: 48,
fontWeight: FontWeight.w900,
color: Color(0xFFE94560),
height: 1,
),
monthStyle: TextStyle(
fontSize: 48,
fontWeight: FontWeight.w900,
color: Color(0xFF0F3460),
height: 1,
),
dayDigitHeight: 54,
monthLetterHeight: 54,
dayMonthSpacing: 10,
monthLetterStagger: Duration(milliseconds: 50),
),
),
const SizedBox(height: 40),
AnimatedDateHeader(
date: _date,
style: const AnimatedDateHeaderStyle(
dayStyle: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w300,
color: Colors.white70,
height: 1,
letterSpacing: 2,
),
monthStyle: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w300,
color: Colors.white38,
height: 1,
letterSpacing: 2,
),
dayDigitHeight: 24,
monthLetterHeight: 24,
dayMonthSpacing: 8,
),
),
const SizedBox(height: 48),
Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton.filled(
onPressed: () => setState(() {
_date = _date.subtract(const Duration(days: 1));
}),
icon: const Icon(Icons.remove),
),
const SizedBox(width: 20),
IconButton.filled(
onPressed: () => setState(() {
_date = _date.add(const Duration(days: 1));
}),
icon: const Icon(Icons.add),
),
],
),
],
),
),
);
}
}
// ---------------------------------------------------------------------------
// Example 3: Full App Bar with date list
// ---------------------------------------------------------------------------
class AppBarExample extends StatefulWidget {
const AppBarExample({super.key});
@override
State<AppBarExample> createState() => _AppBarExampleState();
}
class _AppBarExampleState extends State<AppBarExample> {
late DateTime _selectedDate;
late List<DateTime> _allDates;
late ScrollController _scrollController;
static const _dayLetters = ['M', 'T', 'W', 'T', 'F', 'S', 'S'];
static const bgGrey = Color(0xFFF8F8F8);
static const cardGrey = Color(0xFFEDEDED);
static const textDark = Color(0xFF333333);
@override
void initState() {
super.initState();
_selectedDate = DateTime.now();
_scrollController = ScrollController();
_buildDateRange();
WidgetsBinding.instance.addPostFrameCallback((_) => _scrollToSelected());
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
void _buildDateRange() {
final today = DateTime.now();
final start = DateTime(today.year, today.month - 2, 1);
final end = DateTime(today.year, today.month + 3, 0);
_allDates = [];
for (var d = start; !d.isAfter(end); d = d.add(const Duration(days: 1))) {
_allDates.add(DateTime(d.year, d.month, d.day));
}
}
void _scrollToSelected({bool animate = false}) {
if (!_scrollController.hasClients) return;
final idx = _allDates.indexWhere((d) => _isSameDay(d, _selectedDate));
if (idx == -1) return;
const itemWidth = 50.0;
final viewWidth = _scrollController.position.viewportDimension;
final target = (idx * itemWidth - viewWidth / 2 + itemWidth / 2 + 16)
.clamp(0.0, _scrollController.position.maxScrollExtent);
if (animate) {
_scrollController.animateTo(target,
duration: const Duration(milliseconds: 350), curve: Curves.easeOut);
} else {
_scrollController.jumpTo(target);
}
}
bool _isSameDay(DateTime a, DateTime b) =>
a.year == b.year && a.month == b.month && a.day == b.day;
void _selectDate(DateTime date) {
if (_isSameDay(date, _selectedDate)) return;
setState(() => _selectedDate = date);
WidgetsBinding.instance
.addPostFrameCallback((_) => _scrollToSelected(animate: true));
}
void _goToPreviousMonth() {
final prev = DateTime(_selectedDate.year, _selectedDate.month - 1, 1);
final maxDay = DateTime(prev.year, prev.month + 1, 0).day;
_selectDate(
DateTime(prev.year, prev.month, _selectedDate.day.clamp(1, maxDay)));
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: bgGrey,
body: SafeArea(
child: Column(
children: [
Padding(
padding: const EdgeInsets.fromLTRB(12, 8, 12, 16),
child: Row(
children: [
Material(
color: cardGrey,
shape: const CircleBorder(),
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: _goToPreviousMonth,
customBorder: const CircleBorder(),
child: const SizedBox(
width: 40,
height: 40,
child: Icon(Icons.chevron_left_rounded,
color: textDark, size: 26),
),
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 4),
child: AnimatedDateHeader(
date: _selectedDate,
style: const AnimatedDateHeaderStyle(
dayStyle: TextStyle(
fontSize: 26,
fontWeight: FontWeight.bold,
color: textDark,
height: 1,
),
monthStyle: TextStyle(
fontSize: 26,
fontWeight: FontWeight.bold,
color: textDark,
height: 1,
),
dayDigitHeight: 30,
monthLetterHeight: 30,
),
),
),
),
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(14),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.06),
blurRadius: 16,
offset: const Offset(0, 4),
),
],
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () {},
borderRadius: BorderRadius.circular(14),
child: const SizedBox(
width: 44,
height: 40,
child: Icon(Icons.more_horiz_rounded,
color: textDark, size: 24),
),
),
),
),
],
),
),
const SizedBox(height: 8),
SizedBox(
height: 50,
child: ListView.builder(
controller: _scrollController,
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 16),
itemCount: _allDates.length,
itemExtent: 50,
itemBuilder: (context, index) {
final date = _allDates[index];
final isSelected = _isSameDay(date, _selectedDate);
final isCurrentMonth =
date.month == _selectedDate.month &&
date.year == _selectedDate.year;
final letter = _dayLetters[date.weekday - 1];
return GestureDetector(
onTap: () => _selectDate(date),
child: AnimatedContainer(
duration: const Duration(milliseconds: 250),
width: 44,
height: 60,
margin: const EdgeInsets.symmetric(horizontal: 3),
decoration: BoxDecoration(
color: isSelected ? textDark : cardGrey,
borderRadius: BorderRadius.circular(14),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
AnimatedDefaultTextStyle(
duration: const Duration(milliseconds: 250),
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w500,
color: isSelected
? Colors.white.withValues(alpha: 0.7)
: isCurrentMonth
? textDark.withValues(alpha: 0.4)
: textDark.withValues(alpha: 0.2),
),
child: Text(letter),
),
const SizedBox(height: 2),
AnimatedDefaultTextStyle(
duration: const Duration(milliseconds: 250),
style: TextStyle(
fontSize: 18,
fontWeight: isSelected
? FontWeight.w600
: FontWeight.w400,
color: isSelected
? Colors.white
: isCurrentMonth
? textDark.withValues(alpha: 0.55)
: textDark.withValues(alpha: 0.25),
),
child: Text('${date.day}'),
),
],
),
),
);
},
),
),
const Spacer(),
const Padding(
padding: EdgeInsets.all(16),
child: Text(
'Back button, action buttons, and date list are YOUR UI.\n'
'Only the "15 April" header is from the package.',
textAlign: TextAlign.center,
style: TextStyle(color: Colors.black38, fontSize: 13),
),
),
],
),
),
);
}
}
// ---------------------------------------------------------------------------
// Example 4: Standalone AnimatedDigit
// ---------------------------------------------------------------------------
class DigitExample extends StatefulWidget {
const DigitExample({super.key});
@override
State<DigitExample> createState() => _DigitExampleState();
}
class _DigitExampleState extends State<DigitExample> {
final GlobalKey<AnimatedDigitState> _digitKey =
GlobalKey<AnimatedDigitState>();
int _counter = 0;
void _increment() {
_counter = (_counter + 1) % 10;
_digitKey.currentState?.animateTo('$_counter', Duration.zero);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Standalone AnimatedDigit')),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'AnimatedDigit works for any character:',
style: TextStyle(fontSize: 16),
),
const SizedBox(height: 24),
AnimatedDigit(
key: _digitKey,
digit: '0',
textStyle: const TextStyle(
fontSize: 80,
fontWeight: FontWeight.w900,
color: Colors.deepPurple,
height: 1,
),
containerHeight: 90,
),
const SizedBox(height: 32),
FilledButton.icon(
onPressed: _increment,
icon: const Icon(Icons.add),
label: const Text('Next Digit'),
),
],
),
),
);
}
}