// 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 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart' show debugDumpRenderTree; class CardModel { CardModel(this.value, this.height) { textController = TextEditingController(text: 'Item $value'); } int value; double height; int get color => ((value % 9) + 1) * 100; TextEditingController textController; Key get key => ObjectKey(this); } class CardCollection extends StatefulWidget { @override CardCollectionState createState() => CardCollectionState(); } class CardCollectionState extends State<CardCollection> { static const TextStyle cardLabelStyle = 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; static const List<double> _cardHeights = <double>[ 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, ]; 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 _updateCardSizes() { if (_fixedSizeCards) return; _cardModels = List<CardModel>.generate( _cardModels.length, (int i) { _cardModels[i].height = _editable ? max(_cardHeights[i], 60.0) : _cardHeights[i]; return _cardModels[i]; } ); } void _initVariableSizedCardModels() { _cardModels = List<CardModel>.generate( _cardHeights.length, (int i) => CardModel(i, _editable ? max(_cardHeights[i], 60.0) : _cardHeights[i]) ); } void _initFixedSizedCardModels() { const int cardCount = 27; _cardModels = List<CardModel>.generate( cardCount, (int i) => 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 Drawer( child: IconTheme( data: const IconThemeData(color: Colors.black), child: ListView( children: <Widget>[ const DrawerHeader(child: Center(child: 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(), 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; _updateCardSizes(); }); } 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 ListTile( onTap: enabled ? callback : null, title: Text(label), trailing: Checkbox( value: value, onChanged: enabled ? (_) { callback(); } : null, ), ); } Widget buildDrawerColorRadioItem(String label, MaterialColor itemValue, MaterialColor currentValue, ValueChanged<MaterialColor> onChanged, { IconData icon, bool enabled = true }) { return ListTile( leading: Icon(icon), title: Text(label), onTap: enabled ? () { onChanged(itemValue); } : null, trailing: 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 ListTile( leading: Icon(icon), title: Text(label), onTap: enabled ? () { onChanged(itemValue); } : null, trailing: 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 ListTile( leading: Icon(icon), title: Text(label), onTap: enabled ? () { onChanged(itemValue); } : null, trailing: Radio<TextAlign>( value: itemValue, groupValue: currentValue, onChanged: enabled ? onChanged : null, ), ); } Widget _buildAppBar(BuildContext context) { return AppBar( actions: <Widget>[ Text(_dismissDirectionText(_dismissDirection)) ], flexibleSpace: Container( padding: const EdgeInsets.only(left: 72.0), height: 128.0, alignment: const Alignment(-1.0, 0.5), child: 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 = Dismissible( key: ObjectKey(cardModel), direction: _dismissDirection, onDismissed: (DismissDirection direction) { dismissCard(cardModel); }, child: Card( color: _primaryColor[cardModel.color], child: Container( height: cardModel.height, padding: const EdgeInsets.all(kCardMargins), child: _editable ? Center( child: TextField( key: GlobalObjectKey(cardModel), controller: cardModel.textController, ), ) : DefaultTextStyle.merge( style: cardLabelStyle.copyWith( fontSize: _varyFontSizes ? 5.0 + index : null ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ 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 = 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 = 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 = Positioned.fill( child: Container( margin: const EdgeInsets.all(4.0), child: SingleChildScrollView( child: Container( height: cardModel.height, color: theme.primaryColor, child: Row( children: <Widget>[ leftArrowIcon, Expanded( child: Text(backgroundMessage, style: backgroundTextStyle, textAlign: TextAlign.center, ), ), rightArrowIcon, ], ), ), ), ), ); return IconTheme( key: cardModel.key, data: const IconThemeData(color: Colors.white), child: Stack(children: <Widget>[background, card]), ); } Shader _createShader(Rect bounds) { return const LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: <Color>[Color(0x00FFFFFF), Color(0xFFFFFFFF)], stops: <double>[0.1, 0.35], ) .createShader(bounds); } @override Widget build(BuildContext context) { Widget cardCollection = ListView.builder( itemExtent: _fixedSizeCards ? kFixedCardHeight : null, itemCount: _cardModels.length, itemBuilder: _buildCard, ); if (_sunshine) { cardCollection = Stack( children: <Widget>[ Column(children: <Widget>[Image.network(_sunshineURL)]), ShaderMask(child: cardCollection, shaderCallback: _createShader), ], ); } final Widget body = Container( padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 8.0), color: _primaryColor.shade50, child: cardCollection, ); return Theme( data: ThemeData( primarySwatch: _primaryColor, ), child: Scaffold( appBar: _buildAppBar(context), drawer: _buildDrawer(), body: body, ), ); } } void main() { runApp(MaterialApp( title: 'Cards', home: CardCollection(), )); }