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 : }
|