-
-
Save iska9der/9a78b7bb7bc0d063d5f3f56e843057ae to your computer and use it in GitHub Desktop.
Flutter ExpansionPanelList and ExpansionPanel modification: added iconBuilder to ExpansionPanel to remove or customize the icon
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'; | |
const double _kPanelHeaderCollapsedHeight = kMinInteractiveDimension; | |
class _SaltedKey<S, V> extends LocalKey { | |
const _SaltedKey(this.salt, this.value); | |
final S salt; | |
final V value; | |
@override | |
bool operator ==(Object other) { | |
if (other.runtimeType != runtimeType) return false; | |
return other is _SaltedKey<S, V> && | |
other.salt == salt && | |
other.value == value; | |
} | |
@override | |
int get hashCode => Object.hash(runtimeType, salt, value); | |
@override | |
String toString() { | |
final String saltString = S == String ? "<'$salt'>" : '<$salt>'; | |
final String valueString = V == String ? "<'$value'>" : '<$value>'; | |
return '[$saltString $valueString]'; | |
} | |
} | |
class AppExpansionPanelList extends StatefulWidget { | |
/// Creates an expansion panel list widget. The [expansionCallback] is | |
/// triggered when an expansion panel expand/collapse button is pushed. | |
/// | |
/// The [children] and [animationDuration] arguments must not be null. | |
const AppExpansionPanelList({ | |
super.key, | |
required this.children, | |
this.expansionCallback, | |
this.animationDuration = kThemeAnimationDuration, | |
this.expandedHeaderPadding = EdgeInsets.zero, | |
this.dividerColor, | |
this.elevation = 2, | |
}) : _allowOnlyOnePanelOpen = false, | |
initialOpenPanelValue = null; | |
/// The children of the expansion panel list. They are laid out in a similar | |
/// fashion to [ListBody]. | |
final List<AppExpansionPanel> children; | |
/// The callback that gets called whenever one of the expand/collapse buttons | |
/// is pressed. The arguments passed to the callback are the index of the | |
/// pressed panel and whether the panel is currently expanded or not. | |
/// | |
/// If AppExpansionPanelList.radio is used, the callback may be called a | |
/// second time if a different panel was previously open. The arguments | |
/// passed to the second callback are the index of the panel that will close | |
/// and false, marking that it will be closed. | |
/// | |
/// For AppExpansionPanelList, the callback needs to setState when it's notified | |
/// about the closing/opening panel. On the other hand, the callback for | |
/// AppExpansionPanelList.radio is simply meant to inform the parent widget of | |
/// changes, as the radio panels' open/close states are managed internally. | |
/// | |
/// This callback is useful in order to keep track of the expanded/collapsed | |
/// panels in a parent widget that may need to react to these changes. | |
final ExpansionPanelCallback? expansionCallback; | |
/// The duration of the expansion animation. | |
final Duration animationDuration; | |
// Whether multiple panels can be open simultaneously | |
final bool _allowOnlyOnePanelOpen; | |
/// The value of the panel that initially begins open. (This value is | |
/// only used when initializing with the [AppExpansionPanelList.radio] | |
/// constructor.) | |
final Object? initialOpenPanelValue; | |
/// The padding that surrounds the panel header when expanded. | |
/// | |
/// By default, 16px of space is added to the header vertically (above and below) | |
/// during expansion. | |
final EdgeInsets expandedHeaderPadding; | |
/// Defines color for the divider when [AppExpansionPanel.isExpanded] is false. | |
/// | |
/// If `dividerColor` is null, then [DividerThemeData.color] is used. If that | |
/// is null, then [ThemeData.dividerColor] is used. | |
final Color? dividerColor; | |
/// Defines elevation for the [AppExpansionPanel] while it's expanded. | |
/// | |
/// By default, the value of elevation is 2. | |
final double elevation; | |
@override | |
State<AppExpansionPanelList> createState() => _AppExpansionPanelListState(); | |
} | |
class _AppExpansionPanelListState extends State<AppExpansionPanelList> { | |
ExpansionPanelRadio? _currentOpenPanel; | |
@override | |
void initState() { | |
super.initState(); | |
if (widget._allowOnlyOnePanelOpen) { | |
assert(_allIdentifiersUnique(), | |
'All ExpansionPanelRadio identifier values must be unique.'); | |
if (widget.initialOpenPanelValue != null) { | |
_currentOpenPanel = searchPanelByValue( | |
widget.children.cast<ExpansionPanelRadio>(), | |
widget.initialOpenPanelValue); | |
} | |
} | |
} | |
@override | |
void didUpdateWidget(AppExpansionPanelList oldWidget) { | |
super.didUpdateWidget(oldWidget); | |
if (widget._allowOnlyOnePanelOpen) { | |
assert(_allIdentifiersUnique(), | |
'All ExpansionPanelRadio identifier values must be unique.'); | |
// If the previous widget was non-radio AppExpansionPanelList, initialize the | |
// open panel to widget.initialOpenPanelValue | |
if (!oldWidget._allowOnlyOnePanelOpen) { | |
_currentOpenPanel = searchPanelByValue( | |
widget.children.cast<ExpansionPanelRadio>(), | |
widget.initialOpenPanelValue); | |
} | |
} else { | |
_currentOpenPanel = null; | |
} | |
} | |
bool _allIdentifiersUnique() { | |
final Map<Object, bool> identifierMap = <Object, bool>{}; | |
for (final ExpansionPanelRadio child | |
in widget.children.cast<ExpansionPanelRadio>()) { | |
identifierMap[child.value] = true; | |
} | |
return identifierMap.length == widget.children.length; | |
} | |
bool _isChildExpanded(int index) { | |
if (widget._allowOnlyOnePanelOpen) { | |
final ExpansionPanelRadio radioWidget = | |
widget.children[index] as ExpansionPanelRadio; | |
return _currentOpenPanel?.value == radioWidget.value; | |
} | |
return widget.children[index].isExpanded; | |
} | |
void _handlePressed(bool isExpanded, int index) { | |
widget.expansionCallback?.call(index, isExpanded); | |
if (widget._allowOnlyOnePanelOpen) { | |
final ExpansionPanelRadio pressedChild = | |
widget.children[index] as ExpansionPanelRadio; | |
// If another ExpansionPanelRadio was already open, apply its | |
// expansionCallback (if any) to false, because it's closing. | |
for (int childIndex = 0; | |
childIndex < widget.children.length; | |
childIndex += 1) { | |
final ExpansionPanelRadio child = | |
widget.children[childIndex] as ExpansionPanelRadio; | |
if (widget.expansionCallback != null && | |
childIndex != index && | |
child.value == _currentOpenPanel?.value) { | |
widget.expansionCallback!(childIndex, false); | |
} | |
} | |
setState(() { | |
_currentOpenPanel = isExpanded ? null : pressedChild; | |
}); | |
} | |
} | |
ExpansionPanelRadio? searchPanelByValue( | |
List<ExpansionPanelRadio> panels, Object? value) { | |
for (final ExpansionPanelRadio panel in panels) { | |
if (panel.value == value) return panel; | |
} | |
return null; | |
} | |
@override | |
Widget build(BuildContext context) { | |
assert( | |
kElevationToShadow.containsKey(widget.elevation), | |
'Invalid value for elevation. See the kElevationToShadow constant for' | |
' possible elevation values.', | |
); | |
final List<MergeableMaterialItem> items = <MergeableMaterialItem>[]; | |
for (int index = 0; index < widget.children.length; index += 1) { | |
//todo: Uncomment to add gap between selected panels | |
/*if (_isChildExpanded(index) && index != 0 && !_isChildExpanded(index - 1)) | |
items.add(MaterialGap(key: _SaltedKey<BuildContext, int>(context, index * 2 - 1)));*/ | |
final AppExpansionPanel child = widget.children[index]; | |
final Widget headerWidget = child.headerBuilder( | |
context, | |
_isChildExpanded(index), | |
); | |
Widget? expandIconContainer = ExpandIcon( | |
isExpanded: _isChildExpanded(index), | |
onPressed: !child.canTapOnHeader | |
? (bool isExpanded) => _handlePressed(isExpanded, index) | |
: null, | |
); | |
if (!child.canTapOnHeader) { | |
final MaterialLocalizations localizations = | |
MaterialLocalizations.of(context); | |
expandIconContainer = Semantics( | |
label: _isChildExpanded(index) | |
? localizations.expandedIconTapHint | |
: localizations.collapsedIconTapHint, | |
container: true, | |
child: expandIconContainer, | |
); | |
} | |
final iconContainer = child.iconBuilder; | |
if (iconContainer != null) { | |
expandIconContainer = iconContainer( | |
expandIconContainer, | |
_isChildExpanded(index), | |
); | |
} | |
Widget header = Row( | |
children: <Widget>[ | |
Expanded( | |
child: AnimatedContainer( | |
duration: widget.animationDuration, | |
curve: Curves.fastOutSlowIn, | |
margin: _isChildExpanded(index) | |
? widget.expandedHeaderPadding | |
: EdgeInsets.zero, | |
child: ConstrainedBox( | |
constraints: const BoxConstraints( | |
minHeight: _kPanelHeaderCollapsedHeight), | |
child: headerWidget, | |
), | |
), | |
), | |
if (expandIconContainer != null) expandIconContainer, | |
], | |
); | |
if (child.canTapOnHeader) { | |
header = MergeSemantics( | |
child: InkWell( | |
onTap: () => _handlePressed(_isChildExpanded(index), index), | |
child: header, | |
), | |
); | |
} | |
items.add( | |
MaterialSlice( | |
key: _SaltedKey<BuildContext, int>(context, index * 2), | |
color: child.backgroundColor, | |
child: Column( | |
children: <Widget>[ | |
header, | |
AnimatedCrossFade( | |
firstChild: Container(height: 0.0), | |
secondChild: child.body, | |
firstCurve: | |
const Interval(0.0, 0.6, curve: Curves.fastOutSlowIn), | |
secondCurve: | |
const Interval(0.4, 1.0, curve: Curves.fastOutSlowIn), | |
sizeCurve: Curves.fastOutSlowIn, | |
crossFadeState: _isChildExpanded(index) | |
? CrossFadeState.showSecond | |
: CrossFadeState.showFirst, | |
duration: widget.animationDuration, | |
), | |
], | |
), | |
), | |
); | |
if (_isChildExpanded(index) && index != widget.children.length - 1) { | |
items.add(MaterialGap( | |
key: _SaltedKey<BuildContext, int>(context, index * 2 + 1))); | |
} | |
} | |
return MergeableMaterial( | |
hasDividers: true, | |
dividerColor: widget.dividerColor, | |
elevation: widget.elevation, | |
children: items, | |
); | |
} | |
} | |
typedef ExpansionPanelIconBuilder = Widget? Function( | |
Widget child, | |
bool isExpanded, | |
); | |
class AppExpansionPanel { | |
/// Creates an expansion panel to be used as a child for [ExpansionPanelList]. | |
/// See [ExpansionPanelList] for an example on how to use this widget. | |
/// | |
/// The [headerBuilder], [body], and [isExpanded] arguments must not be null. | |
AppExpansionPanel({ | |
required this.headerBuilder, | |
required this.body, | |
this.iconBuilder, | |
this.isExpanded = false, | |
this.canTapOnHeader = false, | |
this.backgroundColor, | |
}); | |
/// The widget builder that builds the expansion panels' header. | |
final ExpansionPanelHeaderBuilder headerBuilder; | |
/// The widget builder that builds the expansion panels' icon. | |
/// | |
/// If not pass any function, then default icon will be displayed. | |
/// | |
/// If builder function return null, then icon will not displayed. | |
final ExpansionPanelIconBuilder? iconBuilder; | |
/// The body of the expansion panel that's displayed below the header. | |
/// | |
/// This widget is visible only when the panel is expanded. | |
final Widget body; | |
/// Whether the panel is expanded. | |
/// | |
/// Defaults to false. | |
final bool isExpanded; | |
/// Whether tapping on the panel's header will expand/collapse it. | |
/// | |
/// Defaults to false. | |
final bool canTapOnHeader; | |
/// Defines the background color of the panel. | |
/// | |
/// Defaults to [ThemeData.cardColor]. | |
final Color? backgroundColor; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Default
For default behaviour, dont provide
iconBuilder
Icon customization
Provide
iconBuilder
toExpansionPanel
:Remove icon
Just return
null
fromiconBuilder
: