diff --git a/examples/fitness/lib/feed.dart b/examples/fitness/lib/feed.dart index 10da157cf0d..263a558723c 100644 --- a/examples/fitness/lib/feed.dart +++ b/examples/fitness/lib/feed.dart @@ -80,6 +80,7 @@ class FeedFragment extends StatefulComponent { onItemDeleted = source.onItemDeleted; } + AnimationStatus _snackBarStatus = AnimationStatus.dismissed; bool _isShowingSnackBar = false; EventDisposition _handleFitnessModeChange(FitnessMode value) { @@ -168,6 +169,7 @@ class FeedFragment extends StatefulComponent { setState(() { _undoItem = item; _isShowingSnackBar = true; + _snackBarStatus = AnimationStatus.forward; }); } @@ -205,13 +207,16 @@ class FeedFragment extends StatefulComponent { }); } + Anchor _snackBarAnchor = new Anchor(); Widget buildSnackBar() { - if (!_isShowingSnackBar) + if (_snackBarStatus == AnimationStatus.dismissed) return null; return new SnackBar( showing: _isShowingSnackBar, + anchor: _snackBarAnchor, content: new Text("Item deleted."), - actions: [new SnackBarAction(label: "UNDO", onPressed: _handleUndo)] + actions: [new SnackBarAction(label: "UNDO", onPressed: _handleUndo)], + onDismissed: () { setState(() { _snackBarStatus = AnimationStatus.dismissed; }); } ); } @@ -225,10 +230,11 @@ class FeedFragment extends StatefulComponent { Widget buildFloatingActionButton() { switch (_fitnessMode) { case FitnessMode.feed: - return new FloatingActionButton( - child: new Icon(type: 'content/add', size: 24), - onPressed: _handleActionButtonPressed - ); + return _snackBarAnchor.build( + new FloatingActionButton( + child: new Icon(type: 'content/add', size: 24), + onPressed: _handleActionButtonPressed + )); case FitnessMode.chart: return null; } diff --git a/examples/stocks/lib/stock_home.dart b/examples/stocks/lib/stock_home.dart index 6f6eac67e52..43f7f04c383 100644 --- a/examples/stocks/lib/stock_home.dart +++ b/examples/stocks/lib/stock_home.dart @@ -247,11 +247,13 @@ class StockHome extends StatefulComponent { }); } + Anchor _snackBarAnchor = new Anchor(); Widget buildSnackBar() { if (_snackBarStatus == AnimationStatus.dismissed) return null; return new SnackBar( showing: _isSnackBarShowing, + anchor: _snackBarAnchor, content: new Text("Stock purchased!"), actions: [new SnackBarAction(label: "UNDO", onPressed: _handleUndo)], onDismissed: () { setState(() { _snackBarStatus = AnimationStatus.dismissed; }); } @@ -266,11 +268,12 @@ class StockHome extends StatefulComponent { } Widget buildFloatingActionButton() { - return new FloatingActionButton( - child: new Icon(type: 'content/add', size: 24), - backgroundColor: colors.RedAccent[200], - onPressed: _handleStockPurchased - ); + return _snackBarAnchor.build( + new FloatingActionButton( + child: new Icon(type: 'content/add', size: 24), + backgroundColor: colors.RedAccent[200], + onPressed: _handleStockPurchased + )); } void addMenuToOverlays(List overlays) { diff --git a/sky/packages/sky/lib/widgets/animated_component.dart b/sky/packages/sky/lib/widgets/animated_component.dart index 3ebb9c55670..7cb047e52d5 100644 --- a/sky/packages/sky/lib/widgets/animated_component.dart +++ b/sky/packages/sky/lib/widgets/animated_component.dart @@ -20,13 +20,22 @@ abstract class AnimatedComponent extends StatefulComponent { }); } + bool isWatching(performance) => _watchedPerformances.contains(performance); + void watch(AnimationPerformance performance) { - assert(!_watchedPerformances.contains(performance)); + assert(!isWatching(performance)); _watchedPerformances.add(performance); if (mounted) performance.addListener(_performanceChanged); } + void unwatch(AnimationPerformance performance) { + assert(isWatching(performance)); + _watchedPerformances.remove(performance); + if (mounted) + performance.removeListener(_performanceChanged); + } + void didMount() { for (AnimationPerformance performance in _watchedPerformances) performance.addListener(_performanceChanged); diff --git a/sky/packages/sky/lib/widgets/navigator.dart b/sky/packages/sky/lib/widgets/navigator.dart index 1e83a3d3df2..ee36b7cef59 100644 --- a/sky/packages/sky/lib/widgets/navigator.dart +++ b/sky/packages/sky/lib/widgets/navigator.dart @@ -85,7 +85,7 @@ class Transition extends TransitionBase { super.syncFields(source); } - Widget build() { + Widget buildWithChild(Widget child) { // TODO(jackson): Hit testing should ignore transform // TODO(jackson): Block input unless content is interactive return new SlideTransition( diff --git a/sky/packages/sky/lib/widgets/scaffold.dart b/sky/packages/sky/lib/widgets/scaffold.dart index cba929c4d50..835e5891f9d 100644 --- a/sky/packages/sky/lib/widgets/scaffold.dart +++ b/sky/packages/sky/lib/widgets/scaffold.dart @@ -9,6 +9,8 @@ import 'package:sky/rendering/object.dart'; import 'package:sky/theme/view_configuration.dart'; import 'package:sky/widgets/framework.dart'; +import 'package:vector_math/vector_math.dart'; + // Slots are painted in this order and hit tested in reverse of this order enum ScaffoldSlots { body, @@ -117,19 +119,18 @@ class RenderScaffold extends RenderBox { assert(body.parentData is BoxParentData); body.parentData.position = new Point(0.0, bodyPosition); } - double snackBarHeight = 0.0; if (_slots[ScaffoldSlots.snackBar] != null) { RenderBox snackBar = _slots[ScaffoldSlots.snackBar]; // TODO(jackson): On tablet/desktop, minWidth = 288, maxWidth = 568 snackBar.layout(new BoxConstraints(minWidth: size.width, maxWidth: size.width, minHeight: 0.0, maxHeight: size.height), parentUsesSize: true); assert(snackBar.parentData is BoxParentData); - snackBar.parentData.position = new Point(0.0, size.height - snackBar.size.height); - snackBarHeight = snackBar.size.height; + // Position it off-screen. SnackBar slides in with an animation. + snackBar.parentData.position = new Point(0.0, size.height); } if (_slots[ScaffoldSlots.floatingActionButton] != null) { RenderBox floatingActionButton = _slots[ScaffoldSlots.floatingActionButton]; - Size area = new Size(size.width - kButtonX, size.height - kButtonY - snackBarHeight); + Size area = new Size(size.width - kButtonX, size.height - kButtonY); floatingActionButton.layout(new BoxConstraints.loose(area), parentUsesSize: true); assert(floatingActionButton.parentData is BoxParentData); floatingActionButton.parentData.position = (area - floatingActionButton.size).toPoint(); @@ -152,12 +153,32 @@ class RenderScaffold extends RenderBox { } } + static Point _transformPoint(Matrix4 transform, Point point) { + Vector3 position3 = new Vector3(point.x, point.y, 0.0); + Vector3 transformed3 = transform.transform3(position3); + return new Point(transformed3.x, transformed3.y); + } + + Point parentToLocal(RenderBox box, Point point) { + assert(attached); + Matrix4 transform = new Matrix4.identity(); + box.applyPaintTransform(transform); + /* double det = */ transform.invert(); + // TODO(abarth): Check the determinant for degeneracy. + return _transformPoint(transform, point); + } + void hitTestChildren(HitTestResult result, { Point position }) { for (ScaffoldSlots slot in ScaffoldSlots.values.reversed) { RenderBox box = _slots[slot]; if (box != null) { assert(box.parentData is BoxParentData); - if ((box.parentData.position & box.size).contains(position)) { + // TODO(abarth): Need to solve this problem in general. + // Apply the box's transform to check if it contains position. + // But when we pass the position to box.hitTest, we only want to apply + // the top-level transform (box will apply its own transforms). + Point local = parentToLocal(box, position); + if ((Point.origin & box.size).contains(local)) { if (box.hitTest(result, position: (position - box.parentData.position).toPoint())) return; } diff --git a/sky/packages/sky/lib/widgets/snack_bar.dart b/sky/packages/sky/lib/widgets/snack_bar.dart index 46cc64944c0..642bcb76b90 100644 --- a/sky/packages/sky/lib/widgets/snack_bar.dart +++ b/sky/packages/sky/lib/widgets/snack_bar.dart @@ -6,6 +6,7 @@ import 'dart:async'; import 'package:sky/animation/animated_value.dart'; import 'package:sky/animation/animation_performance.dart'; +import 'package:sky/animation/curves.dart'; import 'package:sky/painting/text_style.dart'; import 'package:sky/theme/typography.dart' as typography; import 'package:sky/widgets/basic.dart'; @@ -43,6 +44,7 @@ class SnackBar extends Component { SnackBar({ Key key, + this.anchor, this.content, this.actions, this.showing, @@ -51,6 +53,7 @@ class SnackBar extends Component { assert(content != null); } + Anchor anchor; Widget content; List actions; bool showing; @@ -79,9 +82,11 @@ class SnackBar extends Component { return new SlideTransition( duration: _kSlideInDuration, direction: showing ? Direction.forward : Direction.reverse, - position: new AnimatedValue(const Point(0.0, 50.0), - end: Point.origin), + position: new AnimatedValue(Point.origin, + end: const Point(0.0, -52.0), + curve: easeIn, reverseCurve: easeOut), onDismissed: _onDismissed, + anchor: anchor, child: new Material( level: 2, color: const Color(0xFF323232), diff --git a/sky/packages/sky/lib/widgets/transitions.dart b/sky/packages/sky/lib/widgets/transitions.dart index 44abbf3cf62..dbb2661dd39 100644 --- a/sky/packages/sky/lib/widgets/transitions.dart +++ b/sky/packages/sky/lib/widgets/transitions.dart @@ -10,9 +10,57 @@ import 'package:vector_math/vector_math.dart'; dynamic _maybe(AnimatedValue x) => x != null ? x.value : null; +// A helper class to anchor widgets to one another. Pass an instance of this to +// a Transition, then use the build() method to create a child with the same +// transition applied. +class Anchor { + Anchor(); + + TransitionBase transition; + + Widget build(Widget child) { + return new _AnchorTransition(anchoredTo: this, child: child); + } +} + +// Used with the Anchor class to apply a transition to multiple children. +class _AnchorTransition extends AnimatedComponent { + _AnchorTransition({ + Key key, + this.anchoredTo, + this.child + }) : super(key: key); + + Anchor anchoredTo; + Widget child; + TransitionBase get transition => anchoredTo.transition; + + void initState() { + if (transition != null) + watch(transition.performance); + } + + void syncFields(_AnchorTransition source) { + if (transition != null && isWatching(transition.performance)) + unwatch(transition.performance); + anchoredTo = source.anchoredTo; + if (transition != null) + watch(transition.performance); + child = source.child; + super.syncFields(source); + } + + Widget build() { + if (transition == null) + return child; + return transition.buildWithChild(child); + } +} + abstract class TransitionBase extends AnimatedComponent { TransitionBase({ Key key, + this.anchor, this.child, this.direction, this.duration, @@ -22,6 +70,7 @@ abstract class TransitionBase extends AnimatedComponent { }) : super(key: key); Widget child; + Anchor anchor; Direction direction; Duration duration; AnimationPerformance performance; @@ -29,6 +78,9 @@ abstract class TransitionBase extends AnimatedComponent { Function onCompleted; void initState() { + if (anchor != null) + anchor.transition = this; + if (performance == null) { assert(duration != null); performance = new AnimationPerformance(duration: duration); @@ -67,7 +119,11 @@ abstract class TransitionBase extends AnimatedComponent { } } - Widget build(); + Widget build() { + return buildWithChild(child); + } + + Widget buildWithChild(Widget child); } class SlideTransition extends TransitionBase { @@ -75,6 +131,7 @@ class SlideTransition extends TransitionBase { // to super. Is there a simpler way? SlideTransition({ Key key, + Anchor anchor, this.position, Duration duration, AnimationPerformance performance, @@ -83,6 +140,7 @@ class SlideTransition extends TransitionBase { Function onCompleted, Widget child }) : super(key: key, + anchor: anchor, duration: duration, performance: performance, direction: direction, @@ -97,7 +155,7 @@ class SlideTransition extends TransitionBase { super.syncFields(source); } - Widget build() { + Widget buildWithChild(Widget child) { performance.updateVariable(position); Matrix4 transform = new Matrix4.identity() ..translate(position.value.x, position.value.y); @@ -108,6 +166,7 @@ class SlideTransition extends TransitionBase { class FadeTransition extends TransitionBase { FadeTransition({ Key key, + Anchor anchor, this.opacity, Duration duration, AnimationPerformance performance, @@ -116,6 +175,7 @@ class FadeTransition extends TransitionBase { Function onCompleted, Widget child }) : super(key: key, + anchor: anchor, duration: duration, performance: performance, direction: direction, @@ -130,7 +190,7 @@ class FadeTransition extends TransitionBase { super.syncFields(source); } - Widget build() { + Widget buildWithChild(Widget child) { performance.updateVariable(opacity); return new Opacity(opacity: opacity.value, child: child); } @@ -139,6 +199,7 @@ class FadeTransition extends TransitionBase { class ColorTransition extends TransitionBase { ColorTransition({ Key key, + Anchor anchor, this.color, Duration duration, AnimationPerformance performance, @@ -147,6 +208,7 @@ class ColorTransition extends TransitionBase { Function onCompleted, Widget child }) : super(key: key, + anchor: anchor, duration: duration, performance: performance, direction: direction, @@ -161,7 +223,7 @@ class ColorTransition extends TransitionBase { super.syncFields(source); } - Widget build() { + Widget buildWithChild(Widget child) { performance.updateVariable(color); return new DecoratedBox( decoration: new BoxDecoration(backgroundColor: color.value), @@ -173,6 +235,7 @@ class ColorTransition extends TransitionBase { class SquashTransition extends TransitionBase { SquashTransition({ Key key, + Anchor anchor, this.width, this.height, Duration duration, @@ -182,6 +245,7 @@ class SquashTransition extends TransitionBase { Function onCompleted, Widget child }) : super(key: key, + anchor: anchor, duration: duration, performance: performance, direction: direction, @@ -198,7 +262,7 @@ class SquashTransition extends TransitionBase { super.syncFields(source); } - Widget build() { + Widget buildWithChild(Widget child) { if (width != null) performance.updateVariable(width); if (height != null) @@ -212,6 +276,7 @@ typedef Widget BuilderFunction(); class BuilderTransition extends TransitionBase { BuilderTransition({ Key key, + Anchor anchor, this.variables, this.builder, Duration duration, @@ -221,6 +286,7 @@ class BuilderTransition extends TransitionBase { Function onCompleted, Widget child }) : super(key: key, + anchor: anchor, duration: duration, performance: performance, direction: direction, @@ -237,7 +303,7 @@ class BuilderTransition extends TransitionBase { super.syncFields(source); } - Widget build() { + Widget buildWithChild(Widget child) { for (int i = 0; i < variables.length; ++i) performance.updateVariable(variables[i]); return builder();