card_collection.dart 12.8 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5 6
import 'dart:math';

7
import 'package:flutter/material.dart';
8

9
class CardModel {
10
  CardModel(this.value, this.height) :
11
    textController = TextEditingController(text: 'Item $value');
12

13 14
  int value;
  double height;
15
  int get color => ((value % 9) + 1) * 100;
16
  final TextEditingController textController;
17
  Key get key => ObjectKey(this);
18 19
}

20
class CardCollection extends StatefulWidget {
21
  const CardCollection({super.key});
22

23
  @override
24
  CardCollectionState createState() => CardCollectionState();
25 26
}

27
class CardCollectionState extends State<CardCollection> {
28

29
  static const TextStyle cardLabelStyle =
30
    TextStyle(color: Colors.white, fontSize: 18.0, fontWeight: FontWeight.bold);
31

Hans Muller's avatar
Hans Muller committed
32
  // TODO(hansmuller): need a local image asset
33
  static const String _sunshineURL = 'http://www.walltor.com/images/wallpaper/good-morning-sunshine-58540.jpg';
Hans Muller's avatar
Hans Muller committed
34

35
  static const double kCardMargins = 8.0;
36
  static const double kFixedCardHeight = 100.0;
37
  static const List<double> _cardHeights = <double>[
38 39 40 41
    48.0, 63.0, 85.0, 146.0, 60.0, 55.0, 84.0, 96.0, 50.0,
    48.0, 63.0, 85.0, 146.0, 60.0, 55.0, 84.0, 96.0, 50.0,
    48.0, 63.0, 85.0, 146.0, 60.0, 55.0, 84.0, 96.0, 50.0,
  ];
42

43
  MaterialColor _primaryColor = Colors.deepPurple;
44
  List<CardModel> _cardModels = <CardModel>[];
45
  DismissDirection _dismissDirection = DismissDirection.horizontal;
46
  TextAlign _textAlign = TextAlign.center;
Hans Muller's avatar
Hans Muller committed
47
  bool _editable = false;
48
  bool _fixedSizeCards = false;
Hans Muller's avatar
Hans Muller committed
49
  bool _sunshine = false;
50
  bool _varyFontSizes = false;
51

52
  void _updateCardSizes() {
53
    if (_fixedSizeCards) {
54
      return;
55
    }
56
    _cardModels = List<CardModel>.generate(
57 58 59 60
      _cardModels.length,
      (int i) {
        _cardModels[i].height = _editable ? max(_cardHeights[i], 60.0) : _cardHeights[i];
        return _cardModels[i];
61
      },
62 63 64
    );
  }

65
  void _initVariableSizedCardModels() {
66
    _cardModels = List<CardModel>.generate(
67
      _cardHeights.length,
68
      (int i) => CardModel(i, _editable ? max(_cardHeights[i], 60.0) : _cardHeights[i]),
Hixie's avatar
Hixie committed
69
    );
70 71
  }

72 73
  void _initFixedSizedCardModels() {
    const int cardCount = 27;
74
    _cardModels = List<CardModel>.generate(
Hixie's avatar
Hixie committed
75
      cardCount,
76
      (int i) => CardModel(i, kFixedCardHeight),
Hixie's avatar
Hixie committed
77
    );
78 79 80
  }

  void _initCardModels() {
81
    if (_fixedSizeCards) {
82
      _initFixedSizedCardModels();
83
    } else {
84
      _initVariableSizedCardModels();
85
    }
86 87
  }

88
  @override
89 90
  void initState() {
    super.initState();
91 92 93
    _initCardModels();
  }

94
  void dismissCard(CardModel card) {
95
    if (_cardModels.contains(card)) {
96
      setState(() {
97
        _cardModels.remove(card);
98 99 100 101
      });
    }
  }

102
  Widget _buildDrawer() {
103 104
    return Drawer(
      child: IconTheme(
Adam Barth's avatar
Adam Barth committed
105
        data: const IconThemeData(color: Colors.black),
106
        child: ListView(
107
          children: <Widget>[
108
            const DrawerHeader(child: Center(child: Text('Options'))),
109 110 111 112
            buildDrawerCheckbox('Make card labels editable', _editable, _toggleEditable),
            buildDrawerCheckbox('Fixed size cards', _fixedSizeCards, _toggleFixedSizeCards),
            buildDrawerCheckbox('Let the sun shine', _sunshine, _toggleSunshine),
            buildDrawerCheckbox('Vary font sizes', _varyFontSizes, _toggleVaryFontSizes, enabled: !_editable),
113
            const Divider(),
114 115 116 117
            buildDrawerColorRadioItem('Deep Purple', Colors.deepPurple, _primaryColor, _selectColor),
            buildDrawerColorRadioItem('Green', Colors.green, _primaryColor, _selectColor),
            buildDrawerColorRadioItem('Amber', Colors.amber, _primaryColor, _selectColor),
            buildDrawerColorRadioItem('Teal', Colors.teal, _primaryColor, _selectColor),
118
            const Divider(),
119 120 121
            buildDrawerDirectionRadioItem('Dismiss horizontally', DismissDirection.horizontal, _dismissDirection, _changeDismissDirection, icon: Icons.code),
            buildDrawerDirectionRadioItem('Dismiss left', DismissDirection.endToStart, _dismissDirection, _changeDismissDirection, icon: Icons.arrow_back),
            buildDrawerDirectionRadioItem('Dismiss right', DismissDirection.startToEnd, _dismissDirection, _changeDismissDirection, icon: Icons.arrow_forward),
122
            const Divider(),
123 124 125
            buildFontRadioItem('Left-align text', TextAlign.left, _textAlign, _changeTextAlign, icon: Icons.format_align_left, enabled: !_editable),
            buildFontRadioItem('Center-align text', TextAlign.center, _textAlign, _changeTextAlign, icon: Icons.format_align_center, enabled: !_editable),
            buildFontRadioItem('Right-align text', TextAlign.right, _textAlign, _changeTextAlign, icon: Icons.format_align_right, enabled: !_editable),
126
            const Divider(),
127
            ListTile(
128
              leading: const Icon(Icons.dvr),
129
              onTap: () { debugDumpApp(); debugDumpRenderTree(); },
130
              title: const Text('Dump App to Console'),
131
            ),
132 133 134
          ],
        ),
      ),
135
    );
136 137
  }

138
  String _dismissDirectionText(DismissDirection direction) {
139
    final String s = direction.toString();
140 141 142
    return "dismiss ${s.substring(s.indexOf('.') + 1)}";
  }

Hixie's avatar
Hixie committed
143 144 145
  void _toggleEditable() {
    setState(() {
      _editable = !_editable;
146
      _updateCardSizes();
Hixie's avatar
Hixie committed
147 148 149
    });
  }

150 151 152 153 154 155 156
  void _toggleFixedSizeCards() {
    setState(() {
      _fixedSizeCards = !_fixedSizeCards;
      _initCardModels();
    });
  }

Hans Muller's avatar
Hans Muller committed
157 158 159 160 161 162
  void _toggleSunshine() {
    setState(() {
      _sunshine = !_sunshine;
    });
  }

163 164 165 166 167 168
  void _toggleVaryFontSizes() {
    setState(() {
      _varyFontSizes = !_varyFontSizes;
    });
  }

169
  void _selectColor(MaterialColor? selection) {
170
    setState(() {
171
      _primaryColor = selection!;
172 173 174
    });
  }

175
  void _changeDismissDirection(DismissDirection? newDismissDirection) {
176
    setState(() {
177
      _dismissDirection = newDismissDirection!;
178
    });
Hixie's avatar
Hixie committed
179 180
  }

181
  void _changeTextAlign(TextAlign? newTextAlign) {
Hixie's avatar
Hixie committed
182
    setState(() {
183
      _textAlign = newTextAlign!;
Hixie's avatar
Hixie committed
184
    });
185 186
  }

187
  Widget buildDrawerCheckbox(String label, bool value, void Function() callback, { bool enabled = true }) {
188
    return ListTile(
189
      onTap: enabled ? callback : null,
190 191
      title: Text(label),
      trailing: Checkbox(
192 193
        value: value,
        onChanged: enabled ? (_) { callback(); } : null,
194
      ),
195 196
    );
  }
197

198
  Widget buildDrawerColorRadioItem(String label, MaterialColor itemValue, MaterialColor currentValue, ValueChanged<MaterialColor?> onChanged, { IconData? icon, bool enabled = true }) {
199 200 201
    return ListTile(
      leading: Icon(icon),
      title: Text(label),
202
      onTap: enabled ? () { onChanged(itemValue); } : null,
203
      trailing: Radio<MaterialColor>(
204 205 206
        value: itemValue,
        groupValue: currentValue,
        onChanged: enabled ? onChanged : null,
207
      ),
Hixie's avatar
Hixie committed
208 209 210
    );
  }

211
  Widget buildDrawerDirectionRadioItem(String label, DismissDirection itemValue, DismissDirection currentValue, ValueChanged<DismissDirection?> onChanged, { IconData? icon, bool enabled = true }) {
212 213 214
    return ListTile(
      leading: Icon(icon),
      title: Text(label),
215
      onTap: enabled ? () { onChanged(itemValue); } : null,
216
      trailing: Radio<DismissDirection>(
217 218 219
        value: itemValue,
        groupValue: currentValue,
        onChanged: enabled ? onChanged : null,
220
      ),
Hixie's avatar
Hixie committed
221 222 223
    );
  }

224
  Widget buildFontRadioItem(String label, TextAlign itemValue, TextAlign currentValue, ValueChanged<TextAlign?> onChanged, { IconData? icon, bool enabled = true }) {
225 226 227
    return ListTile(
      leading: Icon(icon),
      title: Text(label),
228
      onTap: enabled ? () { onChanged(itemValue); } : null,
229
      trailing: Radio<TextAlign>(
230 231 232
        value: itemValue,
        groupValue: currentValue,
        onChanged: enabled ? onChanged : null,
233
      ),
234 235 236
    );
  }

237
  AppBar _buildAppBar(BuildContext context) {
238
    return AppBar(
239
      actions: <Widget>[
240
        Text(_dismissDirectionText(_dismissDirection)),
Hans Muller's avatar
Hans Muller committed
241
      ],
242
      flexibleSpace: Container(
243 244
        padding: const EdgeInsets.only(left: 72.0),
        height: 128.0,
245
        alignment: const Alignment(-1.0, 0.5),
246
        child: Text('Swipe Away: ${_cardModels.length}', style: Theme.of(context).primaryTextTheme.titleLarge),
247
      ),
248 249 250
    );
  }

251
  Widget _buildCard(BuildContext context, int index) {
252
    final CardModel cardModel = _cardModels[index];
253 254
    final Widget card = Dismissible(
      key: ObjectKey(cardModel),
255
      direction: _dismissDirection,
256
      onDismissed: (DismissDirection direction) { dismissCard(cardModel); },
257
      child: Card(
258
        color: _primaryColor[cardModel.color],
259
        child: Container(
260
          height: cardModel.height,
261
          padding: const EdgeInsets.all(kCardMargins),
262
          child: _editable ?
263 264 265
            Center(
              child: TextField(
                key: GlobalObjectKey(cardModel),
266
                controller: cardModel.textController,
267
              ),
268
            )
269
          : DefaultTextStyle.merge(
270
              style: cardLabelStyle.copyWith(
271
                fontSize: _varyFontSizes ? 5.0 + index : null
272
              ),
273
              child: Column(
274
                crossAxisAlignment: CrossAxisAlignment.stretch,
275 276
                mainAxisAlignment: MainAxisAlignment.center,
                children: <Widget>[
277
                  Text(cardModel.textController.text, textAlign: _textAlign),
278 279 280 281 282
                ],
              ),
            ),
        ),
      ),
283 284
    );

285
    String backgroundMessage;
286
    switch (_dismissDirection) {
287
      case DismissDirection.horizontal:
288
        backgroundMessage = 'Swipe in either direction';
289
      case DismissDirection.endToStart:
290
        backgroundMessage = 'Swipe left to dismiss';
291
      case DismissDirection.startToEnd:
292
        backgroundMessage = 'Swipe right to dismiss';
293 294 295 296
      case DismissDirection.vertical:
      case DismissDirection.up:
      case DismissDirection.down:
      case DismissDirection.none:
297
        backgroundMessage = 'Unsupported dismissDirection';
298 299
    }

300
    // This icon is wrong in RTL.
301
    Widget leftArrowIcon = const Icon(Icons.arrow_back, size: 36.0);
302
    if (_dismissDirection == DismissDirection.startToEnd) {
303
      leftArrowIcon = Opacity(opacity: 0.1, child: leftArrowIcon);
304
    }
305

306
    // This icon is wrong in RTL.
307
    Widget rightArrowIcon = const Icon(Icons.arrow_forward, size: 36.0);
308
    if (_dismissDirection == DismissDirection.endToStart) {
309
      rightArrowIcon = Opacity(opacity: 0.1, child: rightArrowIcon);
310
    }
311

312
    final ThemeData theme = Theme.of(context);
313
    final TextStyle? backgroundTextStyle = theme.primaryTextTheme.titleLarge;
314

315
    // The background Widget appears behind the Dismissible card when the card
316 317 318 319
    // moves to the left or right. The Positioned widget ensures that the
    // size of the background,card Stack will be based only on the card. The
    // Viewport ensures that when the card's resize animation occurs, the
    // background (text and icons) will just be clipped, not resized.
320 321
    final Widget background = Positioned.fill(
      child: Container(
322
        margin: const EdgeInsets.all(4.0),
323 324
        child: SingleChildScrollView(
          child: Container(
325
            height: cardModel.height,
326
            color: theme.primaryColor,
327
            child: Row(
328 329
              children: <Widget>[
                leftArrowIcon,
330 331
                Expanded(
                  child: Text(backgroundMessage,
332
                    style: backgroundTextStyle,
333 334
                    textAlign: TextAlign.center,
                  ),
335
                ),
336 337 338 339 340 341
                rightArrowIcon,
              ],
            ),
          ),
        ),
      ),
342
    );
343

344
    return IconTheme(
345
      key: cardModel.key,
Adam Barth's avatar
Adam Barth committed
346
      data: const IconThemeData(color: Colors.white),
347
      child: Stack(children: <Widget>[background, card]),
348
    );
349 350
  }

351
  Shader _createShader(Rect bounds) {
352
    return const LinearGradient(
353 354
        begin: Alignment.topCenter,
        end: Alignment.bottomCenter,
355 356
        colors: <Color>[Color(0x00FFFFFF), Color(0xFFFFFFFF)],
        stops: <double>[0.1, 0.35],
Hans Muller's avatar
Hans Muller committed
357
    )
Adam Barth's avatar
Adam Barth committed
358
    .createShader(bounds);
Hans Muller's avatar
Hans Muller committed
359 360
  }

361
  @override
362
  Widget build(BuildContext context) {
363
    Widget cardCollection = ListView.builder(
364 365 366 367
      itemExtent: _fixedSizeCards ? kFixedCardHeight : null,
      itemCount: _cardModels.length,
      itemBuilder: _buildCard,
    );
368

369
    if (_sunshine) {
370
      cardCollection = Stack(
371
        children: <Widget>[
372
          Column(children: <Widget>[Image.network(_sunshineURL)]),
373
          ShaderMask(shaderCallback: _createShader, child: cardCollection),
374
        ],
375 376
      );
    }
Hans Muller's avatar
Hans Muller committed
377

378
    final Widget body = Container(
379
      padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 8.0),
380
      color: _primaryColor.shade50,
381
      child: cardCollection,
382 383
    );

384 385
    return Theme(
      data: ThemeData(
386
        primarySwatch: _primaryColor,
387
      ),
388
      child: Scaffold(
389
        appBar: _buildAppBar(context),
390
        drawer: _buildDrawer(),
391 392
        body: body,
      ),
393 394 395 396 397
    );
  }
}

void main() {
398
  runApp(const MaterialApp(
399
    title: 'Cards',
400
    home: CardCollection(),
401
  ));
402
}