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 { ...@@ -23,8 +23,12 @@ class CardCollectionApp extends App {
final TextStyle backgroundTextStyle = final TextStyle backgroundTextStyle =
typography.white.title.copyWith(textAlign: TextAlign.center); typography.white.title.copyWith(textAlign: TextAlign.center);
MixedViewportLayoutState layoutState = new MixedViewportLayoutState(); MixedViewportLayoutState _layoutState = new MixedViewportLayoutState();
List<CardModel> cardModels; List<CardModel> _cardModels;
DismissDirection _dismissDirection = DismissDirection.horizontal;
bool _drawerShowing = false;
AnimationStatus _drawerStatus = AnimationStatus.dismissed;
void initState() { void initState() {
List<double> cardHeights = <double>[ List<double> cardHeights = <double>[
...@@ -32,7 +36,7 @@ class CardCollectionApp extends App { ...@@ -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,
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); Color color = Color.lerp(colors.Red[300], colors.Blue[900], i / cardHeights.length);
return new CardModel(i, cardHeights[i], color); return new CardModel(i, cardHeights[i], color);
}); });
...@@ -40,20 +44,91 @@ class CardCollectionApp extends App { ...@@ -40,20 +44,91 @@ class CardCollectionApp extends App {
} }
void dismissCard(CardModel card) { void dismissCard(CardModel card) {
if (cardModels.contains(card)) { if (_cardModels.contains(card)) {
setState(() { setState(() {
cardModels.remove(card); _cardModels.remove(card);
}); });
} }
} }
Widget builder(int index) { void _handleOpenDrawer() {
if (index >= cardModels.length) setState(() {
_drawerShowing = true;
_drawerStatus = AnimationStatus.forward;
});
}
void _handleDrawerDismissed() {
setState(() {
_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 buildCard(int index) {
if (index >= _cardModels.length)
return null; return null;
CardModel cardModel = cardModels[index]; CardModel cardModel = _cardModels[index];
Widget card = new Dismissable( Widget card = new Dismissable(
onResized: () { layoutState.invalidate([index]); }, direction: _dismissDirection,
onResized: () { _layoutState.invalidate([index]); },
onDismissed: () { dismissCard(cardModel); }, onDismissed: () { dismissCard(cardModel); },
child: new Card( child: new Card(
color: cardModel.color, color: cardModel.color,
...@@ -65,8 +140,28 @@ class CardCollectionApp extends App { ...@@ -65,8 +140,28 @@ class CardCollectionApp extends App {
) )
); );
Widget backgroundText = String backgroundMessage;
new Text("Swipe in either direction", style: backgroundTextStyle); 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 // The background Widget appears behind the Dismissable card when the card
// moves to the left or right. The Positioned widget ensures that the // moves to the left or right. The Positioned widget ensures that the
...@@ -85,16 +180,20 @@ class CardCollectionApp extends App { ...@@ -85,16 +180,20 @@ class CardCollectionApp extends App {
height: cardModel.height, height: cardModel.height,
decoration: new BoxDecoration(backgroundColor: Theme.of(this).primaryColor), decoration: new BoxDecoration(backgroundColor: Theme.of(this).primaryColor),
child: new Row([ child: new Row([
new Icon(type: 'navigation/arrow_back', size: 36), leftArrowIcon,
new Flexible(child: backgroundText), new Flexible(child: new Text(backgroundMessage, style: backgroundTextStyle)),
new Icon(type: 'navigation/arrow_forward', size: 36) 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() { Widget build() {
...@@ -102,26 +201,24 @@ class CardCollectionApp extends App { ...@@ -102,26 +201,24 @@ class CardCollectionApp extends App {
padding: const EdgeDims.symmetric(vertical: 12.0, horizontal: 8.0), padding: const EdgeDims.symmetric(vertical: 12.0, horizontal: 8.0),
decoration: new BoxDecoration(backgroundColor: Theme.of(this).primarySwatch[50]), decoration: new BoxDecoration(backgroundColor: Theme.of(this).primarySwatch[50]),
child: new ScrollableMixedWidgetList( child: new ScrollableMixedWidgetList(
builder: builder, builder: buildCard,
token: cardModels.length, token: _cardModels.length,
layoutState: layoutState layoutState: _layoutState
) )
); );
return new IconTheme( return new Theme(
data: const IconThemeData(color: IconThemeColor.white), data: new ThemeData(
child: new Theme( brightness: ThemeBrightness.light,
data: new ThemeData( primarySwatch: colors.Blue,
brightness: ThemeBrightness.light, accentColor: colors.RedAccent[200]
primarySwatch: colors.Blue, ),
accentColor: colors.RedAccent[200] child: new Title(
), title: 'Cards',
child: new Title( child: new Scaffold(
title: 'Cards', toolbar: buildToolBar(),
child: new Scaffold( drawer: buildDrawer(),
toolbar: new ToolBar(center: new Text('Swipe Away')), body: cardCollection
body: cardCollection
)
) )
) )
); );
......
...@@ -20,6 +20,15 @@ const double _kMinFlingVelocityDelta = 400.0; ...@@ -20,6 +20,15 @@ const double _kMinFlingVelocityDelta = 400.0;
const double _kFlingVelocityScale = 1.0 / 300.0; const double _kFlingVelocityScale = 1.0 / 300.0;
const double _kDismissCardThreshold = 0.4; const double _kDismissCardThreshold = 0.4;
enum DismissDirection {
vertical,
horizontal,
left,
right,
up,
down
}
typedef void ResizedCallback(); typedef void ResizedCallback();
typedef void DismissedCallback(); typedef void DismissedCallback();
...@@ -29,41 +38,51 @@ class Dismissable extends StatefulComponent { ...@@ -29,41 +38,51 @@ class Dismissable extends StatefulComponent {
Key key, Key key,
this.child, this.child,
this.onResized, this.onResized,
this.onDismissed this.onDismissed,
// TODO(hansmuller): direction this.direction: DismissDirection.horizontal
}) : super(key: key); }) : super(key: key);
Widget child; Widget child;
ResizedCallback onResized; ResizedCallback onResized;
DismissedCallback onDismissed; DismissedCallback onDismissed;
DismissDirection direction;
AnimationPerformance _fadePerformance; AnimationPerformance _fadePerformance;
AnimationPerformance _resizePerformance; AnimationPerformance _resizePerformance;
Size _size; Size _size;
double _dragX = 0.0; double _dragExtent = 0.0;
bool _dragUnderway = false; bool _dragUnderway = false;
void initState() { void initState() {
_fadePerformance = new AnimationPerformance(duration: _kCardDismissFadeout); _fadePerformance = new AnimationPerformance(duration: _kCardDismissFadeout);
} }
void _handleFadeCompleted() {
if (!_dragUnderway)
_startResizePerformance();
}
void syncConstructorArguments(Dismissable source) { void syncConstructorArguments(Dismissable source) {
child = source.child; child = source.child;
onResized = source.onResized; onResized = source.onResized;
onDismissed = source.onDismissed; 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 { Point get _activeCardDragEndPoint {
if (!_isActive) if (!_isActive)
return Point.origin; return Point.origin;
assert(_size != null); 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 { bool get _isActive {
...@@ -104,19 +123,38 @@ class Dismissable extends StatefulComponent { ...@@ -104,19 +123,38 @@ class Dismissable extends StatefulComponent {
if (_fadePerformance.isAnimating) if (_fadePerformance.isAnimating)
return; return;
_dragUnderway = true; _dragUnderway = true;
_dragX = 0.0; _dragExtent = 0.0;
_fadePerformance.progress = 0.0; _fadePerformance.progress = 0.0;
} }
void _handleScrollUpdate(double scrollOffset) { void _handleScrollUpdate(double scrollOffset) {
if (!_isActive || _fadePerformance.isAnimating) if (!_isActive || _fadePerformance.isAnimating)
return; return;
double oldDragX = _dragX;
_dragX -= scrollOffset; double oldDragExtent = _dragExtent;
if (oldDragX.sign != _dragX.sign) 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. setState(() {}); // Rebuild to update the new drag endpoint.
if (!_fadePerformance.isAnimating) if (!_fadePerformance.isAnimating)
_fadePerformance.progress = _dragX.abs() / (_size.width * _kDismissCardThreshold); _fadePerformance.progress = _dragExtent.abs() / (_size.width * _kDismissCardThreshold);
} }
_handleScrollEnd() { _handleScrollEnd() {
...@@ -129,10 +167,33 @@ class Dismissable extends StatefulComponent { ...@@ -129,10 +167,33 @@ class Dismissable extends StatefulComponent {
_fadePerformance.reverse(); _fadePerformance.reverse();
} }
bool _isHorizontalFlingGesture(sky.GestureEvent event) { bool _isFlingGesture(sky.GestureEvent event) {
double vx = event.velocityX.abs(); double vx = event.velocityX;
double vy = event.velocityY.abs(); double vy = event.velocityY;
return vx - vy > _kMinFlingVelocityDelta && vx > _kMinFlingVelocity; 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) { EventDisposition _handleFlingStart(sky.GestureEvent event) {
...@@ -142,9 +203,10 @@ class Dismissable extends StatefulComponent { ...@@ -142,9 +203,10 @@ class Dismissable extends StatefulComponent {
_dragUnderway = false; _dragUnderway = false;
if (_fadePerformance.isCompleted) { // drag then fling if (_fadePerformance.isCompleted) { // drag then fling
_startResizePerformance(); _startResizePerformance();
} else if (_isHorizontalFlingGesture(event)) { } else if (_isFlingGesture(event)) {
_dragX = event.velocityX.sign; double velocity = _directionIsYAxis ? event.velocityY : event.velocityX;
_fadePerformance.fling(velocity: event.velocityX.abs() * _kFlingVelocityScale); _dragExtent = velocity.sign;
_fadePerformance.fling(velocity: velocity.abs() * _kFlingVelocityScale);
} else { } else {
_fadePerformance.reverse(); _fadePerformance.reverse();
} }
...@@ -160,8 +222,8 @@ class Dismissable extends StatefulComponent { ...@@ -160,8 +222,8 @@ class Dismissable extends StatefulComponent {
Widget build() { Widget build() {
if (_resizePerformance != null) { if (_resizePerformance != null) {
AnimatedValue<double> dismissHeight = new AnimatedValue<double>( AnimatedValue<double> squashAxisExtent = new AnimatedValue<double>(
_size.height, _directionIsYAxis ? _size.width : _size.height,
end: 0.0, end: 0.0,
curve: ease, curve: ease,
interval: _kCardDismissResizeInterval interval: _kCardDismissResizeInterval
...@@ -170,13 +232,18 @@ class Dismissable extends StatefulComponent { ...@@ -170,13 +232,18 @@ class Dismissable extends StatefulComponent {
return new SquashTransition( return new SquashTransition(
performance: _resizePerformance, performance: _resizePerformance,
direction: Direction.forward, direction: Direction.forward,
height: dismissHeight); width: _directionIsYAxis ? squashAxisExtent : null,
height: !_directionIsYAxis ? squashAxisExtent : null
);
} }
return new GestureDetector( return new GestureDetector(
onHorizontalScrollStart: _handleScrollStart, onHorizontalScrollStart: _directionIsYAxis ? null : _handleScrollStart,
onHorizontalScrollUpdate: _handleScrollUpdate, onHorizontalScrollUpdate: _directionIsYAxis ? null : _handleScrollUpdate,
onHorizontalScrollEnd: _handleScrollEnd, onHorizontalScrollEnd: _directionIsYAxis ? null : _handleScrollEnd,
onVerticalScrollStart: _directionIsYAxis ? _handleScrollStart : null,
onVerticalScrollUpdate: _directionIsYAxis ? _handleScrollUpdate : null,
onVerticalScrollEnd: _directionIsYAxis ? _handleScrollEnd : null,
child: new Listener( child: new Listener(
onGestureFlingStart: _handleFlingStart, onGestureFlingStart: _handleFlingStart,
child: new SizeObserver( 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