控制多个相同类型的小部件,同时避免列出GlobalKeys

时间:2019-09-02 23:25:55

标签: flutter

我一直在使用自定义的Expansion Tile小部件,以便能够以编程方式控制扩展:

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

现在,我可以将GlobalKey<AppExpansionTileState>对象传递给此小部件以控制其扩展:

import 'package:flutter/material.dart';

void main() {
  runApp(new ExpansionTileTest());
}

class ExpansionTileTest extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final GlobalKey<AppExpansionTileState> key = GlobalKey();

    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('ExpansionTile Test'),
        ),
        body: Column(
          children: <Widget>[
            FlatButton(
              child: Text('Expand'),
              onPressed: () => key.currentState.expand(),
            ),
            AppExpansionTile(
              key: key,
              title: Text('My Tile'),
              children: <Widget>[
                Text('My Tile child'),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

这是问题所在:在我的应用程序中,我非常慷慨地使用AppExpansionTile,并且在某些地方它们相互嵌套。我一直在尝试为这些视图实现Expand AllCollapse All函数。当它们在同一级别上有多个时,很容易通过:

List<GlobalKey<AppExpansionTileState>> keys = List();

然后说:

keys.forEach((k) => k.currentState.expand());

我不太确定上面的方法是好的做法,但是它可以工作。但是,当它们彼此嵌套时必须扩展所有这些图块时,情况变得很复杂,并且GlobalKey解决方案开始看起来很难看(示例得到简化,通常我使用{{1}创建图块}):

ListView.builder

有没有一种方法可以使用单个键来控制分配给它的多个小部件?

侧面说明:AppExpansionTile来自此帖子:https://stackoverflow.com/a/48935106/9488787。谢谢@西蒙。

1 个答案:

答案 0 :(得分:0)

我通过为AppExpansionTile创建一个控制器解决了这个问题:

class AppExpansionTileController extends ValueNotifier<bool> {
  AppExpansionTileController(bool value) : super(value);

  void expand() {
    value = true;
  }

  void collapse() {
    value = false;
  }

  void toggle() {
    value = !value;
  }
}

,然后将此控制器添加到我的AppExpansionTile

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.expansionController // Adding controller
    })
        : 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 AppExpansionTileController expansionController; // Adding controller

    @override
    AppExpansionTileState createState() => new AppExpansionTileState();
}

class AppExpansionTileState extends State<AppExpansionTile> with SingleTickerProviderStateMixin {
    AnimationController _animationController;
    CurvedAnimation _easeOutAnimation;
    CurvedAnimation _easeInAnimation;
    ColorTween _borderColor;
    ColorTween _headerColor;
    ColorTween _iconColor;
    ColorTween _backgroundColor;
    Animation<double> _iconTurns;

    // Adding controller
    AppExpansionTileController _expansionController;
    AppExpansionTileController get _effectiveController => widget.controller ?? _expansionController;

    bool _isExpanded;

    @override
    void initState() {
        super.initState();
        _animationController= new AnimationController(duration: _kExpand, vsync: this);
        _easeOutAnimation = new CurvedAnimation(parent: _animationController, curve: Curves.easeOut);
        _easeInAnimation = new CurvedAnimation(parent: _animationController, 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();

        // If no controller was passed, we're creating our own controller with an initially collapsed state
        if (widget.controller == null) _expansionController = AppExpansionTileController(false);

        _isExpanded = _effectiveController.value;
        //Listening to the value from our controller to change expansion state.
        _effectiveController.addListener(() => _setExpanded(_effectiveController.value));
        if (_isExpanded) _animationController.value = 1.0;
    }

    @override
    void dispose() {
        _animationController.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)
                    _animationController.forward();
                else
                    _animationController.reverse().then<void>((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 && _animationController.isDismissed;
        return new AnimatedBuilder(
            animation: _animationController.view,
            builder: _buildChildren,
            child: closed ? null : new Column(children: widget.children),
        );
    }
}

现在,我可以创建控制器的单个实例,并使用它来控制所有AppExpansionTile

import 'package:flutter/material.dart';

void main() {
  runApp(new ExpansionTileTest());
}

class ExpansionTileTest extends StatelessWidget {
  //Creating a controller with an initially collapsed state
  final AppExpansionTileController expansionController = AppExpansionTileController(false);

  @override
  Widget build(BuildContext context) {

    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('ExpansionTile Test'),
        ),
        body: Column(
          children: <Widget>[
            FlatButton(
              child: Text('Expand All'),
              //Calling our controller's expand method
              onPressed: () => expansionController.expand(),
            ),
            AppExpansionTile(
              expansionController: expansionController,
              title: Text('My Tile 1'),
              children: <Widget>[
                AppExpansionTile(
                  expansionController: expansionController,
                  title: Text('My Child Tile 1'),
                ),
                AppExpansionTile(
                  expansionController: expansionController,
                  title: Text('My Child Tile 2'),
                ),
              ],
            ),
            AppExpansionTile(
              expansionController: expansionController,
              title: Text('My Tile 2'),
              children: <Widget>[
                AppExpansionTile(
                  expansionController: expansionController,
                  title: Text('My Child Tile 1'),
                ),
                AppExpansionTile(
                  expansionController: expansionController,
                  title: Text('My Child Tile 2'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}