Commit 1568b827 authored by Hans Muller's avatar Hans Muller

Merge pull request #2467 from HansMuller/leave_behind

Added support for Dismissable leave-behind list items
parents a5887b6f 2662ea52
// 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'; ...@@ -19,6 +19,7 @@ import '../demo/drop_down_demo.dart';
import '../demo/fitness_demo.dart'; import '../demo/fitness_demo.dart';
import '../demo/grid_list_demo.dart'; import '../demo/grid_list_demo.dart';
import '../demo/icons_demo.dart'; import '../demo/icons_demo.dart';
import '../demo/leave_behind_demo.dart';
import '../demo/list_demo.dart'; import '../demo/list_demo.dart';
import '../demo/modal_bottom_sheet_demo.dart'; import '../demo/modal_bottom_sheet_demo.dart';
import '../demo/menu_demo.dart'; import '../demo/menu_demo.dart';
...@@ -107,6 +108,7 @@ class GalleryHomeState extends State<GalleryHome> { ...@@ -107,6 +108,7 @@ class GalleryHomeState extends State<GalleryHome> {
new GalleryDemo(title: 'Floating Action Button', builder: () => new TabsFabDemo()), new GalleryDemo(title: 'Floating Action Button', builder: () => new TabsFabDemo()),
new GalleryDemo(title: 'Grid', builder: () => new GridListDemo()), new GalleryDemo(title: 'Grid', builder: () => new GridListDemo()),
new GalleryDemo(title: 'Icons', builder: () => new IconsDemo()), 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: 'List', builder: () => new ListDemo()),
new GalleryDemo(title: 'Modal Bottom Sheet', builder: () => new ModalBottomSheetDemo()), new GalleryDemo(title: 'Modal Bottom Sheet', builder: () => new ModalBottomSheetDemo()),
new GalleryDemo(title: 'Menus', builder: () => new MenuDemo()), new GalleryDemo(title: 'Menus', builder: () => new MenuDemo()),
......
...@@ -67,7 +67,7 @@ class GallerySection extends StatelessComponent { ...@@ -67,7 +67,7 @@ class GallerySection extends StatelessComponent {
primarySwatch: colors primarySwatch: colors
); );
final TextStyle titleTextStyle = theme.text.title.copyWith( final TextStyle titleTextStyle = theme.text.title.copyWith(
color: theme.brightness == ThemeBrightness.dark ? Colors.black : Colors.white color: Colors.white
); );
return new Flexible( return new Flexible(
child: new GestureDetector( child: new GestureDetector(
......
...@@ -296,6 +296,7 @@ class CardCollectionState extends State<CardCollection> { ...@@ -296,6 +296,7 @@ class CardCollectionState extends State<CardCollection> {
CardModel cardModel = _cardModels[index]; CardModel cardModel = _cardModels[index];
Widget card = new Dismissable( Widget card = new Dismissable(
key: new ObjectKey(cardModel),
direction: _dismissDirection, direction: _dismissDirection,
onResized: () { _invalidator(<int>[index]); }, onResized: () { _invalidator(<int>[index]); },
onDismissed: (DismissDirection direction) { dismissCard(cardModel); }, onDismissed: (DismissDirection direction) { dismissCard(cardModel); },
......
...@@ -28,7 +28,6 @@ class ListItem extends StatelessComponent { ...@@ -28,7 +28,6 @@ class ListItem extends StatelessComponent {
this.onTap, this.onTap,
this.onLongPress this.onLongPress
}) : super(key: key) { }) : super(key: key) {
assert(primary != null);
assert(isThreeLine ? secondary != null : true); assert(isThreeLine ? secondary != null : true);
} }
...@@ -117,7 +116,7 @@ class ListItem extends StatelessComponent { ...@@ -117,7 +116,7 @@ class ListItem extends StatelessComponent {
final Widget primaryLine = new DefaultTextStyle( final Widget primaryLine = new DefaultTextStyle(
style: primaryTextStyle(context), style: primaryTextStyle(context),
child: primary child: primary ?? new Container()
); );
Widget center = primaryLine; Widget center = primaryLine;
if (isTwoLine || isThreeLine) { if (isTwoLine || isThreeLine) {
......
...@@ -38,24 +38,47 @@ enum DismissDirection { ...@@ -38,24 +38,47 @@ enum DismissDirection {
down 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 /// Dragging or flinging this widget in the [DismissDirection] causes the child
/// flung), the child disappears off the edge and the dismissable widget /// to slide out of view. Following the slide animation, the Dismissable widget
/// animates its height (or width, whichever is perpendicular to the dismiss /// animates its height (or width, whichever is perpendicular to the dismiss
/// direction) to zero. /// 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 { class Dismissable extends StatefulComponent {
Dismissable({ Dismissable({
Key key, Key key,
this.child, this.child,
this.background,
this.secondaryBackground,
this.onResized, this.onResized,
this.onDismissed, this.onDismissed,
this.direction: DismissDirection.horizontal this.direction: DismissDirection.horizontal
}) : super(key: key); }) : super(key: key) {
assert(key != null);
assert(secondaryBackground != null ? background != null : true);
}
final Widget child; 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; final VoidCallback onResized;
/// Called when the widget has been dismissed, after finishing resizing. /// Called when the widget has been dismissed, after finishing resizing.
...@@ -96,6 +119,12 @@ class _DismissableState extends State<Dismissable> { ...@@ -96,6 +119,12 @@ class _DismissableState extends State<Dismissable> {
|| config.direction == DismissDirection.right; || 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 { bool get _isActive {
return _dragUnderway || _moveController.isAnimating; return _dragUnderway || _moveController.isAnimating;
} }
...@@ -235,12 +264,7 @@ class _DismissableState extends State<Dismissable> { ...@@ -235,12 +264,7 @@ class _DismissableState extends State<Dismissable> {
void _handleResizeProgressChanged() { void _handleResizeProgressChanged() {
if (_resizeController.isCompleted) { if (_resizeController.isCompleted) {
if (config.onDismissed != null) { if (config.onDismissed != null) {
DismissDirection direction; config.onDismissed(_dismissDirection);
if (_directionIsXAxis)
direction = _dragExtent > 0 ? DismissDirection.right : DismissDirection.left;
else
direction = _dragExtent > 0 ? DismissDirection.down : DismissDirection.up;
config.onDismissed(direction);
} }
} else { } else {
if (config.onResized != null) if (config.onResized != null)
...@@ -249,31 +273,53 @@ class _DismissableState extends State<Dismissable> { ...@@ -249,31 +273,53 @@ class _DismissableState extends State<Dismissable> {
} }
Widget build(BuildContext context) { 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) { if (_resizeAnimation != null) {
// we've been dragged aside, and are now resizing. // we've been dragged aside, and are now resizing.
assert(() { assert(() {
if (_resizeAnimation.status != AnimationStatus.forward) { if (_resizeAnimation.status != AnimationStatus.forward) {
assert(_resizeAnimation.status == AnimationStatus.completed); assert(_resizeAnimation.status == AnimationStatus.completed);
throw new WidgetError( throw new WidgetError(
'Dismissable widget completed its resize animation without being removed from the tree.\n' 'A dismissed Dismissable widget is still part of the tree.\n' +
'Make sure to implement the onDismissed handler and to immediately remove the Dismissable ' 'Make sure to implement the onDismissed handler and to immediately remove the Dismissable\n' +
'widget from the application once that handler has fired.' 'widget from the application once that handler has fired.'
); );
} }
return true; return true;
}); });
return new AnimatedBuilder( return new AnimatedBuilder(
animation: _resizeAnimation, animation: _resizeAnimation,
builder: (BuildContext context, Widget child) { builder: (BuildContext context, Widget child) {
return new SizedBox( return new SizedBox(
width: !_directionIsXAxis ? _resizeAnimation.value : null, 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( return new GestureDetector(
onHorizontalDragStart: _directionIsXAxis ? _handleDragStart : null, onHorizontalDragStart: _directionIsXAxis ? _handleDragStart : null,
onHorizontalDragUpdate: _directionIsXAxis ? _handleDragUpdate : null, onHorizontalDragUpdate: _directionIsXAxis ? _handleDragUpdate : null,
...@@ -282,10 +328,7 @@ class _DismissableState extends State<Dismissable> { ...@@ -282,10 +328,7 @@ class _DismissableState extends State<Dismissable> {
onVerticalDragUpdate: _directionIsXAxis ? null : _handleDragUpdate, onVerticalDragUpdate: _directionIsXAxis ? null : _handleDragUpdate,
onVerticalDragEnd: _directionIsXAxis ? null : _handleDragEnd, onVerticalDragEnd: _directionIsXAxis ? null : _handleDragEnd,
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
child: new SlideTransition( child: backgroundAndChild
position: _moveAnimation,
child: config.child
)
); );
} }
} }
...@@ -110,6 +110,7 @@ class Test1215DismissableComponent extends StatelessComponent { ...@@ -110,6 +110,7 @@ class Test1215DismissableComponent extends StatelessComponent {
final String text; final String text;
Widget build(BuildContext context) { Widget build(BuildContext context) {
return new Dismissable( return new Dismissable(
key: new ObjectKey(text),
child: new AspectRatio( child: new AspectRatio(
aspectRatio: 1.0, aspectRatio: 1.0,
child: new Text(this.text) 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