|  | // 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. | 
|  |  | 
|  | import 'package:flutter/material.dart'; | 
|  | import 'package:flutter/rendering.dart' show debugDumpRenderTree; | 
|  |  | 
|  | class CardModel { | 
|  | CardModel(this.value, this.height) { | 
|  | textController = new TextEditingController(text: 'Item $value'); | 
|  | } | 
|  | int value; | 
|  | double height; | 
|  | int get color => ((value % 9) + 1) * 100; | 
|  | TextEditingController textController; | 
|  | Key get key => new ObjectKey(this); | 
|  | } | 
|  |  | 
|  | class CardCollection extends StatefulWidget { | 
|  | @override | 
|  | CardCollectionState createState() => new CardCollectionState(); | 
|  | } | 
|  |  | 
|  | class CardCollectionState extends State<CardCollection> { | 
|  |  | 
|  | static const TextStyle cardLabelStyle = | 
|  | const TextStyle(color: Colors.white, fontSize: 18.0, fontWeight: FontWeight.bold); | 
|  |  | 
|  | // TODO(hansmuller): need a local image asset | 
|  | static const String _sunshineURL = "http://www.walltor.com/images/wallpaper/good-morning-sunshine-58540.jpg"; | 
|  |  | 
|  | static const double kCardMargins = 8.0; | 
|  | static const double kFixedCardHeight = 100.0; | 
|  |  | 
|  | MaterialColor _primaryColor = Colors.deepPurple; | 
|  | List<CardModel> _cardModels; | 
|  | DismissDirection _dismissDirection = DismissDirection.horizontal; | 
|  | TextAlign _textAlign = TextAlign.center; | 
|  | bool _editable = false; | 
|  | bool _fixedSizeCards = false; | 
|  | bool _sunshine = false; | 
|  | bool _varyFontSizes = false; | 
|  |  | 
|  | void _initVariableSizedCardModels() { | 
|  | final 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, | 
|  | ]; | 
|  | _cardModels = new List<CardModel>.generate( | 
|  | cardHeights.length, | 
|  | (int i) => new CardModel(i, cardHeights[i]) | 
|  | ); | 
|  | } | 
|  |  | 
|  | void _initFixedSizedCardModels() { | 
|  | const int cardCount = 27; | 
|  | _cardModels = new List<CardModel>.generate( | 
|  | cardCount, | 
|  | (int i) => new CardModel(i, kFixedCardHeight), | 
|  | ); | 
|  | } | 
|  |  | 
|  | void _initCardModels() { | 
|  | if (_fixedSizeCards) | 
|  | _initFixedSizedCardModels(); | 
|  | else | 
|  | _initVariableSizedCardModels(); | 
|  | } | 
|  |  | 
|  | @override | 
|  | void initState() { | 
|  | super.initState(); | 
|  | _initCardModels(); | 
|  | } | 
|  |  | 
|  | void dismissCard(CardModel card) { | 
|  | if (_cardModels.contains(card)) { | 
|  | setState(() { | 
|  | _cardModels.remove(card); | 
|  | }); | 
|  | } | 
|  | } | 
|  |  | 
|  | Widget _buildDrawer() { | 
|  | return new Drawer( | 
|  | child: new IconTheme( | 
|  | data: const IconThemeData(color: Colors.black), | 
|  | child: new ListView( | 
|  | children: <Widget>[ | 
|  | const DrawerHeader(child: const Center(child: const Text('Options'))), | 
|  | 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), | 
|  | const Divider(), | 
|  | 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), | 
|  | const Divider(), | 
|  | 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), | 
|  | const Divider(), | 
|  | 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), | 
|  | const Divider(), | 
|  | new ListTile( | 
|  | leading: const Icon(Icons.dvr), | 
|  | onTap: () { debugDumpApp(); debugDumpRenderTree(); }, | 
|  | title: const Text('Dump App to Console'), | 
|  | ), | 
|  | ], | 
|  | ), | 
|  | ), | 
|  | ); | 
|  | } | 
|  |  | 
|  | String _dismissDirectionText(DismissDirection direction) { | 
|  | final String s = direction.toString(); | 
|  | return "dismiss ${s.substring(s.indexOf('.') + 1)}"; | 
|  | } | 
|  |  | 
|  | void _toggleEditable() { | 
|  | setState(() { | 
|  | _editable = !_editable; | 
|  | }); | 
|  | } | 
|  |  | 
|  | void _toggleFixedSizeCards() { | 
|  | setState(() { | 
|  | _fixedSizeCards = !_fixedSizeCards; | 
|  | _initCardModels(); | 
|  | }); | 
|  | } | 
|  |  | 
|  | void _toggleSunshine() { | 
|  | setState(() { | 
|  | _sunshine = !_sunshine; | 
|  | }); | 
|  | } | 
|  |  | 
|  | void _toggleVaryFontSizes() { | 
|  | setState(() { | 
|  | _varyFontSizes = !_varyFontSizes; | 
|  | }); | 
|  | } | 
|  |  | 
|  | void _selectColor(MaterialColor selection) { | 
|  | setState(() { | 
|  | _primaryColor = selection; | 
|  | }); | 
|  | } | 
|  |  | 
|  | void _changeDismissDirection(DismissDirection newDismissDirection) { | 
|  | setState(() { | 
|  | _dismissDirection = newDismissDirection; | 
|  | }); | 
|  | } | 
|  |  | 
|  | void _changeTextAlign(TextAlign newTextAlign) { | 
|  | setState(() { | 
|  | _textAlign = newTextAlign; | 
|  | }); | 
|  | } | 
|  |  | 
|  | Widget buildDrawerCheckbox(String label, bool value, void callback(), { bool enabled: true }) { | 
|  | return new ListTile( | 
|  | onTap: enabled ? callback : null, | 
|  | title: new Text(label), | 
|  | trailing: new Checkbox( | 
|  | value: value, | 
|  | onChanged: enabled ? (_) { callback(); } : null, | 
|  | ), | 
|  | ); | 
|  | } | 
|  |  | 
|  | Widget buildDrawerColorRadioItem(String label, MaterialColor itemValue, MaterialColor currentValue, ValueChanged<MaterialColor> onChanged, { IconData icon, bool enabled: true }) { | 
|  | return new ListTile( | 
|  | leading: new Icon(icon), | 
|  | title: new Text(label), | 
|  | onTap: enabled ? () { onChanged(itemValue); } : null, | 
|  | trailing: new Radio<MaterialColor>( | 
|  | value: itemValue, | 
|  | groupValue: currentValue, | 
|  | onChanged: enabled ? onChanged : null, | 
|  | ), | 
|  | ); | 
|  | } | 
|  |  | 
|  | Widget buildDrawerDirectionRadioItem(String label, DismissDirection itemValue, DismissDirection currentValue, ValueChanged<DismissDirection> onChanged, { IconData icon, bool enabled: true }) { | 
|  | return new ListTile( | 
|  | leading: new Icon(icon), | 
|  | title: new Text(label), | 
|  | onTap: enabled ? () { onChanged(itemValue); } : null, | 
|  | trailing: new Radio<DismissDirection>( | 
|  | value: itemValue, | 
|  | groupValue: currentValue, | 
|  | onChanged: enabled ? onChanged : null, | 
|  | ), | 
|  | ); | 
|  | } | 
|  |  | 
|  | Widget buildFontRadioItem(String label, TextAlign itemValue, TextAlign currentValue, ValueChanged<TextAlign> onChanged, { IconData icon, bool enabled: true }) { | 
|  | return new ListTile( | 
|  | leading: new Icon(icon), | 
|  | title: new Text(label), | 
|  | onTap: enabled ? () { onChanged(itemValue); } : null, | 
|  | trailing: new Radio<TextAlign>( | 
|  | value: itemValue, | 
|  | groupValue: currentValue, | 
|  | onChanged: enabled ? onChanged : null, | 
|  | ), | 
|  | ); | 
|  | } | 
|  |  | 
|  | Widget _buildAppBar(BuildContext context) { | 
|  | return new AppBar( | 
|  | actions: <Widget>[ | 
|  | new Text(_dismissDirectionText(_dismissDirection)) | 
|  | ], | 
|  | flexibleSpace: new Container( | 
|  | padding: const EdgeInsets.only(left: 72.0), | 
|  | height: 128.0, | 
|  | alignment: const FractionalOffset(0.0, 0.75), | 
|  | child: new Text('Swipe Away: ${_cardModels.length}', style: Theme.of(context).primaryTextTheme.title), | 
|  | ), | 
|  | ); | 
|  | } | 
|  |  | 
|  | Widget _buildCard(BuildContext context, int index) { | 
|  | final CardModel cardModel = _cardModels[index]; | 
|  | final Widget card = new Dismissible( | 
|  | key: new ObjectKey(cardModel), | 
|  | direction: _dismissDirection, | 
|  | onDismissed: (DismissDirection direction) { dismissCard(cardModel); }, | 
|  | child: new Card( | 
|  | color: _primaryColor[cardModel.color], | 
|  | child: new Container( | 
|  | height: cardModel.height, | 
|  | padding: const EdgeInsets.all(kCardMargins), | 
|  | child: _editable ? | 
|  | new Center( | 
|  | child: new TextField( | 
|  | key: new GlobalObjectKey(cardModel), | 
|  | controller: cardModel.textController, | 
|  | ), | 
|  | ) | 
|  | : DefaultTextStyle.merge( | 
|  | style: cardLabelStyle.copyWith( | 
|  | fontSize: _varyFontSizes ? 5.0 + index : null | 
|  | ), | 
|  | child: new Column( | 
|  | crossAxisAlignment: CrossAxisAlignment.stretch, | 
|  | mainAxisAlignment: MainAxisAlignment.center, | 
|  | children: <Widget>[ | 
|  | new Text(cardModel.textController.text, textAlign: _textAlign), | 
|  | ], | 
|  | ), | 
|  | ), | 
|  | ), | 
|  | ), | 
|  | ); | 
|  |  | 
|  | String backgroundMessage; | 
|  | switch(_dismissDirection) { | 
|  | case DismissDirection.horizontal: | 
|  | backgroundMessage = "Swipe in either direction"; | 
|  | break; | 
|  | case DismissDirection.endToStart: | 
|  | backgroundMessage = "Swipe left to dismiss"; | 
|  | break; | 
|  | case DismissDirection.startToEnd: | 
|  | backgroundMessage = "Swipe right to dismiss"; | 
|  | break; | 
|  | default: | 
|  | backgroundMessage = "Unsupported dismissDirection"; | 
|  | } | 
|  |  | 
|  | // TODO(abarth): This icon is wrong in RTL. | 
|  | Widget leftArrowIcon =  const Icon(Icons.arrow_back, size: 36.0); | 
|  | if (_dismissDirection == DismissDirection.startToEnd) | 
|  | leftArrowIcon = new Opacity(opacity: 0.1, child: leftArrowIcon); | 
|  |  | 
|  | // TODO(abarth): This icon is wrong in RTL. | 
|  | Widget rightArrowIcon =  const Icon(Icons.arrow_forward, size: 36.0); | 
|  | if (_dismissDirection == DismissDirection.endToStart) | 
|  | rightArrowIcon = new Opacity(opacity: 0.1, child: rightArrowIcon); | 
|  |  | 
|  | final ThemeData theme = Theme.of(context); | 
|  | final TextStyle backgroundTextStyle = theme.primaryTextTheme.title; | 
|  |  | 
|  | // The background Widget appears behind the Dismissible 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. | 
|  | final Widget background = new Positioned.fill( | 
|  | child: new Container( | 
|  | margin: const EdgeInsets.all(4.0), | 
|  | child: new SingleChildScrollView( | 
|  | child: new Container( | 
|  | height: cardModel.height, | 
|  | color: theme.primaryColor, | 
|  | child: new Row( | 
|  | children: <Widget>[ | 
|  | leftArrowIcon, | 
|  | new Expanded( | 
|  | child: new Text(backgroundMessage, | 
|  | style: backgroundTextStyle, | 
|  | textAlign: TextAlign.center, | 
|  | ), | 
|  | ), | 
|  | rightArrowIcon, | 
|  | ], | 
|  | ), | 
|  | ), | 
|  | ), | 
|  | ), | 
|  | ); | 
|  |  | 
|  | return new IconTheme( | 
|  | key: cardModel.key, | 
|  | data: const IconThemeData(color: Colors.white), | 
|  | child: new Stack(children: <Widget>[background, card]), | 
|  | ); | 
|  | } | 
|  |  | 
|  | Shader _createShader(Rect bounds) { | 
|  | return new LinearGradient( | 
|  | begin: FractionalOffset.topLeft, | 
|  | end: FractionalOffset.bottomLeft, | 
|  | colors: <Color>[const Color(0x00FFFFFF), const Color(0xFFFFFFFF)], | 
|  | stops: <double>[0.1, 0.35], | 
|  | ) | 
|  | .createShader(bounds); | 
|  | } | 
|  |  | 
|  | @override | 
|  | Widget build(BuildContext context) { | 
|  | Widget cardCollection = new ListView.builder( | 
|  | itemExtent: _fixedSizeCards ? kFixedCardHeight : null, | 
|  | itemCount: _cardModels.length, | 
|  | itemBuilder: _buildCard, | 
|  | ); | 
|  |  | 
|  | if (_sunshine) { | 
|  | cardCollection = new Stack( | 
|  | children: <Widget>[ | 
|  | new Column(children: <Widget>[new Image.network(_sunshineURL)]), | 
|  | new ShaderMask(child: cardCollection, shaderCallback: _createShader), | 
|  | ], | 
|  | ); | 
|  | } | 
|  |  | 
|  | final Widget body = new Container( | 
|  | padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 8.0), | 
|  | color: _primaryColor.shade50, | 
|  | child: cardCollection, | 
|  | ); | 
|  |  | 
|  | return new Theme( | 
|  | data: new ThemeData( | 
|  | primarySwatch: _primaryColor, | 
|  | ), | 
|  | child: new Scaffold( | 
|  | appBar: _buildAppBar(context), | 
|  | drawer: _buildDrawer(), | 
|  | body: body, | 
|  | ), | 
|  | ); | 
|  | } | 
|  | } | 
|  |  | 
|  | void main() { | 
|  | runApp(new MaterialApp( | 
|  | title: 'Cards', | 
|  | home: new CardCollection(), | 
|  | )); | 
|  | } |