颤动 - 选择项目后折叠ExpansionTile

时间:2018-02-22 14:50:52

标签: dart flutter

我选择一个项目后,我试图让ExpansionTile崩溃,但它不会关闭已打开的列表。

我尝试使用onExpansionChanged属性,但我没有成功

你怎么能解决这个问题?

在选择项目后插入一个gif,证明ExpansionTile没有崩溃,下面也是使用的代码。

enter image description here

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';
                });
              },              
            ),
          ]
        ),
      ),
    );
  }
}

8 个答案:

答案 0 :(得分:11)

这是一个解决方案。我们只需向expand添加collapsetoggleExpansionTile功能。

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 。正如您在下面看到的,其代码非常简短,易于定制。

我还必须创建两个支持类(仅处理所需的动画)来构建我的小部件:

  • ExpandableSection :可通过一个参数“ expanded”轻松控制的小部件。
  • RotatableSection :一个小部件,可根据一个参数旋转“扩展更多”图标。

主要类别:

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)

下面的解决方案可行,但它非常hacky,可能不是最好的:



    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
)