Added support for List leave-behind items
This commit is contained in:
parent
a5887b6fb4
commit
2662ea5283
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/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> {
|
|||||||
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 {
|
|||||||
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> {
|
|||||||
|
|
||||||
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 {
|
|||||||
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 {
|
|||||||
|
|
||||||
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 {
|
|||||||
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> {
|
|||||||
|| 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> {
|
|||||||
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> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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> {
|
|||||||
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 {
|
|||||||
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)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user