我选择一个项目后,我试图让ExpansionTile
崩溃,但它不会关闭已打开的列表。
我尝试使用onExpansionChanged
属性,但我没有成功
你怎么能解决这个问题?
在选择项目后插入一个gif,证明ExpansionTile
没有崩溃,下面也是使用的代码。
import 'package:flutter/material.dart';
void main() {
runApp(new ExpansionTileSample());
}
class ExpansionTileSample extends StatefulWidget {
@override
ExpansionTileSampleState createState() => new ExpansionTileSampleState();
}
class ExpansionTileSampleState extends State<ExpansionTileSample> {
String foos = 'One';
@override
Widget build(BuildContext context) {
return new MaterialApp(
home: new Scaffold(
appBar: new AppBar(
title: const Text('ExpansionTile'),
),
body: new ExpansionTile(
title: new Text(this.foos),
backgroundColor: Theme.of(context).accentColor.withOpacity(0.025),
children: <Widget>[
new ListTile(
title: const Text('One'),
onTap: () {
setState(() {
this.foos = 'One';
});
},
),
new ListTile(
title: const Text('Two'),
onTap: () {
setState(() {
this.foos = 'Two';
});
},
),
new ListTile(
title: const Text('Three'),
onTap: () {
setState(() {
this.foos = 'Three';
});
},
),
]
),
),
);
}
}
答案 0 :(得分:11)
这是一个解决方案。我们只需向expand
添加collapse
,toggle
和ExpansionTile
功能。
import 'package:flutter/material.dart';
import 'package:meta/meta.dart';
void main() {
runApp(new ExpansionTileSample());
}
class ExpansionTileSample extends StatefulWidget {
@override
ExpansionTileSampleState createState() => new ExpansionTileSampleState();
}
class ExpansionTileSampleState extends State<ExpansionTileSample> {
final GlobalKey<AppExpansionTileState> expansionTile = new GlobalKey();
String foos = 'One';
@override
Widget build(BuildContext context) {
return new MaterialApp(
home: new Scaffold(
appBar: new AppBar(
title: const Text('ExpansionTile'),
),
body: new AppExpansionTile(
key: expansionTile,
title: new Text(this.foos),
backgroundColor: Theme
.of(context)
.accentColor
.withOpacity(0.025),
children: <Widget>[
new ListTile(
title: const Text('One'),
onTap: () {
setState(() {
this.foos = 'One';
expansionTile.currentState.collapse();
});
},
),
new ListTile(
title: const Text('Two'),
onTap: () {
setState(() {
this.foos = 'Two';
expansionTile.currentState.collapse();
});
},
),
new ListTile(
title: const Text('Three'),
onTap: () {
setState(() {
this.foos = 'Three';
expansionTile.currentState.collapse();
});
},
),
]
),
),
);
}
}
// --- Copied and slightly modified version of the ExpansionTile.
const Duration _kExpand = const Duration(milliseconds: 200);
class AppExpansionTile extends StatefulWidget {
const AppExpansionTile({
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);
final Widget leading;
final Widget title;
final ValueChanged<bool> onExpansionChanged;
final List<Widget> children;
final Color backgroundColor;
final Widget trailing;
final bool initiallyExpanded;
@override
AppExpansionTileState createState() => new AppExpansionTileState();
}
class AppExpansionTileState extends State<AppExpansionTile> with SingleTickerProviderStateMixin {
AnimationController _controller;
CurvedAnimation _easeOutAnimation;
CurvedAnimation _easeInAnimation;
ColorTween _borderColor;
ColorTween _headerColor;
ColorTween _iconColor;
ColorTween _backgroundColor;
Animation<double> _iconTurns;
bool _isExpanded = false;
@override
void initState() {
super.initState();
_controller = new AnimationController(duration: _kExpand, vsync: this);
_easeOutAnimation = new CurvedAnimation(parent: _controller, curve: Curves.easeOut);
_easeInAnimation = new CurvedAnimation(parent: _controller, curve: Curves.easeIn);
_borderColor = new ColorTween();
_headerColor = new ColorTween();
_iconColor = new ColorTween();
_iconTurns = new Tween<double>(begin: 0.0, end: 0.5).animate(_easeInAnimation);
_backgroundColor = new ColorTween();
_isExpanded = PageStorage.of(context)?.readState(context) ?? widget.initiallyExpanded;
if (_isExpanded)
_controller.value = 1.0;
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
void expand() {
_setExpanded(true);
}
void collapse() {
_setExpanded(false);
}
void toggle() {
_setExpanded(!_isExpanded);
}
void _setExpanded(bool isExpanded) {
if (_isExpanded != isExpanded) {
setState(() {
_isExpanded = isExpanded;
if (_isExpanded)
_controller.forward();
else
_controller.reverse().then<void>((Null value) {
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.evaluate(_easeOutAnimation) ?? Colors.transparent;
final Color titleColor = _headerColor.evaluate(_easeInAnimation);
return new Container(
decoration: new BoxDecoration(
color: _backgroundColor.evaluate(_easeOutAnimation) ?? Colors.transparent,
border: new Border(
top: new BorderSide(color: borderSideColor),
bottom: new BorderSide(color: borderSideColor),
)
),
child: new Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
IconTheme.merge(
data: new IconThemeData(color: _iconColor.evaluate(_easeInAnimation)),
child: new ListTile(
onTap: toggle,
leading: widget.leading,
title: new DefaultTextStyle(
style: Theme
.of(context)
.textTheme
.subhead
.copyWith(color: titleColor),
child: widget.title,
),
trailing: widget.trailing ?? new RotationTransition(
turns: _iconTurns,
child: const Icon(Icons.expand_more),
),
),
),
new ClipRect(
child: new Align(
heightFactor: _easeInAnimation.value,
child: child,
),
),
],
),
);
}
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
_borderColor.end = theme.dividerColor;
_headerColor
..begin = theme.textTheme.subhead.color
..end = theme.accentColor;
_iconColor
..begin = theme.unselectedWidgetColor
..end = theme.accentColor;
_backgroundColor.end = widget.backgroundColor;
final bool closed = !_isExpanded && _controller.isDismissed;
return new AnimatedBuilder(
animation: _controller.view,
builder: _buildChildren,
child: closed ? null : new Column(children: widget.children),
);
}
}
答案 1 :(得分:6)
提供的解决方案都没有让我满意。
我最终创建了一个自定义的 ExpandableListTile 。正如您在下面看到的,其代码非常简短,易于定制。
我还必须创建两个支持类(仅处理所需的动画)来构建我的小部件:
主要类别:
class ExpandableListTile extends StatelessWidget {
const ExpandableListTile({Key key, this.title, this.expanded, this.onExpandPressed, this.child}) : super(key: key);
final Widget title;
final bool expanded;
final Widget child;
final Function onExpandPressed;
@override
Widget build(BuildContext context) {
return Column(children: <Widget>[
ListTile(
title: title,
onTap: onExpandPressed,
trailing: IconButton(
onPressed: onExpandPressed,
// icon: Icon(Icons.expand_more),
icon: RotatableSection(
rotated: expanded,
child: SizedBox(height: 30, width: 30, child: Icon(Icons.expand_more),)
),
),
),
ExpandableSection(child: child, expand: expanded,)
]);
}
}
用法(简体):
//...
return ExpandableListTile(
onExpandPressed: (){ setState((){ _expandedItem = 0;}) },
title: Text('Item'),
expanded: _expandedItem==0,
child: Padding(
padding: const EdgeInsets.fromLTRB(8,0,0,0),
child: Container(
color: Color.fromRGBO(0, 0, 0, .2),
child: Column(children: <Widget>[
ListTile(title: Text('Item 1')),
ListTile(title: Text('Item 2')),
ListTile(title: Text('Item 3')),
ListTile(title: Text('Item 4'))
],),
),
),
),
//...
ExpandableSection类:
class ExpandableSection extends StatefulWidget {
final Widget child;
final bool expand;
ExpandableSection({this.expand = false, this.child});
@override
_ExpandableSectionState createState() => _ExpandableSectionState();
}
class _ExpandableSectionState extends State<ExpandableSection> with SingleTickerProviderStateMixin {
AnimationController animationController;
Animation<double> sizeAnimation;
Animation<double> opacityAnimation;
@override
void initState() {
super.initState();
prepareAnimations();
_runExpandCheck();
}
///Setting up the animation
void prepareAnimations() {
animationController = AnimationController(vsync: this, duration: Duration(milliseconds: 300),);
sizeAnimation = CurvedAnimation(parent: animationController, curve: Curves.fastOutSlowIn,);
opacityAnimation = CurvedAnimation(parent: animationController, curve: Curves.slowMiddle,);
}
void _runExpandCheck() {
if(widget.expand) { animationController.forward(); }
else { animationController.reverse(); }
}
@override
void didUpdateWidget(ExpandableSection oldWidget) {
super.didUpdateWidget(oldWidget);
_runExpandCheck();
}
@override
void dispose() {
animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return FadeTransition(
opacity: opacityAnimation,
child: SizeTransition(
axisAlignment: 1.0,
sizeFactor: sizeAnimation,
child: widget.child
)
);
}
}
RotatableSection类:
class RotatableSection extends StatefulWidget {
final Widget child;
final bool rotated;
final double initialSpin;
final double endingSpin;
RotatableSection({this.rotated = false, this.child, this.initialSpin=0, this.endingSpin=0.5});
@override
_RotatableSectionState createState() => _RotatableSectionState();
}
class _RotatableSectionState extends State<RotatableSection> with SingleTickerProviderStateMixin {
AnimationController animationController;
Animation<double> animation;
@override
void initState() {
super.initState();
prepareAnimations();
_runCheck();
}
final double _oneSpin = 6.283184;
///Setting up the animation
void prepareAnimations() {
animationController = AnimationController(vsync: this, duration: Duration(milliseconds: 300),
lowerBound: _oneSpin * widget.initialSpin, upperBound: _oneSpin * widget.endingSpin, );
animation = CurvedAnimation( parent: animationController, curve: Curves.linear, );
}
void _runCheck() {
if(widget.rotated) { animationController.forward(); }
else { animationController.reverse(); }
}
@override
void didUpdateWidget(RotatableSection oldWidget) {
super.didUpdateWidget(oldWidget);
_runCheck();
}
@override
void dispose() {
animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: animationController,
child: widget.child,
builder: (BuildContext context, Widget _widget) {
return new Transform.rotate(
angle: animationController.value,
child: _widget,
);
},
);
}
}
答案 2 :(得分:3)
import 'package:flutter/material.dart';
import 'dart:math';
void main() {
runApp(new ExpansionTileSample());
}
class ExpansionTileSample extends StatefulWidget {
@override
ExpansionTileSampleState createState() => new ExpansionTileSampleState();
}
class ExpansionTileSampleState extends State {
String foos = 'One';
int _key;
_collapse() {
int newKey;
do {
_key = new Random().nextInt(10000);
} while(newKey == _key);
}
@override
void initState() {
super.initState();
_collapse();
}
@override
Widget build(BuildContext context) {
return new MaterialApp(
home: new Scaffold(
appBar: new AppBar(
title: const Text('ExpansionTile'),
),
body: new ExpansionTile(
key: new Key(_key.toString()),
initiallyExpanded: false,
title: new Text(this.foos),
backgroundColor: Theme
.of(context)
.accentColor
.withOpacity(0.025),
children: [
new ListTile(
title: const Text('One'),
onTap: () {
setState(() {
this.foos = 'One';
_collapse();
});
},
),
new ListTile(
title: const Text('Two'),
onTap: () {
setState(() {
this.foos = 'Two';
_collapse();
});
},
),
new ListTile(
title: const Text('Three'),
onTap: () {
setState(() {
this.foos = 'Three';
_collapse();
});
},
),
]
),
),
);
}
}
我发现ExpansionTile具有initialExpanded属性,这是使其崩溃的唯一方法。由于属性最初只能在每次调用构建时重新创建ExpansionTile。要强制它,您只需在每次构建时分配不同的密钥。这可能不是性能最好的解决方案,但ExpansionTile非常简单,所以这应该不是问题。
答案 3 :(得分:0)
我制作了一个TreeView小部件。 它使用ExpansionTile模拟层次结构。 每个ExpansionTile可以托管一个ExpansionTile集合,该集合可以托管... etc。
一切正常,直到我想添加2个功能:全部展开/全部折叠。 帮助我克服了这个问题的是GlobalKey。
我的TreeView小部件,托管在页面中,并与全局密钥一起使用。 我公开了一个VoidCallback。该实现在setState方法中设置了一个新键。
// TreeView host page
GlobalKey<TreeViewState> _key = GlobalKey();
void redrawWidgetCallback() {
setState(() {
// Triggers a rebuild of the whole TreeView.
_key = GlobalKey();
});
}
[...]
// In the Scaffold body :
TreeView(
key: _key,
treeViewItems: widget.treeViewItems,
redrawWidgetCallback: redrawWidgetCallback,
)
然后在小部件的折叠/扩展方法中,最后,我调用widget.redrawWidgetCallback。 无需为treeView的每个级别处理一个键:根元素小部件就足够了。
它可能存在性能问题/不正确的方法。但是,由于我的TreeView不能用于50个以上的节点,因此在我找到一个不涉及创建ExpandableTile的更好的解决方案之前,这对我来说是可以的,因为我相信这一行为将有一天在ExpansionTile上可用。 >
PS:请注意,此解决方法无法运行扩展动画。
答案 4 :(得分:0)
从ExpansionTile类创建一个克隆,并用以下内容替换构建方法代码:
@override
Widget build(BuildContext context) {
final bool closed = !_isExpanded && _controller.isDismissed;
return AnimatedBuilder(
animation: _controller.view,
builder: _buildChildren,
child: closed ? null : GestureDetector(
child: Column(children: widget.children),
onTap: _handleTap,
),
);
}
,然后在单击每个项目后ExpansionTile将合拢。
注意: 如果其中一个孩子有onTap回电,则此解决方案不起作用。 在这种情况下,您必须提供onChildTap处理程序以传递用例中被点击的孩子的索引。(请与我联系以获取完整的代码)
答案 5 :(得分:0)
我找到了一个简单的解决方案。只需向ExpansionTile小部件添加一个全局键,它便会按预期工作。
ExpansionTile(
key: GlobalKey(),
title: Text(title),
children: listTiles,
...
)
不确定为什么会起作用,但是我的猜测是,每次在setState中更改标题时,添加键都会以某种方式强制ExpansionTile小部件重新构建,因此返回其初始(折叠)状态。
答案 6 :(得分:0)
使用这个包并遵循我的代码。希望对你有帮助 :)。使用方便。 https://pub.dev/packages/expansion_tile_card/example
final List<GlobalKey<ExpansionTileCardState>> cardKeyList = [];
... ListView.builder(
itemCount: 10,
itemBuilder: (BuildContext context, int index) {
cardKeyList.add(GlobalKey(debugLabel: "index :$index"));
return ExpansionTileCard(
title: Text('title'),
key: cardKeyList[index],
onExpansionChanged: (value) {
if (value) {
Future.delayed(const Duration(milliseconds: 500), () {
for (var i = 0; i < cardKeyList.length; i++) {
if (index != i) {
cardKeyList[i].currentState?.collapse();
}
}
});
}
},
);
}),
答案 7 :(得分:0)
使用UniqueKey
:
ExpansionTile(
key: UniqueKey(),
// Other properties
)