【问题标题】:Flutter - Collapsing ExpansionTile after choosing an itemFlutter - 选择项目后折叠 ExpansionTile
【发布时间】:2018-08-02 11:28:25
【问题描述】:

我试图让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';
                });
              },              
            ),
          ]
        ),
      ),
    );
  }
}

【问题讨论】:

标签: dart flutter


【解决方案1】:

我已经修改了自定义代码,它对我来说很好。

这是解决方案。

 // Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:flutter/material.dart';
import 'package:flutter/widgets.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
/// [AppExpansionTile] to save and restore its expanded state when it is scrolled
/// in and out of view.
///
/// This class overrides the [ListTileTheme.iconColor] and [ListTileTheme.textColor]
/// theme properties for its [ListTile]. These colors animate between values when
/// the tile is expanded and collapsed: between [iconColor], [collapsedIconColor] and
/// between [textColor] and [collapsedTextColor].
///
/// See also:
///
///  * [ListTile], useful for creating expansion tile [children] when the
///    expansion tile represents a sublist.
///  * The "Expand and collapse" section of
///    <https://material.io/components/lists#types>
class AppExpansionTile 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 AppExpansionTile({
    GlobalKey<AppExpansionTileState>? key,
    this.leading,
    required this.title,
    this.subtitle,
    this.onExpansionChanged,
    this.children = const <Widget>[],
    this.trailing,
    this.initiallyExpanded = false,
    this.maintainState = false,
    this.tilePadding,
    this.expandedCrossAxisAlignment,
    this.expandedAlignment,
    this.childrenPadding,
    this.backgroundColor,
    this.collapsedBackgroundColor,
    this.textColor,
    this.collapsedTextColor,
    this.iconColor,
    this.collapsedIconColor,
  })  : assert(initiallyExpanded != null),
        assert(maintainState != null),
        assert(
          expandedCrossAxisAlignment != CrossAxisAlignment.baseline,
          'CrossAxisAlignment.baseline is not supported since the expanded children '
          'are aligned in a column, not a row. Try to use another constant.',
        ),
        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;

  /// Additional content displayed below the title.
  ///
  /// Typically a [Text] widget.
  final Widget? subtitle;

  /// 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;

  /// When not null, defines the background color of tile when the sublist is collapsed.
  final Color? collapsedBackgroundColor;

  /// 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;

  /// Specifies whether the state of the children is maintained when the tile expands and collapses.
  ///
  /// When true, the children are kept in the tree while the tile is collapsed.
  /// When false (default), the children are removed from the tree when the tile is
  /// collapsed and recreated upon expansion.
  final bool maintainState;

  /// Specifies padding for the [ListTile].
  ///
  /// Analogous to [ListTile.contentPadding], this property defines the insets for
  /// the [leading], [title], [subtitle] and [trailing] widgets. It does not inset
  /// the expanded [children] widgets.
  ///
  /// When the value is null, the tile's padding is `EdgeInsets.symmetric(horizontal: 16.0)`.
  final EdgeInsetsGeometry? tilePadding;

  /// Specifies the alignment of [children], which are arranged in a column when
  /// the tile is expanded.
  ///
  /// The internals of the expanded tile make use of a [Column] widget for
  /// [children], and [Align] widget to align the column. The `expandedAlignment`
  /// parameter is passed directly into the [Align].
  ///
  /// Modifying this property controls the alignment of the column within the
  /// expanded tile, not the alignment of [children] widgets within the column.
  /// To align each child within [children], see [expandedCrossAxisAlignment].
  ///
  /// The width of the column is the width of the widest child widget in [children].
  ///
  /// When the value is null, the value of `expandedAlignment` is [Alignment.center].
  final Alignment? expandedAlignment;

  /// Specifies the alignment of each child within [children] when the tile is expanded.
  ///
  /// The internals of the expanded tile make use of a [Column] widget for
  /// [children], and the `crossAxisAlignment` parameter is passed directly into the [Column].
  ///
  /// Modifying this property controls the cross axis alignment of each child
  /// within its [Column]. Note that the width of the [Column] that houses
  /// [children] will be the same as the widest child widget in [children]. It is
  /// not necessarily the width of [Column] is equal to the width of expanded tile.
  ///
  /// To align the [Column] along the expanded tile, use the [expandedAlignment] property
  /// instead.
  ///
  /// When the value is null, the value of `expandedCrossAxisAlignment` is [CrossAxisAlignment.center].
  final CrossAxisAlignment? expandedCrossAxisAlignment;

  /// Specifies padding for [children].
  ///
  /// When the value is null, the value of `childrenPadding` is [EdgeInsets.zero].
  final EdgeInsetsGeometry? childrenPadding;

  /// The icon color of tile's [trailing] expansion icon when the
  /// sublist is expanded.
  ///
  /// Used to override to the [ListTileTheme.iconColor].
  final Color? iconColor;

  /// The icon color of tile's [trailing] expansion icon when the
  /// sublist is collapsed.
  ///
  /// Used to override to the [ListTileTheme.iconColor].
  final Color? collapsedIconColor;

  /// The color of the tile's titles when the sublist is expanded.
  ///
  /// Used to override to the [ListTileTheme.textColor].
  final Color? textColor;

  /// The color of the tile's titles when the sublist is collapsed.
  ///
  /// Used to override to the [ListTileTheme.textColor].
  final Color? collapsedTextColor;

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

class AppExpansionTileState extends State<AppExpansionTile>
    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();

  late AnimationController _controller;
  late Animation<double> _iconTurns;
  late Animation<double> _heightFactor;
  late Animation<Color?> _borderColor;
  late Animation<Color?> _headerColor;
  late Animation<Color?> _iconColor;
  late 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) as bool? ??
        widget.initiallyExpanded;
    if (_isExpanded) _controller.value = 1.0;
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  void expand() {
    _isExpanded = true;
    handleTap();
  }

  void collapse() {
    _isExpanded = false;
    handleTap();
  }

  @override
  void didUpdateWidget(covariant AppExpansionTile oldWidget) {
    if (widget.initiallyExpanded) {
      expand();
    } else {
      collapse();
    }
    super.didUpdateWidget(oldWidget);
  }

  void handleTap() {
    setState(() {
      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(
          top: BorderSide(color: borderSideColor),
          bottom: BorderSide(color: borderSideColor),
        ),
      ),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: <Widget>[
          ListTileTheme.merge(
            iconColor: _iconColor.value,
            textColor: _headerColor.value,
            child: ListTile(
              onTap: () {
                if (widget.onExpansionChanged != null) {
                  widget.onExpansionChanged!(_isExpanded);
                }
              },
              contentPadding: widget.tilePadding,
              leading: widget.leading,
              title: widget.title,
              subtitle: widget.subtitle,
              trailing: widget.trailing ??
                  RotationTransition(
                    turns: _iconTurns,
                    child: const Icon(Icons.expand_more),
                  ),
            ),
          ),
          ClipRect(
            child: Align(
              alignment: widget.expandedAlignment ?? Alignment.center,
              heightFactor: _heightFactor.value,
              child: child,
            ),
          ),
        ],
      ),
    );
  }

  @override
  void didChangeDependencies() {
    final ThemeData theme = Theme.of(context);
    final ColorScheme colorScheme = theme.colorScheme;
    _borderColorTween.end = theme.dividerColor;
    _headerColorTween
      ..begin = widget.collapsedTextColor ?? theme.textTheme.subtitle1!.color
      ..end = widget.textColor ?? colorScheme.secondary;
    _iconColorTween
      ..begin = widget.collapsedIconColor ?? theme.unselectedWidgetColor
      ..end = widget.iconColor ?? colorScheme.secondary;
    _backgroundColorTween
      ..begin = widget.collapsedBackgroundColor
      ..end = widget.backgroundColor;
    super.didChangeDependencies();
  }

  @override
  Widget build(BuildContext context) {
    final bool closed = !_isExpanded && _controller.isDismissed;
    final bool shouldRemoveChildren = closed && !widget.maintainState;

    final Widget result = Offstage(
      child: TickerMode(
        child: Padding(
          padding: widget.childrenPadding ?? EdgeInsets.zero,
          child: Column(
            crossAxisAlignment:
                widget.expandedCrossAxisAlignment ?? CrossAxisAlignment.center,
            children: widget.children,
          ),
        ),
        enabled: !closed,
      ),
      offstage: closed,
    );

    return AnimatedBuilder(
      animation: _controller.view,
      builder: _buildChildren,
      child: shouldRemoveChildren ? null : result,
    );
  }
}

用法

late int _tileIndex=-1;
    return AppExpansionTile(
          title: Text(
           'Tile $index',
          tilePadding: const EdgeInsets.symmetric(horizontal: 24),
          initiallyExpanded: _tileIndex == index,
          onExpansionChanged: (s) {
            if (_tileIndex == index) {
              _tileIndex = -1;
              setState(() {});
            } else {
              setState(() {
                _tileIndex = index!;
              });
            }
          },
    );

【讨论】:

    【解决方案2】:

    对于使用@simon 解决方案的项目列表

    List<GlobalKey<AppExpansionTileState> > expansionTile;
    

    实例化你的expansionTile

    expansionTile=List<GlobalKey<AppExpansionTileState>>.generate(listItems.length, (index) => GlobalKey());
    

    并在 ListView.builder() 中像这样使用

                                   key: expansionTile[index],
                                    onExpansionChanged: (value) {
                                      if (value) {
                                        for (var tileKey in expansionTile) {
                                          if (tileKey.currentState !=
                                              expansionTile[index]
                                                  .currentState) {
                                            tileKey.currentState.collapse();
                                          } else {
                                            tileKey.currentState.expand();
                                          }
                                        }
                                      }
                                    },
    

    【讨论】:

      【解决方案3】:

      这是一种解决方法。只需添加一个全局键(或选择项目后更改的值键),它将强制 ExpansionTile 重建。缺点是会因为折叠而失去动画。

      ExpansionTile(
        key: GlobalKey(),
        title: Text(title),
        children: listTiles,
        ...
      )
      

      【讨论】:

      • 为了获得更多控制权,可以保存 Key,并且仅在您想要折叠 ExpansionTile 时在 setState 内重新分配。
      • 如果目的是在每次更改标题时重建 ExpansionTile,请使用带有标题值的 ValueKey。不那么广泛,而且更有意义。
      • @Juliano,你能提供一个例子吗?我用以下代码尝试了你的建议:ExpansionTile(title: Container(key: ValueKey(1), child: Row(..))) 但它不起作用。然而,这个发布的答案确实有效
      • @GeneBo 如果您想强制更新组件,请尝试更改 ValueKey 的值。
      【解决方案4】:

      使用UniqueKey:

      ExpansionTile(
        key: UniqueKey(),
        // Other properties
      )
      

      【讨论】:

        【解决方案5】:

        使用这个包并遵循我的代码。希望这个能对您有所帮助 :)。使用方便。 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();
                                  }
                                }
                              });
                            }
                          },
                        );
                      }),
        

        【讨论】:

          【解决方案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,
                    );
                },
              );
            }
          }
          

          【讨论】:

          • 我正要这样做,你刚刚拯救了我的一天 :) 感谢分享!
          • 很高兴它对您有所帮助。您只需支持我的回答即可回报您的青睐。 :)
          • 展开后不折叠。
          • @SumitPansuriya,你只需要改变onExpandPressed中的逻辑,让索引在已经等于0的时候取负值(-1)。使用示例很简单,只是为了展示如何通过其父级控制 ExpandableListTile。您所要做的就是更改扩展的布尔属性。
          • 我在 ExpandableListTile 主列上使用 InkWell 在单击标题时进行切换
          【解决方案7】:

          从 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 处理程序以在用例中传递被点击子项的索引。(联系我获取完整代码)

          【讨论】:

          • 我想折叠除此之外的所有其他打开的图块。如果您发布完整的代码会很棒。谢谢
          • 你可以使用一个简单的 ListTile 而不是 ExpansionTile 来处理那个项目。
          【解决方案8】:

          我制作了一个 TreeView 小部件。 它使用 ExpansionTile 来模拟层次结构。 每个 ExpansionTile 都可以托管一个可以托管...等的 ExpansionTile 集合。

          在我想添加 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:请注意,此解决方法不会运行展开动画。

          【讨论】:

            【解决方案9】:

            这里有一个解决方案。我们只需将expandcollapsetoggle 功能添加到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),
                    );
                }
            }
            

            【讨论】:

            • 谢谢,它工作得很好,即使我打开了两个ExpansionTile,它只会折叠选择该项目的那个。但是为此,每个ExpansionTile 都必须有自己的GlobalKey expansionTile,例如GlobalKey expansionTile1
            • 我不明白为什么他们在小部件的构建中没有这个
            • 我在使用 AppExpansionTile 时遇到了这个问题:I/flutter (14705):EXCEPTION CUGHT BY WIDGETS LIBRARY I/flutter (14705): 在构建 NotificationListener: I/flutter (14705):多个小部件使用相同的 GlobalKey。 I/flutter (14705): Key A GlobalKey 在小部件树中一次只能在一个小部件上指定....你能帮帮我吗?
            • 另一种变体(避免使用 GlobalKey)是将 static AppExpansionTileState of(BuildContext context) { return context.ancestorStateOfType(const TypeMatcher&lt;AppExpansionTileState&gt;()); } 添加到 AppExpansionTile 类中,然后简单地使用 AppExpansionTile.of(context).collapse();
            • 您好,如果在打开一个磁贴时有多个磁贴,另一个应该关闭的磁贴如何使其自动关闭
            【解决方案10】:

            下面的解决方案可行,但它很老套,可能不是最好的:

            
            
                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 最初有Expanded 属性,这是使其折叠的唯一方法。由于属性仅在最初有效,因此您希望在每次调用 build 时都重新创建 ExpansionTile。要强制它,您只需在每次构建它时分配不同的密钥。这可能不是最好的解决方案性能,但 ExpansionTile 非常简单,所以这应该不是问题。

            【讨论】:

            • 谢谢,如果您只有一个ExpansionTile,如果我插入两个ExpansionTile 并同时打开它们,它就可以工作。从其中一个中选择项目时,这两个会折叠,而不仅仅是选择该项目的那个。
            • 我写了一篇关于实现您的解决方案的 LanguageSelector Widget 的文章。这是文章的链接:link
            • 当您的扩展图块具有动态项目并且您需要创建动态全局键时,您的示例工作得很好。上面的例子,我试过但给出了same global key的错误。我通过应用您的代码摆脱了这种情况。
            • 我如何处理折叠所有其他打开的磁贴,但用户点击的磁贴?
            • @praveenDp 还没有,我暂时推迟了它,但很快就会提出解决方案
            猜你喜欢
            • 2022-11-12
            • 1970-01-01
            • 2018-12-24
            • 2012-11-16
            • 2020-02-12
            • 1970-01-01
            • 1970-01-01
            • 2015-12-03
            • 1970-01-01
            相关资源
            最近更新 更多