card_collection.dart 15.3 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
import 'package:flutter/material.dart';
6
import 'package:flutter/rendering.dart' show debugDumpRenderTree;
7

8
class CardModel {
9
  CardModel(this.value, this.height) {
10
    inputValue = new InputValue(text: "Item $value");
11
  }
12 13
  int value;
  double height;
14
  int get color => ((value % 9) + 1) * 100;
15
  InputValue inputValue;
Hixie's avatar
Hixie committed
16
  Key get key => new ObjectKey(this);
17 18
}

19 20
class CardCollection extends StatefulComponent {
  CardCollectionState createState() => new CardCollectionState();
21 22
}

23
class CardCollectionState extends State<CardCollection> {
24

25
  static const TextStyle cardLabelStyle =
26
    const TextStyle(color: Colors.white, fontSize: 18.0, fontWeight: FontWeight.bold);
27

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

31 32
  static const kCardMargins = 8.0;

33
  final TextStyle backgroundTextStyle =
34
    Typography.white.title.copyWith(textAlign: TextAlign.center);
35

36
  Map<int, Color> _primaryColor = Colors.deepPurple;
37 38
  List<CardModel> _cardModels;
  DismissDirection _dismissDirection = DismissDirection.horizontal;
Hixie's avatar
Hixie committed
39
  TextStyle _textStyle = new TextStyle(textAlign: TextAlign.center);
Hans Muller's avatar
Hans Muller committed
40
  bool _editable = false;
41 42
  bool _snapToCenter = false;
  bool _fixedSizeCards = false;
Hans Muller's avatar
Hans Muller committed
43
  bool _sunshine = false;
44
  bool _varyFontSizes = false;
45
  InvalidatorCallback _invalidator;
46

47
  void _initVariableSizedCardModels() {
48 49 50 51 52
    List<double> cardHeights = <double>[
      48.0, 63.0, 82.0, 146.0, 60.0, 55.0, 84.0, 96.0, 50.0,
      48.0, 63.0, 82.0, 146.0, 60.0, 55.0, 84.0, 96.0, 50.0,
      48.0, 63.0, 82.0, 146.0, 60.0, 55.0, 84.0, 96.0, 50.0
    ];
Hixie's avatar
Hixie committed
53 54 55 56
    _cardModels = new List<CardModel>.generate(
      cardHeights.length,
      (int i) => new CardModel(i, cardHeights[i])
    );
57 58
  }

59 60 61
  void _initFixedSizedCardModels() {
    const int cardCount = 27;
    const double cardHeight = 100.0;
Hixie's avatar
Hixie committed
62 63 64 65
    _cardModels = new List<CardModel>.generate(
      cardCount,
      (int i) => new CardModel(i, cardHeight)
    );
66 67 68 69 70 71 72 73 74
  }

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

75 76
  void initState() {
    super.initState();
77 78 79
    _initCardModels();
  }

80 81
  double _variableSizeToSnapOffset(double scrollOffset) {
    double cumulativeHeight = 0.0;
Hixie's avatar
Hixie committed
82
    List<double> cumulativeHeights = _cardModels.map((CardModel card) {
83
      cumulativeHeight += card.height + kCardMargins;
84 85 86 87 88
      return cumulativeHeight;
    })
    .toList();

    double offsetForIndex(int i) {
89
      return (kCardMargins + _cardModels[i].height) / 2.0 + ((i == 0) ? 0.0 : cumulativeHeights[i - 1]);
90 91 92 93 94 95 96 97 98 99 100 101
    }

    for (int i = 0; i <  cumulativeHeights.length; i++) {
      if (cumulativeHeights[i] >= scrollOffset)
        return offsetForIndex(i);
    }
    return offsetForIndex(cumulativeHeights.length - 1);
  }

  double _fixedSizeToSnapOffset(double scrollOffset) {
    double cardHeight = _cardModels[0].height;
    int cardIndex = (scrollOffset.clamp(0.0, cardHeight * (_cardModels.length - 1)) / cardHeight).floor();
102
    return cardIndex * cardHeight + cardHeight * 0.5;
103 104
  }

105 106 107 108 109
  double _toSnapOffset(double scrollOffset, Size containerSize) {
    double halfHeight = containerSize.height / 2.0;
    scrollOffset += halfHeight;
    double result = _fixedSizeCards ? _fixedSizeToSnapOffset(scrollOffset) : _variableSizeToSnapOffset(scrollOffset);
    return result - halfHeight;
110 111
  }

112
  void dismissCard(CardModel card) {
113
    if (_cardModels.contains(card)) {
114
      setState(() {
115
        _cardModels.remove(card);
116 117 118 119
      });
    }
  }

120 121
  Widget _buildDrawer() {
    return new Drawer(
122 123
      child: new IconTheme(
        data: const IconThemeData(color: IconThemeColor.black),
124
        child: new Block(children: <Widget>[
125
          new DrawerHeader(child: new Text('Options')),
Hixie's avatar
Hixie committed
126
          buildDrawerCheckbox("Make card labels editable", _editable, _toggleEditable),
127 128 129
          buildDrawerCheckbox("Snap fling scrolls to center", _snapToCenter, _toggleSnapToCenter),
          buildDrawerCheckbox("Fixed size cards", _fixedSizeCards, _toggleFixedSizeCards),
          buildDrawerCheckbox("Let the sun shine", _sunshine, _toggleSunshine),
130
          buildDrawerCheckbox("Vary font sizes", _varyFontSizes, _toggleVaryFontSizes, enabled: !_editable),
131
          new DrawerDivider(),
Hixie's avatar
Hixie committed
132 133 134 135
          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),
136
          new DrawerDivider(),
Hixie's avatar
Hixie committed
137 138 139
          buildDrawerDirectionRadioItem("Dismiss horizontally", DismissDirection.horizontal, _dismissDirection, _changeDismissDirection, icon: 'action/code'),
          buildDrawerDirectionRadioItem("Dismiss left", DismissDirection.left, _dismissDirection, _changeDismissDirection, icon: 'navigation/arrow_back'),
          buildDrawerDirectionRadioItem("Dismiss right", DismissDirection.right, _dismissDirection, _changeDismissDirection, icon: 'navigation/arrow_forward'),
Hixie's avatar
Hixie committed
140 141 142 143 144 145 146 147 148 149
          new DrawerDivider(),
          buildFontRadioItem("Left-align text", new TextStyle(textAlign: TextAlign.left), _textStyle, _changeTextStyle, icon: 'editor/format_align_left', enabled: !_editable),
          buildFontRadioItem("Center-align text", new TextStyle(textAlign: TextAlign.center), _textStyle, _changeTextStyle, icon: 'editor/format_align_center', enabled: !_editable),
          buildFontRadioItem("Right-align text", new TextStyle(textAlign: TextAlign.right), _textStyle, _changeTextStyle, icon: 'editor/format_align_right', enabled: !_editable),
          new DrawerDivider(),
          new DrawerItem(
            icon: 'device/dvr',
            onPressed: () { debugDumpApp(); debugDumpRenderTree(); },
            child: new Text('Dump App to Console')
          ),
150 151 152
        ])
      )
    );
153 154
  }

155
  String _dismissDirectionText(DismissDirection direction) {
156 157 158 159
    String s = direction.toString();
    return "dismiss ${s.substring(s.indexOf('.') + 1)}";
  }

Hixie's avatar
Hixie committed
160 161 162 163 164 165
  void _toggleEditable() {
    setState(() {
      _editable = !_editable;
    });
  }

166 167 168 169 170 171 172 173 174 175 176 177 178
  void _toggleFixedSizeCards() {
    setState(() {
      _fixedSizeCards = !_fixedSizeCards;
      _initCardModels();
    });
  }

  void _toggleSnapToCenter() {
    setState(() {
      _snapToCenter = !_snapToCenter;
    });
  }

Hans Muller's avatar
Hans Muller committed
179 180 181 182 183 184
  void _toggleSunshine() {
    setState(() {
      _sunshine = !_sunshine;
    });
  }

185 186 187 188 189 190
  void _toggleVaryFontSizes() {
    setState(() {
      _varyFontSizes = !_varyFontSizes;
    });
  }

Hixie's avatar
Hixie committed
191
  void _selectColor(Map<int, Color> selection) {
192 193 194 195 196
    setState(() {
      _primaryColor = selection;
    });
  }

197
  void _changeDismissDirection(DismissDirection newDismissDirection) {
198 199 200
    setState(() {
      _dismissDirection = newDismissDirection;
    });
Hixie's avatar
Hixie committed
201 202 203 204 205 206
  }

  void _changeTextStyle(TextStyle newTextStyle) {
    setState(() {
      _textStyle = newTextStyle;
    });
207 208
  }

209
  Widget buildDrawerCheckbox(String label, bool value, void callback(), { bool enabled: true }) {
210
    return new DrawerItem(
211
      onPressed: enabled ? callback : null,
212 213 214 215 216 217 218 219 220
      child: new Row(
        children: <Widget>[
          new Flexible(child: new Text(label)),
          new Checkbox(
            value: value,
            onChanged: enabled ? (_) { callback(); } : null
          )
        ]
      )
221 222
    );
  }
223

Hixie's avatar
Hixie committed
224
  Widget buildDrawerColorRadioItem(String label, Map<int, Color> itemValue, Map<int, Color> currentValue, ValueChanged<Map<int, Color>> onChanged, { String icon, bool enabled: true }) {
225 226
    return new DrawerItem(
      icon: icon,
Hixie's avatar
Hixie committed
227
      onPressed: enabled ? () { onChanged(itemValue); } : null,
228 229 230 231 232 233 234 235 236 237
      child: new Row(
        children: <Widget>[
          new Flexible(child: new Text(label)),
          new Radio<Map<int, Color>>(
            value: itemValue,
            groupValue: currentValue,
            onChanged: enabled ? onChanged : null
          )
        ]
      )
Hixie's avatar
Hixie committed
238 239 240
    );
  }

Hixie's avatar
Hixie committed
241
  Widget buildDrawerDirectionRadioItem(String label, DismissDirection itemValue, DismissDirection currentValue, ValueChanged<DismissDirection> onChanged, { String icon, bool enabled: true }) {
Hixie's avatar
Hixie committed
242 243
    return new DrawerItem(
      icon: icon,
Hixie's avatar
Hixie committed
244
      onPressed: enabled ? () { onChanged(itemValue); } : null,
245 246 247 248 249 250 251 252 253 254
      child: new Row(
        children: <Widget>[
          new Flexible(child: new Text(label)),
          new Radio<DismissDirection>(
            value: itemValue,
            groupValue: currentValue,
            onChanged: enabled ? onChanged : null
          )
        ]
      )
Hixie's avatar
Hixie committed
255 256 257 258 259 260 261
    );
  }

  Widget buildFontRadioItem(String label, TextStyle itemValue, TextStyle currentValue, ValueChanged<TextStyle> onChanged, { String icon, bool enabled: true }) {
    return new DrawerItem(
      icon: icon,
      onPressed: enabled ? () { onChanged(itemValue); } : null,
262 263 264 265 266 267 268 269 270 271
      child: new Row(
        children: <Widget>[
          new Flexible(child: new Text(label)),
          new Radio<TextStyle>(
            value: itemValue,
            groupValue: currentValue,
            onChanged: enabled ? onChanged : null
          )
        ]
      )
272 273 274
    );
  }

275
  Widget _buildToolBar(BuildContext context) {
276
    return new ToolBar(
Hixie's avatar
Hixie committed
277
      right: <Widget>[
278
        new Text(_dismissDirectionText(_dismissDirection))
Hans Muller's avatar
Hans Muller committed
279
      ],
280 281 282 283 284 285 286 287 288 289
      flexibleSpace: (_) {
        return new Container(
          padding: const EdgeDims.only(left: 72.0),
          height: 128.0,
          child: new Align(
            alignment: const FractionalOffset(0.0, 0.75),
            child: new Text('Swipe Away: ${_cardModels.length}', style: Theme.of(context).primaryTextTheme.title)
          )
        );
      }
290 291 292
    );
  }

293
  Widget _buildCard(BuildContext context, int index) {
294
    if (index >= _cardModels.length)
295
      return null;
296

297
    CardModel cardModel = _cardModels[index];
298
    Widget card = new Dismissable(
299
      direction: _dismissDirection,
Hixie's avatar
Hixie committed
300
      onResized: () { _invalidator(<int>[index]); },
301
      onDismissed: () { dismissCard(cardModel); },
302
      child: new Card(
303
        color: _primaryColor[cardModel.color],
304
        child: new Container(
305
          height: cardModel.height,
306
          padding: const EdgeDims.all(kCardMargins),
307
          child: _editable ?
Hixie's avatar
Hixie committed
308
            new Center(
309 310
              child: new Input(
                key: new GlobalObjectKey(cardModel),
311 312 313 314 315
                value: cardModel.inputValue,
                onChanged: (InputValue value) {
                  setState(() {
                    cardModel.inputValue = value;
                  });
316 317 318
                }
              )
            )
Hixie's avatar
Hixie committed
319
          : new DefaultTextStyle(
320
              style: DefaultTextStyle.of(context).merge(cardLabelStyle).merge(_textStyle).copyWith(
321
                fontSize: _varyFontSizes ? _cardModels.length.toDouble() : null
322
              ),
323 324
              child: new Column(
                children: <Widget>[
325
                  new Text(cardModel.inputValue.text)
Hixie's avatar
Hixie committed
326 327 328 329 330
                ],
                alignItems: FlexAlignItems.stretch,
                justifyContent: FlexJustifyContent.center
              )
            )
331 332 333 334
        )
      )
    );

335 336 337 338 339 340 341 342 343 344 345 346 347 348 349
    String backgroundMessage;
    switch(_dismissDirection) {
      case DismissDirection.horizontal:
        backgroundMessage = "Swipe in either direction";
        break;
      case DismissDirection.left:
        backgroundMessage = "Swipe left to dismiss";
        break;
      case DismissDirection.right:
        backgroundMessage = "Swipe right to dismiss";
        break;
      default:
        backgroundMessage = "Unsupported dismissDirection";
    }

350
    Widget leftArrowIcon =  new Icon(icon: 'navigation/arrow_back', size: IconSize.s36);
351 352 353
    if (_dismissDirection == DismissDirection.right)
      leftArrowIcon = new Opacity(opacity: 0.1, child: leftArrowIcon);

354
    Widget rightArrowIcon =  new Icon(icon: 'navigation/arrow_forward', size: IconSize.s36);
355 356
    if (_dismissDirection == DismissDirection.left)
      rightArrowIcon = new Opacity(opacity: 0.1, child: rightArrowIcon);
357 358 359 360 361 362 363 364

    // The background Widget appears behind the Dismissable card when the card
    // 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.
    Widget background = new Positioned(
      top: 0.0,
365 366
      right: 0.0,
      bottom: 0.0,
367 368 369 370 371 372
      left: 0.0,
      child: new Container(
        margin: const EdgeDims.all(4.0),
        child: new Viewport(
          child: new Container(
            height: cardModel.height,
373
            decoration: new BoxDecoration(backgroundColor: Theme.of(context).primaryColor),
374 375 376 377 378 379 380
            child: new Row(
              children: <Widget>[
                leftArrowIcon,
                new Flexible(child: new Text(backgroundMessage, style: backgroundTextStyle)),
                rightArrowIcon
              ]
            )
381
          )
382 383 384
        )
      )
    );
385

386 387 388
    return new IconTheme(
      key: cardModel.key,
      data: const IconThemeData(color: IconThemeColor.white),
389
      child: new Stack(children: <Widget>[background, card])
390
    );
391 392
  }

393
  Shader _createShader(Rect bounds) {
Hans Muller's avatar
Hans Muller committed
394
    return new LinearGradient(
395 396
        begin: bounds.topLeft,
        end: bounds.bottomLeft,
Hixie's avatar
Hixie committed
397 398
        colors: <Color>[const Color(0x00FFFFFF), const Color(0xFFFFFFFF)],
        stops: <double>[0.1, 0.35]
Hans Muller's avatar
Hans Muller committed
399 400 401 402
    )
    .createShader();
  }

403
  Widget build(BuildContext context) {
404 405
    Widget cardCollection;
    if (_fixedSizeCards) {
406
      cardCollection = new ScrollableList (
407
        snapOffsetCallback: _snapToCenter ? _toSnapOffset : null,
408 409
        itemExtent: _cardModels[0].height,
        children: _cardModels.map((CardModel card) => _buildCard(context, card.value))
410 411 412
      );
    } else {
      cardCollection = new ScrollableMixedWidgetList(
413
        builder: _buildCard,
414
        token: _cardModels.length,
415
        snapOffsetCallback: _snapToCenter ? _toSnapOffset : null,
416
        onInvalidatorAvailable: (InvalidatorCallback callback) { _invalidator = callback; }
417 418 419
      );
    }

420 421 422 423 424 425 426 427
    if (_sunshine) {
      cardCollection = new Stack(
        children: <Widget>[
          new Column(children: <Widget>[new NetworkImage(src: _sunshineURL)]),
          new ShaderMask(child: cardCollection, shaderCallback: _createShader)
        ]
      );
    }
Hans Muller's avatar
Hans Muller committed
428

429 430
    Widget body = new Container(
      padding: const EdgeDims.symmetric(vertical: 12.0, horizontal: 8.0),
431
      decoration: new BoxDecoration(backgroundColor: _primaryColor[50]),
432
      child: cardCollection
433 434
    );

435 436 437
    if (_snapToCenter) {
      Widget indicator = new IgnorePointer(
        child: new Align(
438
          alignment: const FractionalOffset(0.0, 0.5),
439 440 441 442 443 444
          child: new Container(
            height: 1.0,
            decoration: new BoxDecoration(backgroundColor: const Color(0x80FFFFFF))
          )
        )
      );
445
      body = new Stack(children: <Widget>[body, indicator]);
446 447
    }

448 449 450 451 452
    return new Theme(
      data: new ThemeData(
        primarySwatch: _primaryColor
      ),
      child: new Scaffold(
453
        toolBar: _buildToolBar(context),
454
        drawer: _buildDrawer(),
455 456
        body: body
      )
457 458 459 460 461
    );
  }
}

void main() {
Adam Barth's avatar
Adam Barth committed
462
  runApp(new MaterialApp(
463
    title: 'Cards',
Hixie's avatar
Hixie committed
464
    routes: <String, RouteBuilder>{
Adam Barth's avatar
Adam Barth committed
465
      '/': (RouteArguments args) => new CardCollection(),
466 467
    }
  ));
468
}