Hans Muller 7782a11534 Adds PageableList, other scrolling related changes and fixes
- PageableList extends ScrollableList
One fixed width or height item is visible and centered at a
time. Fling and drag gestures scroll to the next/previous item.

- Scrollable.scrollTo(), Scrollable.scrollBy(), ensureWidgetIsVisible() API changed
The named animation parameter for these methods was replaced by
duration and curve. All of the methods now return a Future. The Future
completes when the scroll does.

This change eliminates the need for Scrollable to temporarily take ownership
of a ValueAnimation object (see #645).

- Using Future.then() instead of an AnimationPerformance status listener
In ensure_visible.dart _handleTap() uses ensureWidgetIsVisible() to
center the card roughly as before and then. When the implicit scroll
animation is complete, it changes the centered card's label font. The
change is made when the Future returned by ensureWidgetIsVisible()
completes.

- FixedHeightScrollable's itemHeight parameter is now itemExtent
If scrollDirection is ScrollDirection.vertical (the default) then itemExtent should
be the height of each item; otherwise it should be the width of each item.

Replaced _velocityForFlingGesture() in scrollable.dart with Scrollable._eventVelocity()
The original version clamped pixels/ms against pixels/sec constants. The new version
also deals with scrollDirection.

- Plumbed scrollDirection though FixedHeightScrollable and ScrollableList

Both classes should now support horizontal scrolling.
2015-08-19 10:14:21 -07:00

337 lines
8.7 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.
part of fitness;
class FitnessItemList extends Component {
FitnessItemList({ Key key, this.items, this.onDismissed }) : super(key: key) {
assert(items != null);
assert(onDismissed != null);
}
final List<FitnessItem> items;
final FitnessItemHandler onDismissed;
Widget build() {
return new Material(
type: MaterialType.canvas,
child: new ScrollableList<FitnessItem>(
padding: const EdgeDims.all(4.0),
items: items,
itemExtent: kFitnessItemHeight,
itemBuilder: (item) => item.toRow(onDismissed: onDismissed)
)
);
}
}
class DialogMenuItem extends ButtonBase {
DialogMenuItem(this.children, { Key key, this.onPressed }) : super(key: key);
List<Widget> children;
Function onPressed;
void syncFields(DialogMenuItem source) {
children = source.children;
onPressed = source.onPressed;
super.syncFields(source);
}
Widget buildContent() {
return new Listener(
onGestureTap: (_) {
if (onPressed != null)
onPressed();
},
child: new Container(
height: 48.0,
child: new InkWell(
child: new Padding(
padding: const EdgeDims.symmetric(horizontal: 16.0),
child: new Flex(children)
)
)
)
);
}
}
class FeedFragment extends StatefulComponent {
FeedFragment({ this.navigator, this.userData, this.onItemCreated, this.onItemDeleted });
Navigator navigator;
List<FitnessItem> userData;
FitnessItemHandler onItemCreated;
FitnessItemHandler onItemDeleted;
FitnessMode _fitnessMode = FitnessMode.feed;
void initState() {
// if (debug)
// new Timer(new Duration(seconds: 1), dumpState);
super.initState();
}
void syncFields(FeedFragment source) {
navigator = source.navigator;
userData = source.userData;
onItemCreated = source.onItemCreated;
onItemDeleted = source.onItemDeleted;
}
AnimationStatus _snackBarStatus = AnimationStatus.dismissed;
bool _isShowingSnackBar = false;
EventDisposition _handleFitnessModeChange(FitnessMode value) {
setState(() {
_fitnessMode = value;
_drawerShowing = false;
});
return EventDisposition.processed;
}
Drawer buildDrawer() {
if (_drawerStatus == AnimationStatus.dismissed)
return null;
return new Drawer(
showing: _drawerShowing,
level: 3,
onDismissed: _handleDrawerDismissed,
navigator: navigator,
children: [
new DrawerHeader(children: [new Text('Fitness')]),
new DrawerItem(
icon: 'action/view_list',
onPressed: () => _handleFitnessModeChange(FitnessMode.feed),
selected: _fitnessMode == FitnessMode.feed,
children: [new Text('Feed')]),
new DrawerItem(
icon: 'action/assessment',
onPressed: () => _handleFitnessModeChange(FitnessMode.chart),
selected: _fitnessMode == FitnessMode.chart,
children: [new Text('Chart')]),
new DrawerDivider(),
new DrawerItem(
icon: 'action/settings',
onPressed: _handleShowSettings,
children: [new Text('Settings')]),
new DrawerItem(
icon: 'action/help',
children: [new Text('Help & Feedback')])
]
);
}
bool _drawerShowing = false;
AnimationStatus _drawerStatus = AnimationStatus.dismissed;
void _handleOpenDrawer() {
setState(() {
_drawerShowing = true;
_drawerStatus = AnimationStatus.forward;
});
}
void _handleDrawerDismissed() {
setState(() {
_drawerStatus = AnimationStatus.dismissed;
});
}
EventDisposition _handleShowSettings() {
navigator.pop();
navigator.pushNamed('/settings');
return EventDisposition.processed;
}
// TODO(jackson): We should be localizing
String get fitnessModeTitle {
switch(_fitnessMode) {
case FitnessMode.feed: return "Feed";
case FitnessMode.chart: return "Chart";
}
}
Widget buildToolBar() {
return new ToolBar(
left: new IconButton(
icon: "navigation/menu",
onPressed: _handleOpenDrawer),
center: new Text(fitnessModeTitle)
);
}
FitnessItem _undoItem;
void _handleItemDismissed(FitnessItem item) {
onItemDeleted(item);
setState(() {
_undoItem = item;
_isShowingSnackBar = true;
_snackBarStatus = AnimationStatus.forward;
});
}
Widget buildChart() {
double startX;
double endX;
double startY;
double endY;
List<Point> dataSet = new List<Point>();
for (FitnessItem item in userData) {
if (item is Measurement) {
double x = item.when.millisecondsSinceEpoch.toDouble();
double y = item.weight;
if (startX == null)
startX = x;
endX = x;
if (startY == null || startY > y)
startY = y;
if (endY == null || endY < y)
endY = y;
dataSet.add(new Point(x, y));
}
}
playfair.ChartData data = new playfair.ChartData(
startX: startX,
startY: startY,
endX: endX,
endY: endY,
dataSet: dataSet,
numHorizontalGridlines: 5,
roundToPlaces: 1
);
return new playfair.Chart(data: data);
}
Widget buildBody() {
TextStyle style = Theme.of(this).text.title;
if (userData.length == 0)
return new Material(
type: MaterialType.canvas,
child: new Flex(
[new Text("No data yet.\nAdd some!", style: style)],
justifyContent: FlexJustifyContent.center
)
);
switch (_fitnessMode) {
case FitnessMode.feed:
return new FitnessItemList(
items: userData,
onDismissed: _handleItemDismissed
);
case FitnessMode.chart:
return new Material(
type: MaterialType.canvas,
child: new Container(
padding: const EdgeDims.all(20.0),
child: buildChart()
)
);
}
}
void _handleUndo() {
onItemCreated(_undoItem);
setState(() {
_undoItem = null;
_isShowingSnackBar = false;
});
}
Anchor _snackBarAnchor = new Anchor();
Widget buildSnackBar() {
if (_snackBarStatus == AnimationStatus.dismissed)
return null;
return new SnackBar(
showing: _isShowingSnackBar,
anchor: _snackBarAnchor,
content: new Text("Item deleted."),
actions: [new SnackBarAction(label: "UNDO", onPressed: _handleUndo)],
onDismissed: () { setState(() { _snackBarStatus = AnimationStatus.dismissed; }); }
);
}
void _handleActionButtonPressed() {
showDialog(navigator, (navigator) => new AddItemDialog(navigator)).then((routeName) {
if (routeName != null)
navigator.pushNamed(routeName);
});
}
Widget buildFloatingActionButton() {
switch (_fitnessMode) {
case FitnessMode.feed:
return _snackBarAnchor.build(
new FloatingActionButton(
child: new Icon(type: 'content/add', size: 24),
onPressed: _handleActionButtonPressed
));
case FitnessMode.chart:
return null;
}
}
Widget build() {
return new Scaffold(
toolbar: buildToolBar(),
body: buildBody(),
snackBar: buildSnackBar(),
floatingActionButton: buildFloatingActionButton(),
drawer: buildDrawer()
);
}
}
class AddItemDialog extends StatefulComponent {
AddItemDialog(this.navigator);
Navigator navigator;
void syncFields(AddItemDialog source) {
this.navigator = source.navigator;
}
// TODO(jackson): Internationalize
static final Map<String, String> _labels = {
'/measurements/new': 'Measure',
'/meals/new': 'Eat',
};
String _addItemRoute = _labels.keys.first;
void _handleAddItemRouteChanged(String routeName) {
setState(() {
_addItemRoute = routeName;
});
}
Widget build() {
List<Widget> menuItems = [];
for(String routeName in _labels.keys) {
menuItems.add(new DialogMenuItem([
new Flexible(child: new Text(_labels[routeName])),
new Radio(value: routeName, groupValue: _addItemRoute, onChanged: _handleAddItemRouteChanged),
], onPressed: () => _handleAddItemRouteChanged(routeName)));
}
return new Dialog(
title: new Text("What are you doing?"),
content: new ScrollableBlock(menuItems),
onDismiss: navigator.pop,
actions: [
new FlatButton(
child: new Text('CANCEL'),
onPressed: navigator.pop
),
new FlatButton(
child: new Text('ADD'),
onPressed: () {
navigator.pop(_addItemRoute);
}
),
]
);
}
}