card_collection.dart 12.7 KB
Newer Older
1 2 3 4
// Copyright 2015 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 6
import 'dart:math';

7
import 'package:flutter/material.dart';
8
import 'package:flutter/rendering.dart' show debugDumpRenderTree;
9

10
class CardModel {
11
  CardModel(this.value, this.height) {
12
    textController = TextEditingController(text: 'Item $value');
13
  }
14 15
  int value;
  double height;
16
  int get color => ((value % 9) + 1) * 100;
17
  TextEditingController textController;
18
  Key get key => ObjectKey(this);
19 20
}

21
class CardCollection extends StatefulWidget {
22
  @override
23
  CardCollectionState createState() => CardCollectionState();
24 25
}

26
class CardCollectionState extends State<CardCollection> {
27

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

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

34
  static const double kCardMargins = 8.0;
35
  static const double kFixedCardHeight = 100.0;
36
  static const List<double> _cardHeights = <double>[
37 38 39 40
    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,
  ];
41

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

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

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

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

  void _initCardModels() {
    if (_fixedSizeCards)
      _initFixedSizedCardModels();
    else
      _initVariableSizedCardModels();
  }

85
  @override
86 87
  void initState() {
    super.initState();
88 89 90
    _initCardModels();
  }

91
  void dismissCard(CardModel card) {
92
    if (_cardModels.contains(card)) {
93
      setState(() {
94
        _cardModels.remove(card);
95 96 97 98
      });
    }
  }

99
  Widget _buildDrawer() {
100 101
    return Drawer(
      child: IconTheme(
Adam Barth's avatar
Adam Barth committed
102
        data: const IconThemeData(color: Colors.black),
103
        child: ListView(
104
          children: <Widget>[
105
            const DrawerHeader(child: Center(child: Text('Options'))),
106 107 108 109
            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),
110
            const Divider(),
111 112 113 114
            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),
115
            const Divider(),
116 117 118
            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),
119
            const Divider(),
120 121 122
            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),
123
            const Divider(),
124
            ListTile(
125
              leading: const Icon(Icons.dvr),
126
              onTap: () { debugDumpApp(); debugDumpRenderTree(); },
127
              title: const Text('Dump App to Console'),
128
            ),
129 130 131
          ],
        ),
      ),
132
    );
133 134
  }

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

Hixie's avatar
Hixie committed
140 141 142
  void _toggleEditable() {
    setState(() {
      _editable = !_editable;
143
      _updateCardSizes();
Hixie's avatar
Hixie committed
144 145 146
    });
  }

147 148 149 150 151 152 153
  void _toggleFixedSizeCards() {
    setState(() {
      _fixedSizeCards = !_fixedSizeCards;
      _initCardModels();
    });
  }

Hans Muller's avatar
Hans Muller committed
154 155 156 157 158 159
  void _toggleSunshine() {
    setState(() {
      _sunshine = !_sunshine;
    });
  }

160 161 162 163 164 165
  void _toggleVaryFontSizes() {
    setState(() {
      _varyFontSizes = !_varyFontSizes;
    });
  }

166
  void _selectColor(MaterialColor selection) {
167 168 169 170 171
    setState(() {
      _primaryColor = selection;
    });
  }

172
  void _changeDismissDirection(DismissDirection newDismissDirection) {
173 174 175
    setState(() {
      _dismissDirection = newDismissDirection;
    });
Hixie's avatar
Hixie committed
176 177
  }

178
  void _changeTextAlign(TextAlign newTextAlign) {
Hixie's avatar
Hixie committed
179
    setState(() {
180
      _textAlign = newTextAlign;
Hixie's avatar
Hixie committed
181
    });
182 183
  }

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

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

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

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

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

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

282
    String backgroundMessage;
283
    switch (_dismissDirection) {
284
      case DismissDirection.horizontal:
285
        backgroundMessage = 'Swipe in either direction';
286
        break;
287
      case DismissDirection.endToStart:
288
        backgroundMessage = 'Swipe left to dismiss';
289
        break;
290
      case DismissDirection.startToEnd:
291
        backgroundMessage = 'Swipe right to dismiss';
292 293
        break;
      default:
294
        backgroundMessage = 'Unsupported dismissDirection';
295 296
    }

297
    // TODO(abarth): This icon is wrong in RTL.
298
    Widget leftArrowIcon = const Icon(Icons.arrow_back, size: 36.0);
299
    if (_dismissDirection == DismissDirection.startToEnd)
300
      leftArrowIcon = Opacity(opacity: 0.1, child: leftArrowIcon);
301

302
      // TODO(abarth): This icon is wrong in RTL.
303
    Widget rightArrowIcon = const Icon(Icons.arrow_forward, size: 36.0);
304
    if (_dismissDirection == DismissDirection.endToStart)
305
      rightArrowIcon = Opacity(opacity: 0.1, child: rightArrowIcon);
306

307 308 309
    final ThemeData theme = Theme.of(context);
    final TextStyle backgroundTextStyle = theme.primaryTextTheme.title;

310
    // The background Widget appears behind the Dismissible card when the card
311 312 313 314
    // 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.
315 316
    final Widget background = Positioned.fill(
      child: Container(
317
        margin: const EdgeInsets.all(4.0),
318 319
        child: SingleChildScrollView(
          child: Container(
320
            height: cardModel.height,
321
            color: theme.primaryColor,
322
            child: Row(
323 324
              children: <Widget>[
                leftArrowIcon,
325 326
                Expanded(
                  child: Text(backgroundMessage,
327
                    style: backgroundTextStyle,
328 329
                    textAlign: TextAlign.center,
                  ),
330
                ),
331 332 333 334 335 336
                rightArrowIcon,
              ],
            ),
          ),
        ),
      ),
337
    );
338

339
    return IconTheme(
340
      key: cardModel.key,
Adam Barth's avatar
Adam Barth committed
341
      data: const IconThemeData(color: Colors.white),
342
      child: Stack(children: <Widget>[background, card]),
343
    );
344 345
  }

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

356
  @override
357
  Widget build(BuildContext context) {
358
    Widget cardCollection = ListView.builder(
359 360 361 362
      itemExtent: _fixedSizeCards ? kFixedCardHeight : null,
      itemCount: _cardModels.length,
      itemBuilder: _buildCard,
    );
363

364
    if (_sunshine) {
365
      cardCollection = Stack(
366
        children: <Widget>[
367 368
          Column(children: <Widget>[Image.network(_sunshineURL)]),
          ShaderMask(child: cardCollection, shaderCallback: _createShader),
369
        ],
370 371
      );
    }
Hans Muller's avatar
Hans Muller committed
372

373
    final Widget body = Container(
374
      padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 8.0),
375
      color: _primaryColor.shade50,
376
      child: cardCollection,
377 378
    );

379 380
    return Theme(
      data: ThemeData(
381
        primarySwatch: _primaryColor,
382
      ),
383
      child: Scaffold(
384
        appBar: _buildAppBar(context),
385
        drawer: _buildDrawer(),
386 387
        body: body,
      ),
388 389 390 391 392
    );
  }
}

void main() {
393
  runApp(MaterialApp(
394
    title: 'Cards',
395
    home: CardCollection(),
396
  ));
397
}