Commit 2662ea52 authored by Hans Muller's avatar Hans Muller

Added support for List leave-behind items

parent a5887b6f
// Copyright 2016 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';
enum LeaveBehindDemoAction {
reset,
horizontalSwipe,
leftSwipe,
rightSwipe
}
class LeaveBehindItem {
LeaveBehindItem({ this.index, this.name, this.subject, this.body });
LeaveBehindItem.from(LeaveBehindItem item)
: index = item.index, name = item.name, subject = item.subject, body = item.body;
final int index;
final String name;
final String subject;
final String body;
}
class LeaveBehindDemo extends StatefulComponent {
LeaveBehindDemo({ Key key }) : super(key: key);
LeaveBehindDemoState createState() => new LeaveBehindDemoState();
}
class LeaveBehindDemoState extends State<LeaveBehindDemo> {
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
DismissDirection _dismissDirection = DismissDirection.horizontal;
List<LeaveBehindItem> leaveBehindItems;
void initListItems() {
leaveBehindItems = new List.generate(16, (int index) {
return new LeaveBehindItem(
index: index,
name: 'Item $index Sender',
subject: 'Subject: $index',
body: "[$index] first line of the message's body..."
);
});
}
void initState() {
super.initState();
initListItems();
}
void handleDemoAction(LeaveBehindDemoAction action) {
switch(action) {
case LeaveBehindDemoAction.reset:
initListItems();
break;
case LeaveBehindDemoAction.horizontalSwipe:
_dismissDirection = DismissDirection.horizontal;
break;
case LeaveBehindDemoAction.leftSwipe:
_dismissDirection = DismissDirection.left;
break;
case LeaveBehindDemoAction.rightSwipe:
_dismissDirection = DismissDirection.right;
break;
}
}
Widget buildItem(LeaveBehindItem item) {
final ThemeData theme = Theme.of(context);
return new Dismissable(
key: new ObjectKey(item),
direction: _dismissDirection,
onDismissed: (DismissDirection direction) {
setState(() {
leaveBehindItems.remove(item);
});
final String action = (direction == DismissDirection.left) ? 'archived' : 'deleted';
_scaffoldKey.currentState.showSnackBar(new SnackBar(
content: new Text('You $action item ${item.index}')
));
},
background: new Container(
decoration: new BoxDecoration(backgroundColor: theme.primaryColor),
child: new ListItem(
left: new Icon(icon: Icons.delete, color: Colors.white, size: 36.0)
)
),
secondaryBackground: new Container(
decoration: new BoxDecoration(backgroundColor: theme.primaryColor),
child: new ListItem(
right: new Icon(icon: Icons.archive, color: Colors.white, size: 36.0)
)
),
child: new Container(
decoration: new BoxDecoration(
backgroundColor: theme.canvasColor,
border: new Border(bottom: new BorderSide(color: theme.dividerColor))
),
child: new ListItem(
primary: new Text(item.name),
secondary: new Text('${item.subject}\n${item.body}'),
isThreeLine: true
)
)
);
}
Widget build(BuildContext context) {
return new Scaffold(
key: _scaffoldKey,
toolBar: new ToolBar(
center: new Text('Swipe Items to Dismiss'),
right: <Widget>[
new PopupMenuButton<LeaveBehindDemoAction>(
onSelected: handleDemoAction,
items: <PopupMenuEntry>[
new PopupMenuItem<LeaveBehindDemoAction>(
value: LeaveBehindDemoAction.reset,
child: new Text('Reset the list')
),
new PopupMenuDivider(),
new CheckedPopupMenuItem<LeaveBehindDemoAction>(
value: LeaveBehindDemoAction.horizontalSwipe,
checked: _dismissDirection == DismissDirection.horizontal,
child: new Text('Hoizontal swipe')
),
new CheckedPopupMenuItem<LeaveBehindDemoAction>(
value: LeaveBehindDemoAction.leftSwipe,
checked: _dismissDirection == DismissDirection.left,
child: new Text('Only swipe left')
),
new CheckedPopupMenuItem<LeaveBehindDemoAction>(
value: LeaveBehindDemoAction.rightSwipe,
checked: _dismissDirection == DismissDirection.right,
child: new Text('Only swipe right')
)
]
)
]
),
body: new Block(
padding: new EdgeDims.all(4.0),
children: leaveBehindItems.map(buildItem).toList()
)
);
}
}
......@@ -19,6 +19,7 @@ import '../demo/drop_down_demo.dart';
import '../demo/fitness_demo.dart';
import '../demo/grid_list_demo.dart';
import '../demo/icons_demo.dart';
import '../demo/leave_behind_demo.dart';
import '../demo/list_demo.dart';
import '../demo/modal_bottom_sheet_demo.dart';
import '../demo/menu_demo.dart';
......@@ -107,6 +108,7 @@ class GalleryHomeState extends State<GalleryHome> {
new GalleryDemo(title: 'Floating Action Button', builder: () => new TabsFabDemo()),
new GalleryDemo(title: 'Grid', builder: () => new GridListDemo()),
new GalleryDemo(title: 'Icons', builder: () => new IconsDemo()),
new GalleryDemo(title: 'Leave-behind List Items', builder: () => new LeaveBehindDemo()),
new GalleryDemo(title: 'List', builder: () => new ListDemo()),
new GalleryDemo(title: 'Modal Bottom Sheet', builder: () => new ModalBottomSheetDemo()),
new GalleryDemo(title: 'Menus', builder: () => new MenuDemo()),
......
......@@ -67,7 +67,7 @@ class GallerySection extends StatelessComponent {
primarySwatch: colors
);
final TextStyle titleTextStyle = theme.text.title.copyWith(
color: theme.brightness == ThemeBrightness.dark ? Colors.black : Colors.white
color: Colors.white
);
return new Flexible(
child: new GestureDetector(
......
......@@ -296,6 +296,7 @@ class CardCollectionState extends State<CardCollection> {
CardModel cardModel = _cardModels[index];
Widget card = new Dismissable(
key: new ObjectKey(cardModel),
direction: _dismissDirection,
onResized: () { _invalidator(<int>[index]); },
onDismissed: (DismissDirection direction) { dismissCard(cardModel); },
......
......@@ -28,7 +28,6 @@ class ListItem extends StatelessComponent {
this.onTap,
this.onLongPress
}) : super(key: key) {
assert(primary != null);
assert(isThreeLine ? secondary != null : true);
}
......@@ -117,7 +116,7 @@ class ListItem extends StatelessComponent {
final Widget primaryLine = new DefaultTextStyle(
style: primaryTextStyle(context),
child: primary
child: primary ?? new Container()
);
Widget center = primaryLine;
if (isTwoLine || isThreeLine) {
......
......@@ -38,24 +38,47 @@ enum DismissDirection {
down
}
/// Can be dismissed by dragging in one or more directions.
/// Can be dismissed by dragging in the indicated [direction].
///
/// The child is draggable in the indicated direction(s). When released (or
/// flung), the child disappears off the edge and the dismissable widget
/// Dragging or flinging this widget in the [DismissDirection] causes the child
/// to slide out of view. Following the slide animation, the Dismissable widget
/// animates its height (or width, whichever is perpendicular to the dismiss
/// direction) to zero.
///
/// Backgrounds can be used to implement the "leave-behind" idiom. If a background
/// is specified it is stacked behind the Dismissable's child and is exposed when
/// the child moves.
///
/// The [onDimissed] callback runs after Dismissable's size has collapsed to zero.
/// If the Dismissable is a list item, it must have a key that distinguishes it from
/// the other items and its onDismissed callback must remove the item from the list.
class Dismissable extends StatefulComponent {
Dismissable({
Key key,
this.child,
this.background,
this.secondaryBackground,
this.onResized,
this.onDismissed,
this.direction: DismissDirection.horizontal
}) : super(key: key);
}) : super(key: key) {
assert(key != null);
assert(secondaryBackground != null ? background != null : true);
}
final Widget child;
/// Called when the widget changes size (i.e., when contracting after being dismissed).
/// A widget that is stacked behind the child. If secondaryBackground is also
/// specified then this widget only appears when the child has been dragged
/// down or to the right.
final Widget background;
/// A widget that is stacked behind the child and is exposed when the child
/// has been dragged up or to the left. It may only be specified when background
/// has also been specified.
final Widget secondaryBackground;
/// Called when the widget changes size (i.e., when contracting before being dismissed).
final VoidCallback onResized;
/// Called when the widget has been dismissed, after finishing resizing.
......@@ -96,6 +119,12 @@ class _DismissableState extends State<Dismissable> {
|| config.direction == DismissDirection.right;
}
DismissDirection get _dismissDirection {
if (_directionIsXAxis)
return _dragExtent > 0 ? DismissDirection.right : DismissDirection.left;
return _dragExtent > 0 ? DismissDirection.down : DismissDirection.up;
}
bool get _isActive {
return _dragUnderway || _moveController.isAnimating;
}
......@@ -235,12 +264,7 @@ class _DismissableState extends State<Dismissable> {
void _handleResizeProgressChanged() {
if (_resizeController.isCompleted) {
if (config.onDismissed != null) {
DismissDirection direction;
if (_directionIsXAxis)
direction = _dragExtent > 0 ? DismissDirection.right : DismissDirection.left;
else
direction = _dragExtent > 0 ? DismissDirection.down : DismissDirection.up;
config.onDismissed(direction);
config.onDismissed(_dismissDirection);
}
} else {
if (config.onResized != null)
......@@ -249,31 +273,53 @@ class _DismissableState extends State<Dismissable> {
}
Widget build(BuildContext context) {
Widget background = config.background;
if (config.secondaryBackground != null) {
final DismissDirection direction = _dismissDirection;
if (direction == DismissDirection.left || direction == DismissDirection.up)
background = config.secondaryBackground;
}
if (_resizeAnimation != null) {
// we've been dragged aside, and are now resizing.
assert(() {
if (_resizeAnimation.status != AnimationStatus.forward) {
assert(_resizeAnimation.status == AnimationStatus.completed);
throw new WidgetError(
'Dismissable widget completed its resize animation without being removed from the tree.\n'
'Make sure to implement the onDismissed handler and to immediately remove the Dismissable '
'A dismissed Dismissable widget is still part of the tree.\n' +
'Make sure to implement the onDismissed handler and to immediately remove the Dismissable\n' +
'widget from the application once that handler has fired.'
);
}
return true;
});
return new AnimatedBuilder(
animation: _resizeAnimation,
builder: (BuildContext context, Widget child) {
return new SizedBox(
width: !_directionIsXAxis ? _resizeAnimation.value : null,
height: _directionIsXAxis ? _resizeAnimation.value : null
height: _directionIsXAxis ? _resizeAnimation.value : null,
child: background
);
}
);
}
// we are not resizing. (we may be being dragged aside.)
Widget backgroundAndChild = new SlideTransition(
position: _moveAnimation,
child: config.child
);
if (background != null) {
backgroundAndChild = new Stack(
children: <Widget>[
new Positioned(left: 0.0, top: 0.0, bottom: 0.0, right: 0.0, child: background),
new Viewport(child: backgroundAndChild)
]
);
}
// We are not resizing but we may be being dragging in config.direction.
return new GestureDetector(
onHorizontalDragStart: _directionIsXAxis ? _handleDragStart : null,
onHorizontalDragUpdate: _directionIsXAxis ? _handleDragUpdate : null,
......@@ -282,10 +328,7 @@ class _DismissableState extends State<Dismissable> {
onVerticalDragUpdate: _directionIsXAxis ? null : _handleDragUpdate,
onVerticalDragEnd: _directionIsXAxis ? null : _handleDragEnd,
behavior: HitTestBehavior.opaque,
child: new SlideTransition(
position: _moveAnimation,
child: config.child
)
child: backgroundAndChild
);
}
}
......@@ -110,6 +110,7 @@ class Test1215DismissableComponent extends StatelessComponent {
final String text;
Widget build(BuildContext context) {
return new Dismissable(
key: new ObjectKey(text),
child: new AspectRatio(
aspectRatio: 1.0,
child: new Text(this.text)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment