Created
August 23, 2025 12:16
-
-
Save EsinShadrach/6358b51397175f0ae345f5924d5b0889 to your computer and use it in GitHub Desktop.
This file contains hidden or 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 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