Commit 4e01c054 authored by Hans Muller's avatar Hans Muller

Add Dismissable support for DismissDirection

```
enum DismissDirection {
  vertical,
  horizontal,
  left,
  right,
  up,
  down
}
```

To only enable dismissing to the right create the `Dismissable` with `direction: DismissDirection.right`. By default direction is `DismissDirection.horizontal` (left or right).

Updated the card_collection "Swipe Away" demo with a drawer that can be used to select one of the three X axis dismiss directions. Currently the MixedViewport class doesn't support horizontal scrolling, so the demo doesn't support the X axis dismiss directions.
parent d1a56611
......@@ -23,8 +23,12 @@ class CardCollectionApp extends App {
final TextStyle backgroundTextStyle =
typography.white.title.copyWith(textAlign: TextAlign.center);
MixedViewportLayoutState layoutState = new MixedViewportLayoutState();
List<CardModel> cardModels;
MixedViewportLayoutState _layoutState = new MixedViewportLayoutState();
List<CardModel> _cardModels;
DismissDirection _dismissDirection = DismissDirection.horizontal;
bool _drawerShowing = false;
AnimationStatus _drawerStatus = AnimationStatus.dismissed;
void initState() {
List<double> cardHeights = <double>[
......@@ -32,7 +36,7 @@ class CardCollectionApp extends App {
48.0, 63.0, 82.0, 146.0, 60.0, 55.0, 84.0, 96.0, 50.0,
48.0, 63.0, 82.0, 146.0, 60.0, 55.0, 84.0, 96.0, 50.0
];
cardModels = new List.generate(cardHeights.length, (i) {
_cardModels = new List.generate(cardHeights.length, (i) {
Color color = Color.lerp(colors.Red[300], colors.Blue[900], i / cardHeights.length);
return new CardModel(i, cardHeights[i], color);
});
......@@ -40,20 +44,91 @@ class CardCollectionApp extends App {
}
void dismissCard(CardModel card) {
if (cardModels.contains(card)) {
if (_cardModels.contains(card)) {
setState(() {
_cardModels.remove(card);
});
}
}
void _handleOpenDrawer() {
setState(() {
_drawerShowing = true;
_drawerStatus = AnimationStatus.forward;
});
}
void _handleDrawerDismissed() {
setState(() {
cardModels.remove(card);
_drawerStatus = AnimationStatus.dismissed;
});
}
String dismissDirectionText(DismissDirection direction) {
String s = direction.toString();
return "dismiss ${s.substring(s.indexOf('.') + 1)}";
}
void changeDismissDirection(DismissDirection newDismissDirection) {
setState(() {
_dismissDirection = newDismissDirection;
_drawerStatus = AnimationStatus.dismissed;
});
}
Widget buildDrawer() {
if (_drawerStatus == AnimationStatus.dismissed)
return null;
Widget buildDrawerItem(DismissDirection direction, String icon) {
return new DrawerItem(
icon: icon,
onPressed: () { changeDismissDirection(direction); },
child: new Row([
new Flexible(child: new Text(dismissDirectionText(direction))),
new Radio(
value: direction,
onChanged: changeDismissDirection,
groupValue: _dismissDirection
)
])
);
}
return new IconTheme(
data: const IconThemeData(color: IconThemeColor.black),
child: new Drawer(
level: 3,
showing: _drawerShowing,
onDismissed: _handleDrawerDismissed,
children: [
new DrawerHeader(child: new Text('Dismiss Direction')),
buildDrawerItem(DismissDirection.horizontal, 'action/code'),
buildDrawerItem(DismissDirection.left, 'navigation/arrow_back'),
buildDrawerItem(DismissDirection.right, 'navigation/arrow_forward')
]
)
);
}
Widget buildToolBar() {
return new ToolBar(
left: new IconButton(icon: "navigation/menu", onPressed: _handleOpenDrawer),
center: new Text('Swipe Away'),
right: [
new Text(dismissDirectionText(_dismissDirection))
]
);
}
Widget builder(int index) {
if (index >= cardModels.length)
Widget buildCard(int index) {
if (index >= _cardModels.length)
return null;
CardModel cardModel = cardModels[index];
CardModel cardModel = _cardModels[index];
Widget card = new Dismissable(
onResized: () { layoutState.invalidate([index]); },
direction: _dismissDirection,
onResized: () { _layoutState.invalidate([index]); },
onDismissed: () { dismissCard(cardModel); },
child: new Card(
color: cardModel.color,
......@@ -65,8 +140,28 @@ class CardCollectionApp extends App {
)
);
Widget backgroundText =
new Text("Swipe in either direction", style: backgroundTextStyle);
String backgroundMessage;
switch(_dismissDirection) {
case DismissDirection.horizontal:
backgroundMessage = "Swipe in either direction";
break;
case DismissDirection.left:
backgroundMessage = "Swipe left to dismiss";
break;
case DismissDirection.right:
backgroundMessage = "Swipe right to dismiss";
break;
default:
backgroundMessage = "Unsupported dismissDirection";
}
Widget leftArrowIcon = new Icon(type: 'navigation/arrow_back', size: 36);
if (_dismissDirection == DismissDirection.right)
leftArrowIcon = new Opacity(opacity: 0.1, child: leftArrowIcon);
Widget rightArrowIcon = new Icon(type: 'navigation/arrow_forward', size: 36);
if (_dismissDirection == DismissDirection.left)
rightArrowIcon = new Opacity(opacity: 0.1, child: rightArrowIcon);
// The background Widget appears behind the Dismissable card when the card
// moves to the left or right. The Positioned widget ensures that the
......@@ -85,16 +180,20 @@ class CardCollectionApp extends App {
height: cardModel.height,
decoration: new BoxDecoration(backgroundColor: Theme.of(this).primaryColor),
child: new Row([
new Icon(type: 'navigation/arrow_back', size: 36),
new Flexible(child: backgroundText),
new Icon(type: 'navigation/arrow_forward', size: 36)
leftArrowIcon,
new Flexible(child: new Text(backgroundMessage, style: backgroundTextStyle)),
rightArrowIcon
])
)
)
)
);
return new Stack([background, card], key: cardModel.key);
return new IconTheme(
key: cardModel.key,
data: const IconThemeData(color: IconThemeColor.white),
child: new Stack([background, card])
);
}
Widget build() {
......@@ -102,15 +201,13 @@ class CardCollectionApp extends App {
padding: const EdgeDims.symmetric(vertical: 12.0, horizontal: 8.0),
decoration: new BoxDecoration(backgroundColor: Theme.of(this).primarySwatch[50]),
child: new ScrollableMixedWidgetList(
builder: builder,
token: cardModels.length,
layoutState: layoutState
builder: buildCard,
token: _cardModels.length,
layoutState: _layoutState
)
);
return new IconTheme(
data: const IconThemeData(color: IconThemeColor.white),
child: new Theme(
return new Theme(
data: new ThemeData(
brightness: ThemeBrightness.light,
primarySwatch: colors.Blue,
......@@ -119,11 +216,11 @@ class CardCollectionApp extends App {
child: new Title(
title: 'Cards',
child: new Scaffold(
toolbar: new ToolBar(center: new Text('Swipe Away')),
toolbar: buildToolBar(),
drawer: buildDrawer(),
body: cardCollection
)
)
)
);
}
}
......
......@@ -20,6 +20,15 @@ const double _kMinFlingVelocityDelta = 400.0;
const double _kFlingVelocityScale = 1.0 / 300.0;
const double _kDismissCardThreshold = 0.4;
enum DismissDirection {
vertical,
horizontal,
left,
right,
up,
down
}
typedef void ResizedCallback();
typedef void DismissedCallback();
......@@ -29,41 +38,51 @@ class Dismissable extends StatefulComponent {
Key key,
this.child,
this.onResized,
this.onDismissed
// TODO(hansmuller): direction
this.onDismissed,
this.direction: DismissDirection.horizontal
}) : super(key: key);
Widget child;
ResizedCallback onResized;
DismissedCallback onDismissed;
DismissDirection direction;
AnimationPerformance _fadePerformance;
AnimationPerformance _resizePerformance;
Size _size;
double _dragX = 0.0;
double _dragExtent = 0.0;
bool _dragUnderway = false;
void initState() {
_fadePerformance = new AnimationPerformance(duration: _kCardDismissFadeout);
}
void _handleFadeCompleted() {
if (!_dragUnderway)
_startResizePerformance();
}
void syncConstructorArguments(Dismissable source) {
child = source.child;
onResized = source.onResized;
onDismissed = source.onDismissed;
direction = source.direction;
}
bool get _directionIsYAxis {
return
direction == DismissDirection.vertical ||
direction == DismissDirection.up ||
direction == DismissDirection.down;
}
void _handleFadeCompleted() {
if (!_dragUnderway)
_startResizePerformance();
}
Point get _activeCardDragEndPoint {
if (!_isActive)
return Point.origin;
assert(_size != null);
return new Point(_dragX.sign * _size.width * _kDismissCardThreshold, 0.0);
double extent = _directionIsYAxis ? _size.height : _size.width;
return new Point(_dragExtent.sign * extent * _kDismissCardThreshold, 0.0);
}
bool get _isActive {
......@@ -104,19 +123,38 @@ class Dismissable extends StatefulComponent {
if (_fadePerformance.isAnimating)
return;
_dragUnderway = true;
_dragX = 0.0;
_dragExtent = 0.0;
_fadePerformance.progress = 0.0;
}
void _handleScrollUpdate(double scrollOffset) {
if (!_isActive || _fadePerformance.isAnimating)
return;
double oldDragX = _dragX;
_dragX -= scrollOffset;
if (oldDragX.sign != _dragX.sign)
double oldDragExtent = _dragExtent;
switch(direction) {
case DismissDirection.horizontal:
case DismissDirection.vertical:
_dragExtent -= scrollOffset;
break;
case DismissDirection.up:
case DismissDirection.left:
if (_dragExtent - scrollOffset < 0)
_dragExtent -= scrollOffset;
break;
case DismissDirection.down:
case DismissDirection.right:
if (_dragExtent - scrollOffset > 0)
_dragExtent -= scrollOffset;
break;
}
if (oldDragExtent.sign != _dragExtent.sign)
setState(() {}); // Rebuild to update the new drag endpoint.
if (!_fadePerformance.isAnimating)
_fadePerformance.progress = _dragX.abs() / (_size.width * _kDismissCardThreshold);
_fadePerformance.progress = _dragExtent.abs() / (_size.width * _kDismissCardThreshold);
}
_handleScrollEnd() {
......@@ -129,10 +167,33 @@ class Dismissable extends StatefulComponent {
_fadePerformance.reverse();
}
bool _isHorizontalFlingGesture(sky.GestureEvent event) {
double vx = event.velocityX.abs();
double vy = event.velocityY.abs();
return vx - vy > _kMinFlingVelocityDelta && vx > _kMinFlingVelocity;
bool _isFlingGesture(sky.GestureEvent event) {
double vx = event.velocityX;
double vy = event.velocityY;
if (_directionIsYAxis) {
if (vy.abs() - vx.abs() < _kMinFlingVelocityDelta)
return false;
switch(direction) {
case DismissDirection.vertical:
return vy.abs() > _kMinFlingVelocity;
case DismissDirection.up:
return -vy > _kMinFlingVelocity;
default:
return vy > _kMinFlingVelocity;
}
} else {
if (vx.abs() - vy.abs() < _kMinFlingVelocityDelta)
return false;
switch(direction) {
case DismissDirection.horizontal:
return vx.abs() > _kMinFlingVelocity;
case DismissDirection.left:
return -vx > _kMinFlingVelocity;
default:
return vx > _kMinFlingVelocity;
}
}
return false;
}
EventDisposition _handleFlingStart(sky.GestureEvent event) {
......@@ -142,9 +203,10 @@ class Dismissable extends StatefulComponent {
_dragUnderway = false;
if (_fadePerformance.isCompleted) { // drag then fling
_startResizePerformance();
} else if (_isHorizontalFlingGesture(event)) {
_dragX = event.velocityX.sign;
_fadePerformance.fling(velocity: event.velocityX.abs() * _kFlingVelocityScale);
} else if (_isFlingGesture(event)) {
double velocity = _directionIsYAxis ? event.velocityY : event.velocityX;
_dragExtent = velocity.sign;
_fadePerformance.fling(velocity: velocity.abs() * _kFlingVelocityScale);
} else {
_fadePerformance.reverse();
}
......@@ -160,8 +222,8 @@ class Dismissable extends StatefulComponent {
Widget build() {
if (_resizePerformance != null) {
AnimatedValue<double> dismissHeight = new AnimatedValue<double>(
_size.height,
AnimatedValue<double> squashAxisExtent = new AnimatedValue<double>(
_directionIsYAxis ? _size.width : _size.height,
end: 0.0,
curve: ease,
interval: _kCardDismissResizeInterval
......@@ -170,13 +232,18 @@ class Dismissable extends StatefulComponent {
return new SquashTransition(
performance: _resizePerformance,
direction: Direction.forward,
height: dismissHeight);
width: _directionIsYAxis ? squashAxisExtent : null,
height: !_directionIsYAxis ? squashAxisExtent : null
);
}
return new GestureDetector(
onHorizontalScrollStart: _handleScrollStart,
onHorizontalScrollUpdate: _handleScrollUpdate,
onHorizontalScrollEnd: _handleScrollEnd,
onHorizontalScrollStart: _directionIsYAxis ? null : _handleScrollStart,
onHorizontalScrollUpdate: _directionIsYAxis ? null : _handleScrollUpdate,
onHorizontalScrollEnd: _directionIsYAxis ? null : _handleScrollEnd,
onVerticalScrollStart: _directionIsYAxis ? _handleScrollStart : null,
onVerticalScrollUpdate: _directionIsYAxis ? _handleScrollUpdate : null,
onVerticalScrollEnd: _directionIsYAxis ? _handleScrollEnd : null,
child: new Listener(
onGestureFlingStart: _handleFlingStart,
child: new SizeObserver(
......
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