Skip to content

Instantly share code, notes, and snippets.

@EsinShadrach
Created August 23, 2025 12:16
Show Gist options
  • Select an option

  • Save EsinShadrach/6358b51397175f0ae345f5924d5b0889 to your computer and use it in GitHub Desktop.

Select an option

Save EsinShadrach/6358b51397175f0ae345f5924d5b0889 to your computer and use it in GitHub Desktop.
import 'package:flutter/material.dart';
class CustomExpansionTile extends StatefulWidget {
final Widget title;
final List<Widget> children;
final bool initiallyExpanded;
final EdgeInsetsGeometry? margin;
final VoidCallback? onToggle;
const CustomExpansionTile({
super.key,
required this.title,
required this.children,
this.initiallyExpanded = false,
this.margin,
this.onToggle,
});
@override
State<CustomExpansionTile> createState() => _CustomExpansionTileState();
}
class _CustomExpansionTileState extends State<CustomExpansionTile>
with SingleTickerProviderStateMixin {
late bool _expanded;
late AnimationController _controller;
late Animation<double> _iconTurns;
@override
void initState() {
super.initState();
_expanded = widget.initiallyExpanded;
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 500),
);
_iconTurns = CurvedAnimation(
parent: _controller,
curve: Curves.easeOutBack,
).drive(Tween(begin: 0.0, end: 0.5));
if (_expanded) {
WidgetsBinding.instance.addPostFrameCallback((_) async {
await Future.delayed(const Duration(milliseconds: 150));
if (mounted) _controller.forward();
});
}
}
void _handleTap() {
setState(() {
_expanded = !_expanded;
if (_expanded) {
_controller.forward();
} else {
_controller.reverse();
}
widget.onToggle?.call();
});
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
Widget _buildStaggeredChildren() {
final children = <Widget>[];
const baseDelay = 0.05; // 50ms between children
for (int i = 0; i < widget.children.length; i++) {
final start = i * baseDelay;
final end = start + 0.4;
final curved = CurvedAnimation(
parent: _controller,
curve: Interval(
start.clamp(0, 1),
end.clamp(0, 1),
curve: Curves.easeOutCubic,
),
);
children.add(
FadeTransition(
opacity: curved,
child: SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, 10),
end: Offset.zero,
).animate(curved),
child: widget.children[i],
),
),
);
}
return Column(children: children);
}
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Column(
children: [
ListTile(
title: widget.title,
onTap: _handleTap,
trailing: RotationTransition(
turns: _iconTurns,
child: Icon(
Icons.expand_more_rounded,
color: _expanded ? colorScheme.primary : null,
),
),
),
ClipRect(
child: AnimatedSize(
duration: const Duration(milliseconds: 400),
curve: Curves.easeInOut,
child: ConstrainedBox(
constraints:
_expanded
? const BoxConstraints()
: const BoxConstraints(maxHeight: 0),
child: _buildStaggeredChildren(),
),
),
),
],
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment