expansion_panel.dart 6.63 KB
Newer Older
1 2 3 4
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'package:flutter/foundation.dart';
6 7 8 9 10 11 12 13 14 15
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';

import 'expand_icon.dart';
import 'mergeable_material.dart';
import 'theme.dart';

const double _kPanelHeaderCollapsedHeight = 48.0;
const double _kPanelHeaderExpandedHeight = 64.0;

16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
class _SaltedKey<S, V> extends LocalKey {
  const _SaltedKey(this.salt, this.value);

  final S salt;
  final V value;

  @override
  bool operator ==(dynamic other) {
    if (other.runtimeType != runtimeType)
      return false;
    final _SaltedKey<S, V> typedOther = other;
    return salt == typedOther.salt
        && value == typedOther.value;
  }

  @override
  int get hashCode => hashValues(runtimeType, salt, value);

  @override
  String toString() {
    final String saltString = S == String ? '<\'$salt\'>' : '<$salt>';
    final String valueString = V == String ? '<\'$value\'>' : '<$value>';
    return '[$saltString $valueString]';
  }
}

42 43 44 45 46 47 48 49 50 51 52
/// Signature for the callback that's called when an [ExpansionPanel] is
/// expanded or collapsed.
///
/// The position of the panel within an [ExpansionPanelList] is given by
/// [panelIndex].
typedef void ExpansionPanelCallback(int panelIndex, bool isExpanded);

/// Signature for the callback that's called when the header of the
/// [ExpansionPanel] needs to rebuild.
typedef Widget ExpansionPanelHeaderBuilder(BuildContext context, bool isExpanded);

53
/// A material expansion panel. It has a header and a body and can be either
54 55 56 57 58 59 60 61 62 63 64 65 66
/// expanded or collapsed. The body of the panel is only visible when it is
/// expanded.
///
/// Expansion panels are only intended to be used as children for
/// [ExpansionPanelList].
///
/// See also:
///
///  * [ExpansionPanelList]
///  * <https://material.google.com/components/expansion-panels.html>
class ExpansionPanel {
  /// Creates an expansion panel to be used as a child for [ExpansionPanelList].
  ///
67
  /// The [headerBuilder], [body], and [isExpanded] arguments must not be null.
68 69 70 71
  ExpansionPanel({
    @required this.headerBuilder,
    @required this.body,
    this.isExpanded: false
72 73 74
  }) : assert(headerBuilder != null),
       assert(body != null),
       assert(isExpanded != null);
75 76 77 78 79 80 81 82 83 84 85 86 87 88 89

  /// The widget builder that builds the expansion panels' header.
  final ExpansionPanelHeaderBuilder headerBuilder;

  /// The body of the expansion panel that's displayed below the header.
  ///
  /// This widget is visible only when the panel is expanded.
  final Widget body;

  /// Whether the panel is expanded.
  ///
  /// Defaults to false.
  final bool isExpanded;
}

90
/// A material expansion panel list that lays out its children and animates
91 92 93 94 95 96 97 98 99
/// expansions.
///
/// See also:
///
///  * [ExpansionPanel]
///  * <https://material.google.com/components/expansion-panels.html>
class ExpansionPanelList extends StatelessWidget {
  /// Creates an expansion panel list widget. The [expansionCallback] is
  /// triggered when an expansion panel expand/collapse button is pushed.
100
  const ExpansionPanelList({
101 102 103 104
    Key key,
    this.children: const <ExpansionPanel>[],
    this.expansionCallback,
    this.animationDuration: kThemeAnimationDuration
105 106 107
  }) : assert(children != null),
       assert(animationDuration != null),
       super(key: key);
108

109
  /// The children of the expansion panel list. They are laid out in a similar
110
  /// fashion to [ListBody].
111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135
  final List<ExpansionPanel> children;

  /// The callback that gets called whenever one of the expand/collapse buttons
  /// is pressed. The arguments passed to the callback are the index of the
  /// to-be-expanded panel in the list and whether the panel is currently
  /// expanded or not.
  ///
  /// This callback is useful in order to keep track of the expanded/collapsed
  /// panels in a parent widget that may need to react to these changes.
  final ExpansionPanelCallback expansionCallback;

  /// The duration of the expansion animation.
  final Duration animationDuration;

  bool _isChildExpanded(int index) {
    return children[index].isExpanded;
  }

  @override
  Widget build(BuildContext context) {
    final List<MergeableMaterialItem> items = <MergeableMaterialItem>[];
    const EdgeInsets kExpandedEdgeInsets = const EdgeInsets.symmetric(
      vertical: _kPanelHeaderExpandedHeight - _kPanelHeaderCollapsedHeight
    );

136 137 138
    for (int index = 0; index < children.length; index += 1) {
      if (_isChildExpanded(index) && index != 0 && !_isChildExpanded(index - 1))
        items.add(new MaterialGap(key: new _SaltedKey<BuildContext, int>(context, index * 2 - 1)));
139

140
      final Row header = new Row(
141
        children: <Widget>[
142
          new Expanded(
143 144 145
            child: new AnimatedContainer(
              duration: animationDuration,
              curve: Curves.fastOutSlowIn,
146
              margin: _isChildExpanded(index) ? kExpandedEdgeInsets : EdgeInsets.zero,
147 148
              child: new SizedBox(
                height: _kPanelHeaderCollapsedHeight,
149
                child: children[index].headerBuilder(
150
                  context,
151 152 153 154
                  children[index].isExpanded,
                ),
              ),
            ),
155 156
          ),
          new Container(
157
            margin: const EdgeInsetsDirectional.only(end: 8.0),
158
            child: new ExpandIcon(
159
              isExpanded: _isChildExpanded(index),
160 161
              padding: const EdgeInsets.all(16.0),
              onPressed: (bool isExpanded) {
162 163 164 165 166 167
                if (expansionCallback != null)
                  expansionCallback(index, isExpanded);
              },
            ),
          ),
        ],
168 169 170 171
      );

      items.add(
        new MaterialSlice(
172
          key: new _SaltedKey<BuildContext, int>(context, index * 2),
173 174 175
          child: new Column(
            children: <Widget>[
              header,
176
              new AnimatedCrossFade(
177
                firstChild: new Container(height: 0.0),
178
                secondChild: children[index].body,
179 180
                firstCurve: const Interval(0.0, 0.6, curve: Curves.fastOutSlowIn),
                secondCurve: const Interval(0.4, 1.0, curve: Curves.fastOutSlowIn),
181
                sizeCurve: Curves.fastOutSlowIn,
182
                crossFadeState: _isChildExpanded(index) ? CrossFadeState.showSecond : CrossFadeState.showFirst,
183
                duration: animationDuration,
184 185 186 187
              ),
            ],
          ),
        ),
188 189
      );

190 191
      if (_isChildExpanded(index) && index != children.length - 1)
        items.add(new MaterialGap(key: new _SaltedKey<BuildContext, int>(context, index * 2 + 1)));
192 193 194 195
    }

    return new MergeableMaterial(
      hasDividers: true,
196
      children: items,
197 198 199
    );
  }
}