Created
June 13, 2022 22:03
-
-
Save funyin/49315994ee78dce8d5f840c618e06f9a to your computer and use it in GitHub Desktop.
A fix for flutters expansion panel list. Removed header icon and gap between selected panels
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; | |
const EdgeInsets _kPanelHeaderExpandedDefaultPadding = EdgeInsets.symmetric( | |
vertical: 64.0 - _kPanelHeaderCollapsedHeight, | |
); | |
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({ | |
Key? key, | |
required this.children, | |
this.expansionCallback, | |
this.animationDuration = kThemeAnimationDuration, | |
this.expandedHeaderPadding = EdgeInsets.zero, | |
this.dividerColor, | |
this.elevation = 2, | |
}) : _allowOnlyOnePanelOpen = false, | |
initialOpenPanelValue = null, | |
super(key: key); | |
/// The children of the expansion panel list. They are laid out in a similar | |
/// fashion to [ListBody]. | |
final List<ExpansionPanel> 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 [ExpansionPanel.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 [ExpansionPanel] 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 ExpansionPanel child = widget.children[index]; | |
final Widget headerWidget = child.headerBuilder( | |
context, | |
_isChildExpanded(index), | |
); | |
Widget expandIconContainer = Container( | |
margin: const EdgeInsetsDirectional.only(end: 8.0), | |
child: ExpandIcon( | |
isExpanded: _isChildExpanded(index), | |
padding: const EdgeInsets.all(16.0), | |
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, | |
); | |
} | |
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, | |
), | |
), | |
), | |
//todo: Uncomment to remove header icon | |
// 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, | |
); | |
} | |
} |
this is useful
thank you very much!
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I added some ToDos on the areas's that were edited