chzn_expansion_panel_list.dart 8.7 KB

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