"Fossies" - the Fresh Open Source Software Archive

Member "flutter-1.22.4/packages/flutter/lib/src/material/expansion_panel.dart" (13 Nov 2020, 18861 Bytes) of package /linux/misc/flutter-1.22.4.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Dart source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file.

    1 // Copyright 2014 The Flutter Authors. All rights reserved.
    2 // Use of this source code is governed by a BSD-style license that can be
    3 // found in the LICENSE file.
    4 
    5 // @dart = 2.8
    6 
    7 import 'package:flutter/rendering.dart';
    8 import 'package:flutter/widgets.dart';
    9 
   10 import 'constants.dart';
   11 import 'expand_icon.dart';
   12 import 'ink_well.dart';
   13 import 'material_localizations.dart';
   14 import 'mergeable_material.dart';
   15 import 'shadows.dart';
   16 import 'theme.dart';
   17 
   18 const double _kPanelHeaderCollapsedHeight = kMinInteractiveDimension;
   19 const EdgeInsets _kPanelHeaderExpandedDefaultPadding = EdgeInsets.symmetric(
   20     vertical: 64.0 - _kPanelHeaderCollapsedHeight
   21 );
   22 
   23 class _SaltedKey<S, V> extends LocalKey {
   24   const _SaltedKey(this.salt, this.value);
   25 
   26   final S salt;
   27   final V value;
   28 
   29   @override
   30   bool operator ==(Object other) {
   31     if (other.runtimeType != runtimeType)
   32       return false;
   33     return other is _SaltedKey<S, V>
   34         && other.salt == salt
   35         && other.value == value;
   36   }
   37 
   38   @override
   39   int get hashCode => hashValues(runtimeType, salt, value);
   40 
   41   @override
   42   String toString() {
   43     final String saltString = S == String ? "<'$salt'>" : '<$salt>';
   44     final String valueString = V == String ? "<'$value'>" : '<$value>';
   45     return '[$saltString $valueString]';
   46   }
   47 }
   48 
   49 /// Signature for the callback that's called when an [ExpansionPanel] is
   50 /// expanded or collapsed.
   51 ///
   52 /// The position of the panel within an [ExpansionPanelList] is given by
   53 /// [panelIndex].
   54 typedef ExpansionPanelCallback = void Function(int panelIndex, bool isExpanded);
   55 
   56 /// Signature for the callback that's called when the header of the
   57 /// [ExpansionPanel] needs to rebuild.
   58 typedef ExpansionPanelHeaderBuilder = Widget Function(BuildContext context, bool isExpanded);
   59 
   60 /// A material expansion panel. It has a header and a body and can be either
   61 /// expanded or collapsed. The body of the panel is only visible when it is
   62 /// expanded.
   63 ///
   64 /// Expansion panels are only intended to be used as children for
   65 /// [ExpansionPanelList].
   66 ///
   67 /// See [ExpansionPanelList] for a sample implementation.
   68 ///
   69 /// See also:
   70 ///
   71 ///  * [ExpansionPanelList]
   72 ///  * <https://material.io/design/components/lists.html#types>
   73 class ExpansionPanel {
   74   /// Creates an expansion panel to be used as a child for [ExpansionPanelList].
   75   /// See [ExpansionPanelList] for an example on how to use this widget.
   76   ///
   77   /// The [headerBuilder], [body], and [isExpanded] arguments must not be null.
   78   ExpansionPanel({
   79     @required this.headerBuilder,
   80     @required this.body,
   81     this.isExpanded = false,
   82     this.canTapOnHeader = false,
   83   }) : assert(headerBuilder != null),
   84        assert(body != null),
   85        assert(isExpanded != null),
   86        assert(canTapOnHeader != null);
   87 
   88   /// The widget builder that builds the expansion panels' header.
   89   final ExpansionPanelHeaderBuilder headerBuilder;
   90 
   91   /// The body of the expansion panel that's displayed below the header.
   92   ///
   93   /// This widget is visible only when the panel is expanded.
   94   final Widget body;
   95 
   96   /// Whether the panel is expanded.
   97   ///
   98   /// Defaults to false.
   99   final bool isExpanded;
  100 
  101   /// Whether tapping on the panel's header will expand/collapse it.
  102   ///
  103   /// Defaults to false.
  104   final bool canTapOnHeader;
  105 
  106 }
  107 
  108 /// An expansion panel that allows for radio-like functionality.
  109 /// This means that at any given time, at most, one [ExpansionPanelRadio]
  110 /// can remain expanded.
  111 ///
  112 /// A unique identifier [value] must be assigned to each panel.
  113 /// This identifier allows the [ExpansionPanelList] to determine
  114 /// which [ExpansionPanelRadio] instance should be expanded.
  115 ///
  116 /// See [ExpansionPanelList.radio] for a sample implementation.
  117 class ExpansionPanelRadio extends ExpansionPanel {
  118 
  119   /// An expansion panel that allows for radio functionality.
  120   ///
  121   /// A unique [value] must be passed into the constructor. The
  122   /// [headerBuilder], [body], [value] must not be null.
  123   ExpansionPanelRadio({
  124     @required this.value,
  125     @required ExpansionPanelHeaderBuilder headerBuilder,
  126     @required Widget body,
  127     bool canTapOnHeader = false,
  128   }) : assert(value != null),
  129       super(
  130         body: body,
  131         headerBuilder: headerBuilder,
  132         canTapOnHeader: canTapOnHeader,
  133       );
  134 
  135   /// The value that uniquely identifies a radio panel so that the currently
  136   /// selected radio panel can be identified.
  137   final Object value;
  138 }
  139 
  140 /// A material expansion panel list that lays out its children and animates
  141 /// expansions.
  142 ///
  143 /// Note that [expansionCallback] behaves differently for [ExpansionPanelList]
  144 /// and [ExpansionPanelList.radio].
  145 ///
  146 /// {@tool dartpad --template=stateful_widget_scaffold}
  147 ///
  148 /// Here is a simple example of how to implement ExpansionPanelList.
  149 ///
  150 /// ```dart preamble
  151 /// // stores ExpansionPanel state information
  152 /// class Item {
  153 ///   Item({
  154 ///     this.expandedValue,
  155 ///     this.headerValue,
  156 ///     this.isExpanded = false,
  157 ///   });
  158 ///
  159 ///   String expandedValue;
  160 ///   String headerValue;
  161 ///   bool isExpanded;
  162 /// }
  163 ///
  164 /// List<Item> generateItems(int numberOfItems) {
  165 ///   return List.generate(numberOfItems, (int index) {
  166 ///     return Item(
  167 ///       headerValue: 'Panel $index',
  168 ///       expandedValue: 'This is item number $index',
  169 ///     );
  170 ///   });
  171 /// }
  172 /// ```
  173 ///
  174 /// ```dart
  175 /// List<Item> _data = generateItems(8);
  176 ///
  177 /// @override
  178 /// Widget build(BuildContext context) {
  179 ///   return SingleChildScrollView(
  180 ///     child: Container(
  181 ///       child: _buildPanel(),
  182 ///     ),
  183 ///   );
  184 /// }
  185 ///
  186 /// Widget _buildPanel() {
  187 ///   return ExpansionPanelList(
  188 ///     expansionCallback: (int index, bool isExpanded) {
  189 ///       setState(() {
  190 ///         _data[index].isExpanded = !isExpanded;
  191 ///       });
  192 ///     },
  193 ///     children: _data.map<ExpansionPanel>((Item item) {
  194 ///       return ExpansionPanel(
  195 ///         headerBuilder: (BuildContext context, bool isExpanded) {
  196 ///           return ListTile(
  197 ///             title: Text(item.headerValue),
  198 ///           );
  199 ///         },
  200 ///         body: ListTile(
  201 ///           title: Text(item.expandedValue),
  202 ///           subtitle: Text('To delete this panel, tap the trash can icon'),
  203 ///           trailing: Icon(Icons.delete),
  204 ///           onTap: () {
  205 ///             setState(() {
  206 ///               _data.removeWhere((currentItem) => item == currentItem);
  207 ///             });
  208 ///           }
  209 ///         ),
  210 ///         isExpanded: item.isExpanded,
  211 ///       );
  212 ///     }).toList(),
  213 ///   );
  214 /// }
  215 /// ```
  216 /// {@end-tool}
  217 ///
  218 /// See also:
  219 ///
  220 ///  * [ExpansionPanel]
  221 ///  * [ExpansionPanelList.radio]
  222 ///  * <https://material.io/design/components/lists.html#types>
  223 class ExpansionPanelList extends StatefulWidget {
  224   /// Creates an expansion panel list widget. The [expansionCallback] is
  225   /// triggered when an expansion panel expand/collapse button is pushed.
  226   ///
  227   /// The [children] and [animationDuration] arguments must not be null.
  228   const ExpansionPanelList({
  229     Key key,
  230     this.children = const <ExpansionPanel>[],
  231     this.expansionCallback,
  232     this.animationDuration = kThemeAnimationDuration,
  233     this.expandedHeaderPadding = _kPanelHeaderExpandedDefaultPadding,
  234     this.dividerColor,
  235     this.elevation = 2,
  236   }) : assert(children != null),
  237        assert(animationDuration != null),
  238        _allowOnlyOnePanelOpen = false,
  239        initialOpenPanelValue = null,
  240        super(key: key);
  241 
  242   /// Creates a radio expansion panel list widget.
  243   ///
  244   /// This widget allows for at most one panel in the list to be open.
  245   /// The expansion panel callback is triggered when an expansion panel
  246   /// expand/collapse button is pushed. The [children] and [animationDuration]
  247   /// arguments must not be null. The [children] objects must be instances
  248   /// of [ExpansionPanelRadio].
  249   ///
  250   /// {@tool dartpad --template=stateful_widget_scaffold}
  251   ///
  252   /// Here is a simple example of how to implement ExpansionPanelList.radio.
  253   ///
  254   /// ```dart preamble
  255   /// // stores ExpansionPanel state information
  256   /// class Item {
  257   ///   Item({
  258   ///     this.id,
  259   ///     this.expandedValue,
  260   ///     this.headerValue,
  261   ///   });
  262   ///
  263   ///   int id;
  264   ///   String expandedValue;
  265   ///   String headerValue;
  266   /// }
  267   ///
  268   /// List<Item> generateItems(int numberOfItems) {
  269   ///   return List.generate(numberOfItems, (int index) {
  270   ///     return Item(
  271   ///       id: index,
  272   ///       headerValue: 'Panel $index',
  273   ///       expandedValue: 'This is item number $index',
  274   ///     );
  275   ///   });
  276   /// }
  277   /// ```
  278   ///
  279   /// ```dart
  280   /// List<Item> _data = generateItems(8);
  281   ///
  282   /// @override
  283   /// Widget build(BuildContext context) {
  284   ///   return SingleChildScrollView(
  285   ///     child: Container(
  286   ///       child: _buildPanel(),
  287   ///     ),
  288   ///   );
  289   /// }
  290   ///
  291   /// Widget _buildPanel() {
  292   ///   return ExpansionPanelList.radio(
  293   ///     initialOpenPanelValue: 2,
  294   ///     children: _data.map<ExpansionPanelRadio>((Item item) {
  295   ///       return ExpansionPanelRadio(
  296   ///         value: item.id,
  297   ///         headerBuilder: (BuildContext context, bool isExpanded) {
  298   ///           return ListTile(
  299   ///             title: Text(item.headerValue),
  300   ///           );
  301   ///         },
  302   ///         body: ListTile(
  303   ///           title: Text(item.expandedValue),
  304   ///           subtitle: Text('To delete this panel, tap the trash can icon'),
  305   ///           trailing: Icon(Icons.delete),
  306   ///           onTap: () {
  307   ///             setState(() {
  308   ///               _data.removeWhere((currentItem) => item == currentItem);
  309   ///             });
  310   ///           }
  311   ///         )
  312   ///       );
  313   ///     }).toList(),
  314   ///   );
  315   /// }
  316   /// ```
  317   /// {@end-tool}
  318   const ExpansionPanelList.radio({
  319     Key key,
  320     this.children = const <ExpansionPanelRadio>[],
  321     this.expansionCallback,
  322     this.animationDuration = kThemeAnimationDuration,
  323     this.initialOpenPanelValue,
  324     this.expandedHeaderPadding = _kPanelHeaderExpandedDefaultPadding,
  325     this.dividerColor,
  326     this.elevation = 2,
  327   }) : assert(children != null),
  328        assert(animationDuration != null),
  329        _allowOnlyOnePanelOpen = true,
  330        super(key: key);
  331 
  332   /// The children of the expansion panel list. They are laid out in a similar
  333   /// fashion to [ListBody].
  334   final List<ExpansionPanel> children;
  335 
  336   /// The callback that gets called whenever one of the expand/collapse buttons
  337   /// is pressed. The arguments passed to the callback are the index of the
  338   /// pressed panel and whether the panel is currently expanded or not.
  339   ///
  340   /// If ExpansionPanelList.radio is used, the callback may be called a
  341   /// second time if a different panel was previously open. The arguments
  342   /// passed to the second callback are the index of the panel that will close
  343   /// and false, marking that it will be closed.
  344   ///
  345   /// For ExpansionPanelList, the callback needs to setState when it's notified
  346   /// about the closing/opening panel. On the other hand, the callback for
  347   /// ExpansionPanelList.radio is simply meant to inform the parent widget of
  348   /// changes, as the radio panels' open/close states are managed internally.
  349   ///
  350   /// This callback is useful in order to keep track of the expanded/collapsed
  351   /// panels in a parent widget that may need to react to these changes.
  352   final ExpansionPanelCallback expansionCallback;
  353 
  354   /// The duration of the expansion animation.
  355   final Duration animationDuration;
  356 
  357   // Whether multiple panels can be open simultaneously
  358   final bool _allowOnlyOnePanelOpen;
  359 
  360   /// The value of the panel that initially begins open. (This value is
  361   /// only used when initializing with the [ExpansionPanelList.radio]
  362   /// constructor.)
  363   final Object initialOpenPanelValue;
  364 
  365   /// The padding that surrounds the panel header when expanded.
  366   ///
  367   /// By default, 16px of space is added to the header vertically (above and below)
  368   /// during expansion.
  369   final EdgeInsets expandedHeaderPadding;
  370 
  371   /// Defines color for the divider when [ExpansionPanel.isExpanded] is false.
  372   ///
  373   /// If `dividerColor` is null, then [DividerThemeData.color] is used. If that
  374   /// is null, then [ThemeData.dividerColor] is used.
  375   final Color dividerColor;
  376 
  377   /// Defines elevation for the [ExpansionPanel] while it's expanded.
  378   ///
  379   /// This uses [kElevationToShadow] to simulate shadows, it does not use
  380   /// [Material]'s arbitrary elevation feature.
  381   ///
  382   /// The following values can be used to define the elevation: 0, 1, 2, 3, 4, 6,
  383   /// 8, 9, 12, 16, 24.
  384   ///
  385   /// By default, the value of elevation is 2.
  386   final int elevation;
  387 
  388   @override
  389   State<StatefulWidget> createState() => _ExpansionPanelListState();
  390 }
  391 
  392 class _ExpansionPanelListState extends State<ExpansionPanelList> {
  393   ExpansionPanelRadio _currentOpenPanel;
  394 
  395   @override
  396   void initState() {
  397     super.initState();
  398     if (widget._allowOnlyOnePanelOpen) {
  399       assert(_allIdentifiersUnique(), 'All ExpansionPanelRadio identifier values must be unique.');
  400       if (widget.initialOpenPanelValue != null) {
  401         _currentOpenPanel =
  402           searchPanelByValue(widget.children.cast<ExpansionPanelRadio>(), widget.initialOpenPanelValue);
  403       }
  404     }
  405   }
  406 
  407   @override
  408   void didUpdateWidget(ExpansionPanelList oldWidget) {
  409     super.didUpdateWidget(oldWidget);
  410 
  411     if (widget._allowOnlyOnePanelOpen) {
  412       assert(_allIdentifiersUnique(), 'All ExpansionPanelRadio identifier values must be unique.');
  413       // If the previous widget was non-radio ExpansionPanelList, initialize the
  414       // open panel to widget.initialOpenPanelValue
  415       if (!oldWidget._allowOnlyOnePanelOpen) {
  416         _currentOpenPanel =
  417           searchPanelByValue(widget.children.cast<ExpansionPanelRadio>(), widget.initialOpenPanelValue);
  418       }
  419     } else {
  420       _currentOpenPanel = null;
  421     }
  422   }
  423 
  424   bool _allIdentifiersUnique() {
  425     final Map<Object, bool> identifierMap = <Object, bool>{};
  426     for (final ExpansionPanelRadio child in widget.children.cast<ExpansionPanelRadio>()) {
  427       identifierMap[child.value] = true;
  428     }
  429     return identifierMap.length == widget.children.length;
  430   }
  431 
  432   bool _isChildExpanded(int index) {
  433     if (widget._allowOnlyOnePanelOpen) {
  434       final ExpansionPanelRadio radioWidget = widget.children[index] as ExpansionPanelRadio;
  435       return _currentOpenPanel?.value == radioWidget.value;
  436     }
  437     return widget.children[index].isExpanded;
  438   }
  439 
  440   void _handlePressed(bool isExpanded, int index) {
  441     if (widget.expansionCallback != null)
  442       widget.expansionCallback(index, isExpanded);
  443 
  444     if (widget._allowOnlyOnePanelOpen) {
  445       final ExpansionPanelRadio pressedChild = widget.children[index] as ExpansionPanelRadio;
  446 
  447       // If another ExpansionPanelRadio was already open, apply its
  448       // expansionCallback (if any) to false, because it's closing.
  449       for (int childIndex = 0; childIndex < widget.children.length; childIndex += 1) {
  450         final ExpansionPanelRadio child = widget.children[childIndex] as ExpansionPanelRadio;
  451         if (widget.expansionCallback != null &&
  452             childIndex != index &&
  453             child.value == _currentOpenPanel?.value)
  454           widget.expansionCallback(childIndex, false);
  455       }
  456 
  457       setState(() {
  458         _currentOpenPanel = isExpanded ? null : pressedChild;
  459       });
  460     }
  461   }
  462 
  463   ExpansionPanelRadio searchPanelByValue(List<ExpansionPanelRadio> panels, Object value)  {
  464     for (final ExpansionPanelRadio panel in panels) {
  465       if (panel.value == value)
  466         return panel;
  467     }
  468     return null;
  469   }
  470 
  471   @override
  472   Widget build(BuildContext context) {
  473     assert(kElevationToShadow.containsKey(widget.elevation),
  474       'Invalid value for elevation. See the kElevationToShadow constant for'
  475       ' possible elevation values.'
  476     );
  477 
  478     final List<MergeableMaterialItem> items = <MergeableMaterialItem>[];
  479 
  480     for (int index = 0; index < widget.children.length; index += 1) {
  481       if (_isChildExpanded(index) && index != 0 && !_isChildExpanded(index - 1))
  482         items.add(MaterialGap(key: _SaltedKey<BuildContext, int>(context, index * 2 - 1)));
  483 
  484       final ExpansionPanel child = widget.children[index];
  485       final Widget headerWidget = child.headerBuilder(
  486         context,
  487         _isChildExpanded(index),
  488       );
  489 
  490       Widget expandIconContainer = Container(
  491         margin: const EdgeInsetsDirectional.only(end: 8.0),
  492         child: ExpandIcon(
  493           isExpanded: _isChildExpanded(index),
  494           padding: const EdgeInsets.all(16.0),
  495           onPressed: !child.canTapOnHeader
  496               ? (bool isExpanded) => _handlePressed(isExpanded, index)
  497               : null,
  498         ),
  499       );
  500       if (!child.canTapOnHeader) {
  501         final MaterialLocalizations localizations = MaterialLocalizations.of(context);
  502         expandIconContainer = Semantics(
  503           label: _isChildExpanded(index)? localizations.expandedIconTapHint : localizations.collapsedIconTapHint,
  504           container: true,
  505           child: expandIconContainer,
  506         );
  507       }
  508       Widget header = Row(
  509         children: <Widget>[
  510           Expanded(
  511             child: AnimatedContainer(
  512               duration: widget.animationDuration,
  513               curve: Curves.fastOutSlowIn,
  514               margin: _isChildExpanded(index) ? widget.expandedHeaderPadding : EdgeInsets.zero,
  515               child: ConstrainedBox(
  516                 constraints: const BoxConstraints(minHeight: _kPanelHeaderCollapsedHeight),
  517                 child: headerWidget,
  518               ),
  519             ),
  520           ),
  521           expandIconContainer,
  522         ],
  523       );
  524       if (child.canTapOnHeader) {
  525         header = MergeSemantics(
  526           child: InkWell(
  527             onTap: () => _handlePressed(_isChildExpanded(index), index),
  528             child: header,
  529           ),
  530         );
  531       }
  532       items.add(
  533         MaterialSlice(
  534           key: _SaltedKey<BuildContext, int>(context, index * 2),
  535           child: Column(
  536             children: <Widget>[
  537               header,
  538               AnimatedCrossFade(
  539                 firstChild: Container(height: 0.0),
  540                 secondChild: child.body,
  541                 firstCurve: const Interval(0.0, 0.6, curve: Curves.fastOutSlowIn),
  542                 secondCurve: const Interval(0.4, 1.0, curve: Curves.fastOutSlowIn),
  543                 sizeCurve: Curves.fastOutSlowIn,
  544                 crossFadeState: _isChildExpanded(index) ? CrossFadeState.showSecond : CrossFadeState.showFirst,
  545                 duration: widget.animationDuration,
  546               ),
  547             ],
  548           ),
  549         ),
  550       );
  551 
  552       if (_isChildExpanded(index) && index != widget.children.length - 1)
  553         items.add(MaterialGap(key: _SaltedKey<BuildContext, int>(context, index * 2 + 1)));
  554     }
  555 
  556     return MergeableMaterial(
  557       hasDividers: true,
  558       dividerColor: widget.dividerColor,
  559       elevation: widget.elevation,
  560       children: items,
  561     );
  562   }
  563 }