mirror of
https://github.com/flutter/flutter
synced 2024-10-13 19:52:53 +00:00
51cd8b6799
Fixes #5835.
470 lines
15 KiB
Dart
470 lines
15 KiB
Dart
// 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) {
|
|
inputValue = new InputValue(text: "Item $value");
|
|
}
|
|
int value;
|
|
double height;
|
|
int get color => ((value % 9) + 1) * 100;
|
|
InputValue inputValue;
|
|
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;
|
|
|
|
final TextStyle backgroundTextStyle = Typography.white.title;
|
|
|
|
Map<int, Color> _primaryColor = Colors.deepPurple;
|
|
List<CardModel> _cardModels;
|
|
DismissDirection _dismissDirection = DismissDirection.horizontal;
|
|
TextAlign _textAlign = TextAlign.center;
|
|
bool _editable = false;
|
|
bool _snapToCenter = false;
|
|
bool _fixedSizeCards = false;
|
|
bool _sunshine = false;
|
|
bool _varyFontSizes = false;
|
|
|
|
void _initVariableSizedCardModels() {
|
|
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();
|
|
}
|
|
|
|
Iterable<int> get _cardIndices sync* {
|
|
for (int i = 0; i < _cardModels.length; i += 1)
|
|
yield i;
|
|
}
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_initCardModels();
|
|
}
|
|
|
|
double _variableSizeToSnapOffset(double scrollOffset) {
|
|
double cumulativeHeight = 0.0;
|
|
List<double> cumulativeHeights = _cardModels.map((CardModel card) {
|
|
cumulativeHeight += card.height + kCardMargins;
|
|
return cumulativeHeight;
|
|
})
|
|
.toList();
|
|
|
|
double offsetForIndex(int i) {
|
|
return (kCardMargins + _cardModels[i].height) / 2.0 + ((i == 0) ? 0.0 : cumulativeHeights[i - 1]);
|
|
}
|
|
|
|
for (int i = 0; i < cumulativeHeights.length; i++) {
|
|
if (cumulativeHeights[i] >= scrollOffset)
|
|
return offsetForIndex(i);
|
|
}
|
|
return offsetForIndex(cumulativeHeights.length - 1);
|
|
}
|
|
|
|
double _fixedSizeToSnapOffset(double scrollOffset) {
|
|
int cardIndex = (scrollOffset.clamp(0.0, kFixedCardHeight * (_cardModels.length - 1)) / kFixedCardHeight).floor();
|
|
return cardIndex * kFixedCardHeight + kFixedCardHeight * 0.5;
|
|
}
|
|
|
|
double _toSnapOffset(double scrollOffset, Size containerSize) {
|
|
double halfHeight = containerSize.height / 2.0;
|
|
scrollOffset += halfHeight;
|
|
double result = _fixedSizeCards ? _fixedSizeToSnapOffset(scrollOffset) : _variableSizeToSnapOffset(scrollOffset);
|
|
return result - halfHeight;
|
|
}
|
|
|
|
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 Block(children: <Widget>[
|
|
new DrawerHeader(child: new Center(child: new Text('Options'))),
|
|
buildDrawerCheckbox("Make card labels editable", _editable, _toggleEditable),
|
|
buildDrawerCheckbox("Snap fling scrolls to center", _snapToCenter, _toggleSnapToCenter),
|
|
buildDrawerCheckbox("Fixed size cards", _fixedSizeCards, _toggleFixedSizeCards),
|
|
buildDrawerCheckbox("Let the sun shine", _sunshine, _toggleSunshine),
|
|
buildDrawerCheckbox("Vary font sizes", _varyFontSizes, _toggleVaryFontSizes, enabled: !_editable),
|
|
new 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),
|
|
new 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),
|
|
new 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),
|
|
new Divider(),
|
|
new DrawerItem(
|
|
icon: new Icon(Icons.dvr),
|
|
onPressed: () { debugDumpApp(); debugDumpRenderTree(); },
|
|
child: new Text('Dump App to Console')
|
|
),
|
|
])
|
|
)
|
|
);
|
|
}
|
|
|
|
String _dismissDirectionText(DismissDirection direction) {
|
|
String s = direction.toString();
|
|
return "dismiss ${s.substring(s.indexOf('.') + 1)}";
|
|
}
|
|
|
|
void _toggleEditable() {
|
|
setState(() {
|
|
_editable = !_editable;
|
|
});
|
|
}
|
|
|
|
void _toggleFixedSizeCards() {
|
|
setState(() {
|
|
_fixedSizeCards = !_fixedSizeCards;
|
|
_initCardModels();
|
|
});
|
|
}
|
|
|
|
void _toggleSnapToCenter() {
|
|
setState(() {
|
|
_snapToCenter = !_snapToCenter;
|
|
});
|
|
}
|
|
|
|
void _toggleSunshine() {
|
|
setState(() {
|
|
_sunshine = !_sunshine;
|
|
});
|
|
}
|
|
|
|
void _toggleVaryFontSizes() {
|
|
setState(() {
|
|
_varyFontSizes = !_varyFontSizes;
|
|
});
|
|
}
|
|
|
|
void _selectColor(Map<int, Color> 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 DrawerItem(
|
|
onPressed: enabled ? callback : null,
|
|
child: new Row(
|
|
children: <Widget>[
|
|
new Flexible(child: new Text(label)),
|
|
new Checkbox(
|
|
value: value,
|
|
onChanged: enabled ? (_) { callback(); } : null
|
|
)
|
|
]
|
|
)
|
|
);
|
|
}
|
|
|
|
Widget buildDrawerColorRadioItem(String label, Map<int, Color> itemValue, Map<int, Color> currentValue, ValueChanged<Map<int, Color>> onChanged, { IconData icon, bool enabled: true }) {
|
|
return new DrawerItem(
|
|
icon: new Icon(icon),
|
|
onPressed: enabled ? () { onChanged(itemValue); } : null,
|
|
child: new Row(
|
|
children: <Widget>[
|
|
new Flexible(child: new Text(label)),
|
|
new Radio<Map<int, Color>>(
|
|
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 DrawerItem(
|
|
icon: new Icon(icon),
|
|
onPressed: enabled ? () { onChanged(itemValue); } : null,
|
|
child: new Row(
|
|
children: <Widget>[
|
|
new Flexible(child: new Text(label)),
|
|
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 DrawerItem(
|
|
icon: new Icon(icon),
|
|
onPressed: enabled ? () { onChanged(itemValue); } : null,
|
|
child: new Row(
|
|
children: <Widget>[
|
|
new Flexible(child: new Text(label)),
|
|
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,
|
|
align: 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) {
|
|
if (index >= _cardModels.length)
|
|
return null;
|
|
|
|
CardModel cardModel = _cardModels[index];
|
|
Widget card = new Dismissable(
|
|
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 Input(
|
|
key: new GlobalObjectKey(cardModel),
|
|
value: cardModel.inputValue,
|
|
onChanged: (InputValue value) {
|
|
setState(() {
|
|
cardModel.inputValue = value;
|
|
});
|
|
}
|
|
)
|
|
)
|
|
: new DefaultTextStyle.inherit(
|
|
context: context,
|
|
style: cardLabelStyle.copyWith(
|
|
fontSize: _varyFontSizes ? 5.0 + index : null
|
|
),
|
|
child: new Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: <Widget>[
|
|
new Text(cardModel.inputValue.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 = new 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 = new Icon(Icons.arrow_forward, size: 36.0);
|
|
if (_dismissDirection == DismissDirection.endToStart)
|
|
rightArrowIcon = new Opacity(opacity: 0.1, child: rightArrowIcon);
|
|
|
|
// 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.stretch(
|
|
child: new Container(
|
|
margin: const EdgeInsets.all(4.0),
|
|
child: new Viewport(
|
|
child: new Container(
|
|
height: cardModel.height,
|
|
decoration: new BoxDecoration(backgroundColor: Theme.of(context).primaryColor),
|
|
child: new Row(
|
|
children: <Widget>[
|
|
leftArrowIcon,
|
|
new Flexible(
|
|
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;
|
|
if (_fixedSizeCards) {
|
|
cardCollection = new ScrollableList(
|
|
snapOffsetCallback: _snapToCenter ? _toSnapOffset : null,
|
|
itemExtent: kFixedCardHeight,
|
|
children: _cardIndices.map/*<Widget>*/((int index) => _buildCard(context, index))
|
|
);
|
|
} else {
|
|
cardCollection = new LazyBlock(
|
|
delegate: new LazyBlockBuilder(builder: _buildCard),
|
|
snapOffsetCallback: _snapToCenter ? _toSnapOffset : null
|
|
);
|
|
}
|
|
|
|
if (_sunshine) {
|
|
cardCollection = new Stack(
|
|
children: <Widget>[
|
|
new Column(children: <Widget>[new Image.network(_sunshineURL)]),
|
|
new ShaderMask(child: cardCollection, shaderCallback: _createShader)
|
|
]
|
|
);
|
|
}
|
|
|
|
Widget body = new Container(
|
|
padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 8.0),
|
|
decoration: new BoxDecoration(backgroundColor: _primaryColor[50]),
|
|
child: cardCollection
|
|
);
|
|
|
|
if (_snapToCenter) {
|
|
Widget indicator = new IgnorePointer(
|
|
child: new Align(
|
|
alignment: FractionalOffset.centerLeft,
|
|
child: new Container(
|
|
height: 1.0,
|
|
decoration: new BoxDecoration(backgroundColor: const Color(0x80FFFFFF))
|
|
)
|
|
)
|
|
);
|
|
body = new Stack(children: <Widget>[body, indicator]);
|
|
}
|
|
|
|
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()
|
|
));
|
|
}
|