ChznExpansionPanelList.dart 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  1. // 修改了ExpansionPanelList的State的build方法
  2. // 并删去了ExpansionPane(已经在原文件中定义),防止重定义
  3. // 原版会在展开的项的上下添加及其难看的鸿沟
  4. import 'package:flutter/material.dart';
  5. import 'package:flutter/widgets.dart';
  6. const double _kPanelHeaderCollapsedHeight = kMinInteractiveDimension;
  7. const EdgeInsets _kPanelHeaderExpandedDefaultPadding = EdgeInsets.symmetric(
  8. vertical: 64.0 - _kPanelHeaderCollapsedHeight,
  9. );
  10. class _SaltedKey<S, V> extends LocalKey {
  11. const _SaltedKey(this.salt, this.value);
  12. final S salt;
  13. final V value;
  14. @override
  15. bool operator ==(Object other) {
  16. if (other.runtimeType != runtimeType)
  17. return false;
  18. return other is _SaltedKey<S, V>
  19. && other.salt == salt
  20. && other.value == value;
  21. }
  22. @override
  23. int get hashCode => hashValues(runtimeType, salt, value);
  24. @override
  25. String toString() {
  26. final String saltString = S == String ? "<'$salt'>" : '<$salt>';
  27. final String valueString = V == String ? "<'$value'>" : '<$value>';
  28. return '[$saltString $valueString]';
  29. }
  30. }
  31. typedef ExpansionPanelCallback = void Function(int panelIndex, bool isExpanded);
  32. typedef ExpansionPanelHeaderBuilder = Widget Function(BuildContext context, bool isExpanded);
  33. class ChznExpansionPanelList extends StatefulWidget {
  34. const ChznExpansionPanelList({
  35. Key? key,
  36. this.children = const <ExpansionPanel>[],
  37. this.expansionCallback,
  38. this.animationDuration = kThemeAnimationDuration,
  39. this.expandedHeaderPadding = _kPanelHeaderExpandedDefaultPadding,
  40. this.dividerColor,
  41. this.elevation = 2,
  42. }) : assert(children != null),
  43. assert(animationDuration != null),
  44. _allowOnlyOnePanelOpen = false,
  45. initialOpenPanelValue = null,
  46. super(key: key);
  47. const ChznExpansionPanelList.radio({
  48. Key? key,
  49. this.children = const <ExpansionPanelRadio>[],
  50. this.expansionCallback,
  51. this.animationDuration = kThemeAnimationDuration,
  52. this.initialOpenPanelValue,
  53. this.expandedHeaderPadding = _kPanelHeaderExpandedDefaultPadding,
  54. this.dividerColor,
  55. this.elevation = 2,
  56. }) : assert(children != null),
  57. assert(animationDuration != null),
  58. _allowOnlyOnePanelOpen = true,
  59. super(key: key);
  60. final List<ExpansionPanel> children;
  61. final ExpansionPanelCallback? expansionCallback;
  62. final Duration animationDuration;
  63. final bool _allowOnlyOnePanelOpen;
  64. final Object? initialOpenPanelValue;
  65. final EdgeInsets expandedHeaderPadding;
  66. final Color? dividerColor;
  67. final double elevation;
  68. @override
  69. State<StatefulWidget> createState() => _ExpansionPanelListState();
  70. }
  71. class _ExpansionPanelListState extends State<ChznExpansionPanelList> {
  72. ExpansionPanelRadio? _currentOpenPanel;
  73. @override
  74. void initState() {
  75. super.initState();
  76. if (widget._allowOnlyOnePanelOpen) {
  77. assert(_allIdentifiersUnique(), 'All ExpansionPanelRadio identifier values must be unique.');
  78. if (widget.initialOpenPanelValue != null) {
  79. _currentOpenPanel =
  80. searchPanelByValue(widget.children.cast<ExpansionPanelRadio>(), widget.initialOpenPanelValue);
  81. }
  82. }
  83. }
  84. @override
  85. void didUpdateWidget(ChznExpansionPanelList oldWidget) {
  86. super.didUpdateWidget(oldWidget);
  87. if (widget._allowOnlyOnePanelOpen) {
  88. assert(_allIdentifiersUnique(), 'All ExpansionPanelRadio identifier values must be unique.');
  89. // If the previous widget was non-radio ExpansionPanelList, initialize the
  90. // open panel to widget.initialOpenPanelValue
  91. if (!oldWidget._allowOnlyOnePanelOpen) {
  92. _currentOpenPanel =
  93. searchPanelByValue(widget.children.cast<ExpansionPanelRadio>(), widget.initialOpenPanelValue);
  94. }
  95. } else {
  96. _currentOpenPanel = null;
  97. }
  98. }
  99. bool _allIdentifiersUnique() {
  100. final Map<Object, bool> identifierMap = <Object, bool>{};
  101. for (final ExpansionPanelRadio child in widget.children.cast<ExpansionPanelRadio>()) {
  102. identifierMap[child.value] = true;
  103. }
  104. return identifierMap.length == widget.children.length;
  105. }
  106. bool _isChildExpanded(int index) {
  107. if (widget._allowOnlyOnePanelOpen) {
  108. final ExpansionPanelRadio radioWidget = widget.children[index] as ExpansionPanelRadio;
  109. return _currentOpenPanel?.value == radioWidget.value;
  110. }
  111. return widget.children[index].isExpanded;
  112. }
  113. void _handlePressed(bool isExpanded, int index) {
  114. widget.expansionCallback?.call(index, isExpanded);
  115. if (widget._allowOnlyOnePanelOpen) {
  116. final ExpansionPanelRadio pressedChild = widget.children[index] as ExpansionPanelRadio;
  117. // If another ExpansionPanelRadio was already open, apply its
  118. // expansionCallback (if any) to false, because it's closing.
  119. for (int childIndex = 0; childIndex < widget.children.length; childIndex += 1) {
  120. final ExpansionPanelRadio child = widget.children[childIndex] as ExpansionPanelRadio;
  121. if (widget.expansionCallback != null &&
  122. childIndex != index &&
  123. child.value == _currentOpenPanel?.value)
  124. widget.expansionCallback!(childIndex, false);
  125. }
  126. setState(() {
  127. _currentOpenPanel = isExpanded ? null : pressedChild;
  128. });
  129. }
  130. }
  131. ExpansionPanelRadio? searchPanelByValue(List<ExpansionPanelRadio> panels, Object? value) {
  132. for (final ExpansionPanelRadio panel in panels) {
  133. if (panel.value == value)
  134. return panel;
  135. }
  136. return null;
  137. }
  138. @override
  139. Widget build(BuildContext context) {
  140. assert(kElevationToShadow.containsKey(widget.elevation),
  141. 'Invalid value for elevation. See the kElevationToShadow constant for'
  142. ' possible elevation values.',
  143. );
  144. final List<MergeableMaterialItem> items = <MergeableMaterialItem>[];
  145. for (int index = 0; index < widget.children.length; index += 1) {
  146. // if (_isChildExpanded(index) && index != 0 && !_isChildExpanded(index - 1))
  147. // items.add(MaterialGap(key: _SaltedKey<BuildContext, int>(context, index * 2 - 1)));
  148. final ExpansionPanel child = widget.children[index];
  149. final Widget headerWidget = child.headerBuilder(
  150. context,
  151. _isChildExpanded(index),
  152. );
  153. Widget expandIconContainer = Container(
  154. margin: const EdgeInsetsDirectional.only(end: 8.0),
  155. child: ExpandIcon(
  156. isExpanded: _isChildExpanded(index),
  157. padding: const EdgeInsets.all(16.0),
  158. onPressed: !child.canTapOnHeader
  159. ? (bool isExpanded) => _handlePressed(isExpanded, index)
  160. : null,
  161. ),
  162. );
  163. if (!child.canTapOnHeader) {
  164. final MaterialLocalizations localizations = MaterialLocalizations.of(context);
  165. expandIconContainer = Semantics(
  166. label: _isChildExpanded(index)? localizations.expandedIconTapHint : localizations.collapsedIconTapHint,
  167. container: true,
  168. child: expandIconContainer,
  169. );
  170. }
  171. Widget header = Row(
  172. children: <Widget>[
  173. Expanded(
  174. child: AnimatedContainer(
  175. duration: widget.animationDuration,
  176. curve: Curves.fastOutSlowIn,
  177. margin: _isChildExpanded(index) ? widget.expandedHeaderPadding : EdgeInsets.zero,
  178. child: ConstrainedBox(
  179. constraints: const BoxConstraints(minHeight: _kPanelHeaderCollapsedHeight),
  180. child: headerWidget,
  181. ),
  182. ),
  183. ),
  184. expandIconContainer,
  185. ],
  186. );
  187. if (child.canTapOnHeader) {
  188. header = MergeSemantics(
  189. child: InkWell(
  190. onTap: () => _handlePressed(_isChildExpanded(index), index),
  191. child: header,
  192. ),
  193. );
  194. }
  195. items.add(
  196. MaterialSlice(
  197. key: _SaltedKey<BuildContext, int>(context, index * 2),
  198. color: child.backgroundColor,
  199. child: Column(
  200. children: <Widget>[
  201. header,
  202. AnimatedCrossFade(
  203. firstChild: Container(height: 0.0),
  204. secondChild: child.body,
  205. firstCurve: const Interval(0.0, 0.6, curve: Curves.fastOutSlowIn),
  206. secondCurve: const Interval(0.4, 1.0, curve: Curves.fastOutSlowIn),
  207. sizeCurve: Curves.fastOutSlowIn,
  208. crossFadeState: _isChildExpanded(index) ? CrossFadeState.showSecond : CrossFadeState.showFirst,
  209. duration: widget.animationDuration,
  210. ),
  211. ],
  212. ),
  213. ),
  214. );
  215. // if (_isChildExpanded(index) && index != widget.children.length - 1)
  216. // items.add(MaterialGap(key: _SaltedKey<BuildContext, int>(context, index * 2 + 1)));
  217. }
  218. return MergeableMaterial(
  219. hasDividers: true,
  220. dividerColor: widget.dividerColor,
  221. elevation: widget.elevation,
  222. children: items,
  223. );
  224. }
  225. }