背景
做移动端开发的朋友经常会遇到数据需要层级展示的场景,如二级或者三级列表。因此flutter也为我们提供了列表中可用来折叠的组件
未展开的效果图
展开的效果图
存在问题:
1.ExpansionTile背景色展开后才显示
2.上下分割线展开后才显示
3.分割线颜色不可修改
4.标题和图标展开时展示成了主题色
那如何解决这些问题呢? 先看源码再说~
源码解析
1 2 3 4 5 6 7 8 9 10 11 | const ExpansionTile({ Key key, this.leading, @required this.title, this.backgroundColor, this.onExpansionChanged, this.children = const <Widget>[], this.trailing, this.initiallyExpanded = false, }) : assert(initiallyExpanded != null), super(key: key); |
leading:左侧头部组件,如用户头像
title:常见标题组件
backgroundColor:展开时子列表背景色
children:展开的widgets
trailing:用于替换尾部箭头的组件
initiallyExpanded:设置默认是否展开
可以看到,ExpansionTile提供的属性非常有限,并没有提供我们用于解决上边问题的方法,ExpansionTile中的代码并不多,其中定义了各种动画,并设置了动画区间,ExpansionTile的展示效果也都依赖于这些颜色~
1 2 3 4 5 6 7 8 9 10 11 12 | final ColorTween _borderColorTween = ColorTween(); final ColorTween _headerColorTween = ColorTween(); final ColorTween _iconColorTween = ColorTween(); final ColorTween _backgroundColorTween = ColorTween(); AnimationController _controller; Animation<double> _iconTurns; Animation<double> _heightFactor; Animation<Color> _borderColor; Animation<Color> _headerColor; Animation<Color> _iconColor; Animation<Color> _backgroundColor; |
_ExpansionTileState中定义的各种动画
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | @override void didChangeDependencies() { final ThemeData theme = Theme.of(context); _borderColorTween ..end = theme.dividerColor; _headerColorTween ..begin = theme.textTheme.subhead.color ..end = theme.accentColor; _iconColorTween ..begin = theme.unselectedWidgetColor ..end = theme.accentColor; _backgroundColorTween ..end = widget.backgroundColor; super.didChangeDependencies(); } |
复写
完整代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 | import 'package:flutter/material.dart'; // import 'colors.dart'; // import 'icons.dart'; // import 'list_tile.dart'; // import 'theme.dart'; // import 'theme_data.dart'; const Duration _kExpand = Duration(milliseconds: 200); /// A single-line [ListTile] with a trailing button that expands or collapses /// the tile to reveal or hide the [children]. /// /// This widget is typically used with [ListView] to create an /// "expand / collapse" list entry. When used with scrolling widgets like /// [ListView], a unique [PageStorageKey] must be specified to enable the /// [HJExpansionTile] to save and restore its expanded state when it is scrolled /// in and out of view. /// /// See also: /// /// * [ListTile], useful for creating expansion tile [children] when the /// expansion tile represents a sublist. /// * The "Expand/collapse" section of /// <https://material.io/guidelines/components/lists-controls.html>. // 分割线显示时机 enum DividerDisplayTime { always, //总是显示 opened, //展开时显示 closed, //关闭时显示 never //不显示 } class HJExpansionTile extends StatefulWidget { /// Creates a single-line [ListTile] with a trailing button that expands or collapses /// the tile to reveal or hide the [children]. The [initiallyExpanded] property must /// be non-null. const HJExpansionTile({ Key key, this.leading, @required this.title, this.backgroundColor, this.dividerColor, this.iconColor, this.dividerDisplayTime, this.onExpansionChanged, this.children = const <Widget>[], this.trailing, this.initiallyExpanded = false, }) : assert(initiallyExpanded != null), super(key: key); /// A widget to display before the title. /// /// Typically a [CircleAvatar] widget. final Widget leading; /// The primary content of the list item. /// /// Typically a [Text] widget. final Widget title; /// Called when the tile expands or collapses. /// /// When the tile starts expanding, this function is called with the value /// true. When the tile starts collapsing, this function is called with /// the value false. final ValueChanged<bool> onExpansionChanged; /// The widgets that are displayed when the tile expands. /// /// Typically [ListTile] widgets. final List<Widget> children; /// The color to display behind the sublist when expanded. final Color backgroundColor; /// A widget to display instead of a rotating arrow icon. final Widget trailing; /// Specifies if the list tile is initially expanded (true) or collapsed (false, the default). final bool initiallyExpanded; final Color dividerColor; final DividerDisplayTime dividerDisplayTime; final Color iconColor; @override _HJExpansionTileState createState() => _HJExpansionTileState(); } class _HJExpansionTileState extends State<HJExpansionTile> with SingleTickerProviderStateMixin { static final Animatable<double> _easeOutTween = CurveTween(curve: Curves.easeOut); static final Animatable<double> _easeInTween = CurveTween(curve: Curves.easeIn); static final Animatable<double> _halfTween = Tween<double>(begin: 0.0, end: 0.5); final ColorTween _borderColorTween = ColorTween(); final ColorTween _headerColorTween = ColorTween(); final ColorTween _iconColorTween = ColorTween(); final ColorTween _backgroundColorTween = ColorTween(); AnimationController _controller; Animation<double> _iconTurns; Animation<double> _heightFactor; Animation<Color> _borderColor; Animation<Color> _headerColor; Animation<Color> _iconColor; Animation<Color> _backgroundColor; bool _isExpanded = false; @override void initState() { super.initState(); _controller = AnimationController(duration: _kExpand, vsync: this); _heightFactor = _controller.drive(_easeInTween); _iconTurns = _controller.drive(_halfTween.chain(_easeInTween)); _borderColor = _controller.drive(_borderColorTween.chain(_easeOutTween)); _headerColor = _controller.drive(_headerColorTween.chain(_easeInTween)); _iconColor = _controller.drive(_iconColorTween.chain(_easeInTween)); _backgroundColor = _controller.drive(_backgroundColorTween.chain(_easeOutTween)); _isExpanded = PageStorage.of(context)?.readState(context) ?? widget.initiallyExpanded; if (_isExpanded) _controller.value = 1.0; } @override void dispose() { _controller.dispose(); super.dispose(); } void _handleTap() { setState(() { _isExpanded = !_isExpanded; if (_isExpanded) { _controller.forward(); } else { _controller.reverse().then<void>((void value) { if (!mounted) return; setState(() { // Rebuild without widget.children. }); }); } PageStorage.of(context)?.writeState(context, _isExpanded); }); if (widget.onExpansionChanged != null) widget.onExpansionChanged(_isExpanded); } Widget _buildChildren(BuildContext context, Widget child) { final Color borderSideColor = _borderColor.value ?? Colors.transparent; return Container( decoration: BoxDecoration( color: _backgroundColor.value ?? Colors.transparent, border: Border( bottom: BorderSide(color: borderSideColor), ), ), child: Column( mainAxisSize: MainAxisSize.min, children: <Widget>[ ListTileTheme.merge( iconColor: _iconColor.value, textColor: _headerColor.value, child: ListTile( onTap: _handleTap, leading: widget.leading, title: widget.title, trailing: widget.trailing ?? RotationTransition( turns: _iconTurns, child: const Icon(Icons.expand_more), ), ), ), ClipRect( child: Align( heightFactor: _heightFactor.value, child: child, ), ), ], ), ); } @override void didChangeDependencies() { setupDidvierColorTween(); setupIconColorTween(); setupBackgroundColor(); super.didChangeDependencies(); } void setupDidvierColorTween() { final ThemeData theme = Theme.of(context); Color beginColor = this.widget.dividerColor ?? theme.dividerColor; Color endColor = beginColor; switch (widget.dividerDisplayTime) { case DividerDisplayTime.always: break; case DividerDisplayTime.opened: endColor = Colors.transparent; break; case DividerDisplayTime.closed: beginColor = Colors.transparent; break; case DividerDisplayTime.never: beginColor = Colors.transparent; endColor = Colors.transparent; break; default: } _borderColorTween ..begin = beginColor ..end = endColor; } void setupIconColorTween(){ final ThemeData theme = Theme.of(context); Color beginColor = this.widget.iconColor ?? theme.unselectedWidgetColor; Color endColor = beginColor; _iconColorTween ..begin = beginColor ..end = endColor; } void setupBackgroundColor(){ _backgroundColorTween ..begin = widget.backgroundColor ..end = widget.backgroundColor; } @override Widget build(BuildContext context) { final bool closed = !_isExpanded && _controller.isDismissed; return AnimatedBuilder( animation: _controller.view, builder: _buildChildren, child: closed ? null : Column(children: widget.children), ); } } |
修改内容说明
代码拷出来以后,原先引入的文件会报错,只需将它们注释或删除,引入
1 2 3 4 5 6 7 | import 'package:flutter/material.dart'; // import 'colors.dart'; // import 'icons.dart'; // import 'list_tile.dart'; // import 'theme.dart'; // import 'theme_data.dart'; |
然后扩展了三个属性,用来配置分割线和icon颜色,当然icon你也可以用trail来设置~
定义了分割线枚举来区分分割线的显示时机
1 2 3 4 5 6 7 | // 分割线显示时机 enum DividerDisplayTime { always, //总是显示 opened, //展开时显示 closed, //关闭时显示 never //不显示 } |
扩展的三个属性
1 2 3 4 5 | final Color dividerColor; final DividerDisplayTime dividerDisplayTime; final Color iconColor; |
调整后的初始化方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | const HJExpansionTile({ Key key, this.leading, @required this.title, this.backgroundColor, this.dividerColor, this.iconColor, this.dividerDisplayTime, this.onExpansionChanged, this.children = const <Widget>[], this.trailing, this.initiallyExpanded = false, }) : assert(initiallyExpanded != null), super(key: key); |
调整背景色,分割线颜色,icon的动画颜色区间
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | @override void didChangeDependencies() { setupDidvierColorTween(); setupIconColorTween(); setupBackgroundColor(); super.didChangeDependencies(); } void setupDidvierColorTween() { final ThemeData theme = Theme.of(context); Color beginColor = this.widget.dividerColor ?? theme.dividerColor; Color endColor = beginColor; switch (widget.dividerDisplayTime) { case DividerDisplayTime.always: break; case DividerDisplayTime.opened: endColor = Colors.transparent; break; case DividerDisplayTime.closed: beginColor = Colors.transparent; break; case DividerDisplayTime.never: beginColor = Colors.transparent; endColor = Colors.transparent; break; default: } _borderColorTween ..begin = beginColor ..end = endColor; } void setupIconColorTween(){ final ThemeData theme = Theme.of(context); Color beginColor = this.widget.iconColor ?? theme.unselectedWidgetColor; Color endColor = beginColor; _iconColorTween ..begin = beginColor ..end = endColor; } void setupBackgroundColor(){ _backgroundColorTween ..begin = widget.backgroundColor ..end = widget.backgroundColor; } |
我不想要上方的分割线,所以只留了一个~任性??
1 2 3 4 5 6 7 8 9 10 11 | Widget _buildChildren(BuildContext context, Widget child) { final Color borderSideColor = _borderColor.value ?? Colors.transparent; return Container( decoration: BoxDecoration( color: _backgroundColor.value ?? Colors.transparent, border: Border( bottom: BorderSide(color: borderSideColor), ), ), ... |
至此已修改完成~
修改后效果
调用Demo
1 2 3 4 5 6 7 8 | HJExpansionTile tile = HJExpansionTile( title: Text(e['title']), children: <Widget>[_expandCell(e['values'])], backgroundColor: Color(0xFFF9FAFC), dividerColor: Color(0xFFE6E6E6), dividerDisplayTime: DividerDisplayTime.always, //默认 iconColor: Colors.grey, ); |
12039186-cd78a2e213f586cf.png
12039186-e32b4e7933f8dbaa.png
结语
本篇文章是在开发总所遇问题的总觉,如有更加方案,欢迎留言讨论,学习道路永无止境,不断学习和总结才能进步~??