import 'package:flutter/material.dart'; enum ExpandableState { collapsed, expanded, } class ExpandableController { VoidCallback? toggle; ExpandableState state = ExpandableState.collapsed; } class Expandable extends StatefulWidget { const Expandable({ Key? key, required this.header, required this.body, this.animationController, this.animation, this.animationDurationMultiplier = 1.0, this.onExpandChanged, this.onExpandWillChange, this.controller, this.expandOverride, this.curve = Curves.easeInOut, this.initialState = ExpandableState.collapsed, }) : super(key: key); final Widget header; final Widget body; final AnimationController? animationController; final Animation? animation; final double animationDurationMultiplier; final void Function(ExpandableState)? onExpandChanged; final void Function(ExpandableState)? onExpandWillChange; final ExpandableController? controller; final VoidCallback? expandOverride; final Curve curve; final ExpandableState initialState; @override State createState() => _ExpandableState(); } class _ExpandableState extends State with TickerProviderStateMixin { late final AnimationController animationController; late final Animation animation; late final Duration duration; late final ExpandableController? controller; late ExpandableState _toggleState; Future toggle() async { if (animation.isDismissed) { widget.onExpandWillChange?.call(ExpandableState.expanded); await animationController.forward(); _toggleState = ExpandableState.expanded; widget.onExpandChanged?.call(_toggleState); } else if (animation.isCompleted) { widget.onExpandWillChange?.call(ExpandableState.collapsed); await animationController.reverse(); _toggleState = ExpandableState.collapsed; widget.onExpandChanged?.call(_toggleState); } controller?.state = _toggleState; } @override void initState() { _toggleState = widget.initialState; controller = widget.controller; controller?.toggle = toggle; duration = Duration( milliseconds: (500 * widget.animationDurationMultiplier).toInt(), ); animationController = widget.animationController ?? AnimationController( vsync: this, duration: duration, ); final tween = _toggleState == ExpandableState.collapsed ? Tween(begin: 0.0, end: 1.0) : Tween(begin: 1.0, end: 0.0); animation = widget.animation ?? tween.animate( CurvedAnimation( curve: widget.curve, parent: animationController, ), ); super.initState(); } @override void dispose() { animationController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Column( mainAxisSize: MainAxisSize.min, children: [ MouseRegion( cursor: SystemMouseCursors.click, child: GestureDetector( onTap: widget.expandOverride ?? toggle, child: Container( color: Colors.transparent, child: widget.header, ), ), ), SizeTransition( sizeFactor: animation, axisAlignment: 1.0, child: widget.body, ), ], ); } }