LCOV - code coverage report
Current view: top level - buttons - expandable_list_tile_button.dart (source / functions) Hit Total Coverage
Test: lcov.info Lines: 61 92 66.3 %
Date: 2024-11-26 10:38:40 Functions: 0 0 -

          Line data    Source code
       1             : import 'package:flutter/material.dart';
       2             : 
       3             : import 'list_tile_button.dart';
       4             : 
       5             : /// A widget that provides an expandable list tile button with customizable headers and content.
       6             : ///
       7             : /// The [ExpandableListTileButton] can be used to create a list tile that expands to reveal additional content when tapped.
       8             : /// It supports custom headers, icons, and expanded content.
       9             : ///
      10             : /// Example usage:
      11             : /// ```dart
      12             : /// ExpandableListTileButton.listTile(
      13             : ///   title: Text('Tap to expand'),
      14             : ///   expanded: Text('Expanded content here'),
      15             : /// );
      16             : /// ```
      17             : class ExpandableListTileButton extends StatefulWidget {
      18             :   /// The content to display when the tile is expanded.
      19             :   final Widget expanded;
      20             : 
      21             :   /// The primary content of the tile when collapsed.
      22             :   final Widget? title;
      23             : 
      24             :   /// Additional content displayed below the [title] when collapsed.
      25             :   final Widget? subtitle;
      26             : 
      27             :   /// The background color of the tile when collapsed.
      28             :   final Color? backgroundColor;
      29             : 
      30             :   /// The background color of the expanded content.
      31             :   final Color? expandedColor;
      32             : 
      33             :   /// The color of the leading icon.
      34             :   final Color? iconColor;
      35             : 
      36             :   /// The color of the trailing expand/collapse icon.
      37             :   final Color? trailingIconColor;
      38             : 
      39             :   /// The color of the border around the tile.
      40             :   final Color? borderColor;
      41             : 
      42             :   /// The elevation of the tile's shadow.
      43             :   final double elevation;
      44             : 
      45             :   /// The leading widget of the tile, typically an icon or avatar.
      46             :   final Widget? leading;
      47             : 
      48             :   /// The icon data for the leading icon.
      49             :   final IconData? icon;
      50             : 
      51             :   /// A builder function to create a custom header widget.
      52             :   ///
      53             :   /// The function provides a [tapAction] callback and a [isExpanded] boolean to control the expansion state.
      54             :   final Widget Function(Function tapAction, bool isExpanded)? customHeader;
      55             : 
      56             :   /// Creates an [ExpandableListTileButton] with the given properties.
      57           1 :   const ExpandableListTileButton({
      58             :     super.key,
      59             :     required this.expanded,
      60             :     this.title,
      61             :     this.subtitle,
      62             :     this.backgroundColor,
      63             :     this.expandedColor,
      64             :     this.iconColor,
      65             :     this.trailingIconColor,
      66             :     this.borderColor,
      67             :     this.elevation = 4.0,
      68             :     this.leading,
      69             :     this.icon,
      70             :     this.customHeader,
      71             :   });
      72             : 
      73             :   /// Creates an [ExpandableListTileButton] with a default [ListTileButton] header.
      74             :   ///
      75             :   /// Example usage:
      76             :   /// ```dart
      77             :   /// ExpandableListTileButton.listTile(
      78             :   ///   title: Text('Tap to expand'),
      79             :   ///   expanded: Text('Expanded content here'),
      80             :   /// );
      81             :   /// ```
      82           1 :   factory ExpandableListTileButton.listTile({
      83             :     required Widget expanded,
      84             :     required Widget title,
      85             :     Widget? subtitle,
      86             :     Color? backgroundColor,
      87             :     Color? expandedColor,
      88             :     Color? trailingIconColor,
      89             :     Color? borderColor,
      90             :     double elevation = 4.0,
      91             :     Widget? leading,
      92             :   }) {
      93           1 :     return ExpandableListTileButton(
      94             :       expanded: expanded,
      95             :       title: title,
      96             :       subtitle: subtitle,
      97             :       backgroundColor: backgroundColor,
      98             :       expandedColor: expandedColor,
      99             :       trailingIconColor: trailingIconColor,
     100             :       borderColor: borderColor,
     101             :       elevation: elevation,
     102             :       leading: leading,
     103           2 :       customHeader: (toggleExpansion, isExpanded) => ListTileButton(
     104           2 :         onPressed: () => toggleExpansion.call(),
     105             :         leading: leading,
     106             :         body: title,
     107             :         subtitle: subtitle,
     108           1 :         trailing: Icon(
     109             :           isExpanded ? Icons.expand_less : Icons.expand_more,
     110             :           color: trailingIconColor,
     111             :         ),
     112             :         backgroundColor: backgroundColor,
     113             :       ),
     114             :     );
     115             :   }
     116             : 
     117             :   /// Creates an [ExpandableListTileButton] with a default [IconListTileButton] header.
     118             :   ///
     119             :   /// Example usage:
     120             :   /// ```dart
     121             :   /// ExpandableListTileButton.iconListTile(
     122             :   ///   icon: Icons.info,
     123             :   ///   title: Text('Tap to expand'),
     124             :   ///   expanded: Text('Expanded content here'),
     125             :   /// );
     126             :   /// ```
     127           0 :   factory ExpandableListTileButton.iconListTile({
     128             :     required Widget expanded,
     129             :     required IconData icon,
     130             :     required Widget title,
     131             :     Widget? subtitle,
     132             :     Color? backgroundColor,
     133             :     Color? expandedColor,
     134             :     Color? iconColor,
     135             :     Color? trailingIconColor,
     136             :     Color? borderColor,
     137             :     double elevation = 4.0,
     138             :     double sizeFactor = 1.0,
     139             :   }) {
     140           0 :     return ExpandableListTileButton(
     141             :       expanded: expanded,
     142             :       title: title,
     143             :       subtitle: subtitle,
     144             :       backgroundColor: backgroundColor,
     145             :       expandedColor: expandedColor,
     146             :       icon: icon,
     147             :       iconColor: iconColor,
     148             :       trailingIconColor: trailingIconColor,
     149             :       borderColor: borderColor,
     150             :       elevation: elevation,
     151           0 :       customHeader: (toggleExpansion, isExpanded) => IconListTileButton(
     152             :         icon: icon,
     153             :         title: title,
     154             :         subtitle: subtitle,
     155           0 :         trailing: Icon(
     156             :           isExpanded ? Icons.expand_less : Icons.expand_more,
     157             :           color: trailingIconColor,
     158             :         ),
     159           0 :         onPressed: () => toggleExpansion.call(),
     160             :         backgroundColor: backgroundColor,
     161             :         iconColor: iconColor,
     162             :         leadingSizeFactor: sizeFactor,
     163             :       ),
     164             :     );
     165             :   }
     166             : 
     167             :   /// Creates an [ExpandableListTileButton] with a custom header.
     168             :   ///
     169             :   /// The [customHeader] builder function is used to create the header widget.
     170             :   ///
     171             :   /// Example usage:
     172             :   /// ```dart
     173             :   /// ExpandableListTileButton.custom(
     174             :   ///   expanded: Text('Expanded content here'),
     175             :   ///   customHeader: (toggleExpansion, isExpanded) => YourCustomHeaderWidget(),
     176             :   /// );
     177             :   /// ```
     178           1 :   factory ExpandableListTileButton.custom({
     179             :     required Widget expanded,
     180             :     required Widget Function(Function tapAction, bool isExpanded) customHeader,
     181             :     Color? backgroundColor,
     182             :     Color? expandedColor,
     183             :     Color? iconColor,
     184             :     Color? trailingIconColor,
     185             :     Color? borderColor,
     186             :     double elevation = 4.0,
     187             :   }) {
     188           1 :     return ExpandableListTileButton(
     189             :       expanded: expanded,
     190             :       backgroundColor: backgroundColor,
     191             :       expandedColor: expandedColor,
     192             :       iconColor: iconColor,
     193             :       trailingIconColor: trailingIconColor,
     194             :       borderColor: borderColor,
     195             :       elevation: elevation,
     196             :       customHeader: customHeader,
     197             :     );
     198             :   }
     199             : 
     200           1 :   @override
     201             :   State<ExpandableListTileButton> createState() =>
     202           1 :       _ExpandableListTileButtonState();
     203             : }
     204             : 
     205             : class _ExpandableListTileButtonState extends State<ExpandableListTileButton>
     206             :     with SingleTickerProviderStateMixin {
     207             :   /// Tracks the expansion state of the tile.
     208             :   bool _isExpanded = false;
     209             : 
     210             :   /// Controls the animation for expanding and collapsing.
     211             :   late AnimationController _controller;
     212             : 
     213             :   /// The animation for size transition.
     214             :   late Animation<double> _animation;
     215             : 
     216             :   /// The widget displayed in the expanded area.
     217             :   late Widget _bodyWidget;
     218             : 
     219             :   /// The height of the header widget.
     220             :   double _headerHeight = 0.0;
     221             : 
     222             :   /// A key to identify the header widget and measure its size.
     223             :   final GlobalKey _headerKey = GlobalKey();
     224             : 
     225             :   /// Initializes the state of the widget, sets up the animation controller, and measures the header height.
     226           1 :   @override
     227             :   void initState() {
     228           1 :     super.initState();
     229           1 :     _bodyWidget = const SizedBox();
     230           2 :     _controller = AnimationController(
     231             :       vsync: this,
     232             :       duration: const Duration(milliseconds: 400),
     233             :     );
     234           2 :     _animation = CurvedAnimation(
     235           1 :       parent: _controller,
     236             :       curve: Curves.easeInOut,
     237             :     );
     238             : 
     239           3 :     _controller.addStatusListener((status) {
     240           1 :       if (status == AnimationStatus.dismissed) {
     241           2 :         setState(() {
     242           1 :           _bodyWidget = const SizedBox();
     243             :         });
     244           1 :       } else if (status == AnimationStatus.forward ||
     245           1 :           status == AnimationStatus.completed) {
     246           2 :         setState(() {
     247           3 :           _bodyWidget = widget.expanded;
     248             :         });
     249             :       }
     250             :     });
     251             : 
     252           3 :     WidgetsBinding.instance.addPostFrameCallback((_) {
     253           1 :       _updateHeaderHeight();
     254             :     });
     255             :   }
     256             : 
     257             :   /// Updates the height of the header widget.
     258           1 :   void _updateHeaderHeight() {
     259             :     final RenderBox? renderBox =
     260           3 :         _headerKey.currentContext?.findRenderObject() as RenderBox?;
     261             :     if (renderBox != null) {
     262           2 :       final height = renderBox.size.height;
     263           2 :       if (_headerHeight != height) {
     264           2 :         setState(() {
     265           1 :           _headerHeight = height;
     266             :         });
     267             :       }
     268             :     }
     269             :   }
     270             : 
     271             :   /// Called whenever the widget configuration changes.
     272           0 :   @override
     273             :   void didUpdateWidget(covariant ExpandableListTileButton oldWidget) {
     274           0 :     super.didUpdateWidget(oldWidget);
     275           0 :     WidgetsBinding.instance.addPostFrameCallback((_) {
     276           0 :       _updateHeaderHeight();
     277             :     });
     278             :   }
     279             : 
     280             :   /// Disposes the animation controller.
     281           1 :   @override
     282             :   void dispose() {
     283           2 :     _controller.dispose();
     284           1 :     super.dispose();
     285             :   }
     286             : 
     287             :   /// Toggles the expansion state of the tile and triggers the animation.
     288           1 :   void _toggleExpansion() {
     289           2 :     setState(() {
     290           2 :       _isExpanded = !_isExpanded;
     291           1 :       if (_isExpanded) {
     292           2 :         _controller.forward();
     293             :       } else {
     294           2 :         _controller.reverse();
     295             :       }
     296             :     });
     297             :   }
     298             : 
     299             :   /// Builds the widget tree.
     300           1 :   @override
     301             :   Widget build(BuildContext context) {
     302           1 :     final theme = Theme.of(context);
     303             : 
     304           1 :     return Stack(
     305           1 :       children: [
     306           1 :         Padding(
     307           3 :           padding: EdgeInsets.only(top: _headerHeight / 2),
     308           1 :           child: SizeTransition(
     309           1 :             sizeFactor: _animation,
     310             :             axisAlignment: 1.0,
     311           1 :             child: Container(
     312             :               width: double.infinity,
     313           1 :               decoration: BoxDecoration(
     314           2 :                 color: widget.expandedColor ??
     315           3 :                     theme.colorScheme.secondary.withOpacity(0.3),
     316             :                 borderRadius: const BorderRadius.only(
     317             :                   bottomLeft: Radius.circular(10),
     318             :                   bottomRight: Radius.circular(10),
     319             :                 ),
     320           1 :                 border: Border.all(
     321           2 :                   color: widget.borderColor ?? Colors.transparent,
     322             :                 ),
     323             :               ),
     324           3 :               padding: EdgeInsets.only(top: _headerHeight / 2),
     325           1 :               child: _bodyWidget,
     326             :             ),
     327             :           ),
     328             :         ),
     329           1 :         Container(
     330           1 :           key: _headerKey,
     331           2 :           child: widget.customHeader != null
     332           5 :               ? widget.customHeader!(_toggleExpansion, _isExpanded)
     333           0 :               : _buildDefaultHeader(context, theme),
     334             :         ),
     335             :       ],
     336             :     );
     337             :   }
     338             : 
     339             :   /// Builds the default header if [customHeader] is not provided.
     340           0 :   Widget _buildDefaultHeader(BuildContext context, ThemeData theme) {
     341           0 :     return widget.icon != null
     342           0 :         ? IconListTileButton(
     343           0 :             icon: widget.icon!,
     344           0 :             iconColor: widget.iconColor ?? theme.iconTheme.color,
     345           0 :             title: widget.title!,
     346           0 :             subtitle: widget.subtitle,
     347           0 :             onPressed: _toggleExpansion,
     348           0 :             trailing: Icon(
     349           0 :               _isExpanded ? Icons.expand_less : Icons.expand_more,
     350           0 :               color: widget.trailingIconColor ?? theme.iconTheme.color,
     351             :             ),
     352           0 :             backgroundColor: widget.backgroundColor ?? theme.cardColor,
     353             :           )
     354           0 :         : ListTileButton(
     355           0 :             onPressed: _toggleExpansion,
     356           0 :             leading: widget.leading,
     357           0 :             body: widget.title,
     358           0 :             subtitle: widget.subtitle,
     359           0 :             trailing: Icon(
     360           0 :               _isExpanded ? Icons.expand_less : Icons.expand_more,
     361           0 :               color: widget.trailingIconColor ?? theme.iconTheme.color,
     362             :             ),
     363           0 :             backgroundColor: widget.backgroundColor ?? theme.cardColor,
     364             :           );
     365             :   }
     366             : }

Generated by: LCOV version 1.14