Merge pull request #2467 from HansMuller/leave_behind
Added support for Dismissable leave-behind list items
This commit is contained in:
commit
1568b8277c
149
examples/material_gallery/lib/demo/leave_behind_demo.dart
Normal file
149
examples/material_gallery/lib/demo/leave_behind_demo.dart
Normal file
@ -0,0 +1,149 @@
|
||||
// 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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user