expansion_panels_demo.dart 10.6 KB
Newer Older
1 2 3 4 5 6
// 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.

import 'package:flutter/material.dart';

7 8
import '../../gallery/demo.dart';

9 10
@visibleForTesting
enum Location {
11 12 13 14 15
  Barbados,
  Bahamas,
  Bermuda
}

16 17
typedef DemoItemBodyBuilder<T> = Widget Function(DemoItem<T> item);
typedef ValueToString<T> = String Function(T value);
18 19

class DualHeaderWithHint extends StatelessWidget {
20
  const DualHeaderWithHint({
21 22 23 24 25 26 27 28 29 30 31 32
    this.name,
    this.value,
    this.hint,
    this.showHint
  });

  final String name;
  final String value;
  final String hint;
  final bool showHint;

  Widget _crossFade(Widget first, Widget second, bool isExpanded) {
33
    return AnimatedCrossFade(
34 35
      firstChild: first,
      secondChild: second,
36 37
      firstCurve: const Interval(0.0, 0.6, curve: Curves.fastOutSlowIn),
      secondCurve: const Interval(0.4, 1.0, curve: Curves.fastOutSlowIn),
38 39 40 41 42 43 44 45
      sizeCurve: Curves.fastOutSlowIn,
      crossFadeState: isExpanded ? CrossFadeState.showSecond : CrossFadeState.showFirst,
      duration: const Duration(milliseconds: 200),
    );
  }

  @override
  Widget build(BuildContext context) {
46 47 48
    final ThemeData theme = Theme.of(context);
    final TextTheme textTheme = theme.textTheme;

49
    return Row(
50
      children: <Widget>[
51
        Expanded(
52
          flex: 2,
53
          child: Container(
54
            margin: const EdgeInsets.only(left: 24.0),
55
            child: FittedBox(
56
              fit: BoxFit.scaleDown,
57
              alignment: Alignment.centerLeft,
58
              child: Text(
59 60 61 62 63
                name,
                style: textTheme.body1.copyWith(fontSize: 15.0),
              ),
            ),
          ),
64
        ),
65
        Expanded(
66
          flex: 3,
67
          child: Container(
68 69
            margin: const EdgeInsets.only(left: 24.0),
            child: _crossFade(
70 71
              Text(value, style: textTheme.caption.copyWith(fontSize: 15.0)),
              Text(hint, style: textTheme.caption.copyWith(fontSize: 15.0)),
72 73 74 75 76 77 78 79 80 81
              showHint
            )
          )
        )
      ]
    );
  }
}

class CollapsibleBody extends StatelessWidget {
82
  const CollapsibleBody({
83
    this.margin = EdgeInsets.zero,
84 85 86 87 88 89 90 91 92 93 94 95
    this.child,
    this.onSave,
    this.onCancel
  });

  final EdgeInsets margin;
  final Widget child;
  final VoidCallback onSave;
  final VoidCallback onCancel;

  @override
  Widget build(BuildContext context) {
96 97 98
    final ThemeData theme = Theme.of(context);
    final TextTheme textTheme = theme.textTheme;

99
    return Column(
100
      children: <Widget>[
101
        Container(
102 103 104 105 106
          margin: const EdgeInsets.only(
            left: 24.0,
            right: 24.0,
            bottom: 24.0
          ) - margin,
107 108
          child: Center(
            child: DefaultTextStyle(
109
              style: textTheme.caption.copyWith(fontSize: 15.0),
110 111 112 113
              child: child
            )
          )
        ),
114
        const Divider(height: 1.0),
115
        Container(
116
          padding: const EdgeInsets.symmetric(vertical: 16.0),
117
          child: Row(
118 119
            mainAxisAlignment: MainAxisAlignment.end,
            children: <Widget>[
120
              Container(
121
                margin: const EdgeInsets.only(right: 8.0),
122
                child: FlatButton(
123
                  onPressed: onCancel,
124
                  child: const Text('CANCEL', style: TextStyle(
125 126 127 128 129 130
                    color: Colors.black54,
                    fontSize: 15.0,
                    fontWeight: FontWeight.w500
                  ))
                )
              ),
131
              Container(
132
                margin: const EdgeInsets.only(right: 8.0),
133
                child: FlatButton(
134 135
                  onPressed: onSave,
                  textTheme: ButtonTextTheme.accent,
136
                  child: const Text('SAVE')
137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153
                )
              )
            ]
          )
        )
      ]
    );
  }
}

class DemoItem<T> {
  DemoItem({
    this.name,
    this.value,
    this.hint,
    this.builder,
    this.valueToString
154
  }) : textController = TextEditingController(text: valueToString(value));
155 156 157

  final String name;
  final String hint;
158
  final TextEditingController textController;
159
  final DemoItemBodyBuilder<T> builder;
160 161 162 163 164 165
  final ValueToString<T> valueToString;
  T value;
  bool isExpanded = false;

  ExpansionPanelHeaderBuilder get headerBuilder {
    return (BuildContext context, bool isExpanded) {
166
      return DualHeaderWithHint(
167 168 169 170 171 172 173
        name: name,
        value: valueToString(value),
        hint: hint,
        showHint: isExpanded
      );
    };
  }
174 175

  Widget build() => builder(this);
176 177
}

Josh Soref's avatar
Josh Soref committed
178
class ExpansionPanelsDemo extends StatefulWidget {
179
  static const String routeName = '/material/expansion_panels';
180 181

  @override
182
  _ExpansionPanelsDemoState createState() => _ExpansionPanelsDemoState();
183 184
}

Josh Soref's avatar
Josh Soref committed
185
class _ExpansionPanelsDemoState extends State<ExpansionPanelsDemo> {
186 187 188 189 190 191 192
  List<DemoItem<dynamic>> _demoItems;

  @override
  void initState() {
    super.initState();

    _demoItems = <DemoItem<dynamic>>[
193
      DemoItem<String>(
194
        name: 'Trip',
195 196 197
        value: 'Caribbean cruise',
        hint: 'Change trip name',
        valueToString: (String value) => value,
198
        builder: (DemoItem<String> item) {
199 200 201 202 203 204
          void close() {
            setState(() {
              item.isExpanded = false;
            });
          }

205 206
          return Form(
            child: Builder(
Matt Perry's avatar
Matt Perry committed
207
              builder: (BuildContext context) {
208
                return CollapsibleBody(
Matt Perry's avatar
Matt Perry committed
209 210 211
                  margin: const EdgeInsets.symmetric(horizontal: 16.0),
                  onSave: () { Form.of(context).save(); close(); },
                  onCancel: () { Form.of(context).reset(); close(); },
212
                  child: Padding(
Matt Perry's avatar
Matt Perry committed
213
                    padding: const EdgeInsets.symmetric(horizontal: 16.0),
214
                    child: TextFormField(
215
                      controller: item.textController,
216
                      decoration: InputDecoration(
217 218 219 220
                        hintText: item.hint,
                        labelText: item.name,
                      ),
                      onSaved: (String value) { item.value = value; },
Matt Perry's avatar
Matt Perry committed
221
                    ),
222
                  ),
Matt Perry's avatar
Matt Perry committed
223
                );
224 225
              },
            ),
226
          );
227
        },
228
      ),
229
      DemoItem<Location>(
230
        name: 'Location',
231
        value: Location.Bahamas,
232
        hint: 'Select location',
233 234
        valueToString: (Location location) => location.toString().split('.')[1],
        builder: (DemoItem<Location> item) {
235 236 237 238 239
          void close() {
            setState(() {
              item.isExpanded = false;
            });
          }
240 241
          return Form(
            child: Builder(
Matt Perry's avatar
Matt Perry committed
242
              builder: (BuildContext context) {
243
                return CollapsibleBody(
Matt Perry's avatar
Matt Perry committed
244 245
                  onSave: () { Form.of(context).save(); close(); },
                  onCancel: () { Form.of(context).reset(); close(); },
246
                  child: FormField<Location>(
Matt Perry's avatar
Matt Perry committed
247
                    initialValue: item.value,
248 249
                    onSaved: (Location result) { item.value = result; },
                    builder: (FormFieldState<Location> field) {
250
                      return Column(
Matt Perry's avatar
Matt Perry committed
251 252 253
                        mainAxisSize: MainAxisSize.min,
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: <Widget>[
254 255 256 257 258
                          RadioListTile<Location>(
                            value: Location.Bahamas,
                            title: const Text('Bahamas'),
                            groupValue: field.value,
                            onChanged: field.didChange,
Matt Perry's avatar
Matt Perry committed
259
                          ),
260 261 262 263 264 265 266 267 268 269 270
                          RadioListTile<Location>(
                            value: Location.Barbados,
                            title: const Text('Barbados'),
                            groupValue: field.value,
                            onChanged: field.didChange,
                          ),
                          RadioListTile<Location>(
                            value: Location.Bermuda,
                            title: const Text('Bermuda'),
                            groupValue: field.value,
                            onChanged: field.didChange,
Matt Perry's avatar
Matt Perry committed
271 272 273 274 275 276 277 278
                          ),
                        ]
                      );
                    }
                  ),
                );
              }
            )
279 280 281
          );
        }
      ),
282
      DemoItem<double>(
283
        name: 'Sun',
284
        value: 80.0,
285
        hint: 'Select sun level',
286
        valueToString: (double amount) => '${amount.round()}',
287
        builder: (DemoItem<double> item) {
288 289 290 291 292 293
          void close() {
            setState(() {
              item.isExpanded = false;
            });
          }

294 295
          return Form(
            child: Builder(
Matt Perry's avatar
Matt Perry committed
296
              builder: (BuildContext context) {
297
                return CollapsibleBody(
Matt Perry's avatar
Matt Perry committed
298 299
                  onSave: () { Form.of(context).save(); close(); },
                  onCancel: () { Form.of(context).reset(); close(); },
300
                  child: FormField<double>(
Matt Perry's avatar
Matt Perry committed
301 302 303
                    initialValue: item.value,
                    onSaved: (double value) { item.value = value; },
                    builder: (FormFieldState<double> field) {
304
                      return Slider(
Matt Perry's avatar
Matt Perry committed
305 306 307 308 309 310
                        min: 0.0,
                        max: 100.0,
                        divisions: 5,
                        activeColor: Colors.orange[100 + (field.value * 5.0).round()],
                        label: '${field.value.round()}',
                        value: field.value,
311
                        onChanged: field.didChange,
Matt Perry's avatar
Matt Perry committed
312 313 314 315
                      );
                    },
                  ),
                );
316
              }
Matt Perry's avatar
Matt Perry committed
317
            )
318 319 320 321 322 323 324 325
          );
        }
      )
    ];
  }

  @override
  Widget build(BuildContext context) {
326
    return Scaffold(
327 328 329 330 331 332
      appBar: AppBar(
        title: const Text('Expansion panels'),
        actions: <Widget>[
          MaterialDemoDocumentationButton(ExpansionPanelsDemo.routeName),
        ],
      ),
333 334
      body: SingleChildScrollView(
        child: SafeArea(
335 336
          top: false,
          bottom: false,
337
          child: Container(
338
            margin: const EdgeInsets.all(24.0),
339
            child: ExpansionPanelList(
340 341 342 343 344
              expansionCallback: (int index, bool isExpanded) {
                setState(() {
                  _demoItems[index].isExpanded = !isExpanded;
                });
              },
345
              children: _demoItems.map<ExpansionPanel>((DemoItem<dynamic> item) {
346
                return ExpansionPanel(
347 348
                  isExpanded: item.isExpanded,
                  headerBuilder: item.headerBuilder,
349
                  body: item.build()
350 351 352 353 354 355
                );
              }).toList()
            ),
          ),
        ),
      ),
356 357 358
    );
  }
}