Index: sky/examples/fn/widgets/drawer.dart |
diff --git a/sky/examples/fn/widgets/drawer.dart b/sky/examples/fn/widgets/drawer.dart |
new file mode 100644 |
index 0000000000000000000000000000000000000000..ee0c58b38d015b09320b056ca926ce20f747b187 |
--- /dev/null |
+++ b/sky/examples/fn/widgets/drawer.dart |
@@ -0,0 +1,199 @@ |
+part of widgets; |
+ |
+const double _kWidth = 256.0; |
+const double _kMinFlingVelocity = 0.4; |
+const double _kMinAnimationDurationMS = 246.0; |
+const double _kMaxAnimationDurationMS = 600.0; |
+const Cubic _kAnimationCurve = easeOut; |
+ |
+class DrawerAnimation { |
+ |
+ Stream<double> get onPositionChanged => _controller.stream; |
+ |
+ StreamController _controller; |
+ AnimationGenerator _animation; |
+ double _position; |
+ bool get _isAnimating => _animation != null; |
+ bool get _isMostlyClosed => _position <= -_kWidth / 2; |
+ |
+ DrawerAnimation() { |
+ _controller = new StreamController(sync: true); |
+ _setPosition(-_kWidth); |
+ } |
+ |
+ void toggle(_) => _isMostlyClosed ? _open() : _close(); |
+ |
+ void handleMaskTap(_) => _close(); |
+ |
+ void handlePointerDown(_) => _cancelAnimation(); |
+ |
+ void handlePointerMove(sky.PointerEvent event) { |
+ assert(_animation == null); |
+ _setPosition(_position + event.dx); |
+ } |
+ |
+ void handlePointerUp(_) { |
+ if (!_isAnimating) |
+ _settle(); |
+ } |
+ |
+ void handlePointerCancel(_) { |
+ if (!_isAnimating) |
+ _settle(); |
+ } |
+ |
+ void _open() => _animateToPosition(0.0); |
+ |
+ void _close() => _animateToPosition(-_kWidth); |
+ |
+ void _settle() => _isMostlyClosed ? _close() : _open(); |
+ |
+ void _setPosition(double value) { |
+ _position = math.min(0.0, math.max(value, -_kWidth)); |
+ _controller.add(_position); |
+ } |
+ |
+ void _cancelAnimation() { |
+ if (_animation != null) { |
+ _animation.cancel(); |
+ _animation = null; |
+ } |
+ } |
+ |
+ void _animate(double duration, double begin, double end, Curve curve) { |
+ _cancelAnimation(); |
+ |
+ _animation = new AnimationGenerator(duration, begin: begin, end: end, |
+ curve: curve); |
+ |
+ _animation.onTick.listen(_setPosition, onDone: () { |
+ _animation = null; |
+ }); |
+ } |
+ |
+ void _animateToPosition(double targetPosition) { |
+ double distance = (targetPosition - _position).abs(); |
+ double duration = math.max( |
+ _kMinAnimationDurationMS, |
+ _kMaxAnimationDurationMS * distance / _kWidth); |
+ |
+ _animate(duration, _position, targetPosition, _kAnimationCurve); |
+ } |
+ |
+ void handleFlingStart(event) { |
+ double direction = event.velocityX.sign; |
+ double velocityX = event.velocityX.abs() / 1000; |
+ if (velocityX < _kMinFlingVelocity) |
+ return; |
+ |
+ double targetPosition = direction < 0.0 ? -_kWidth : 0.0; |
+ double distance = (targetPosition - _position).abs(); |
+ double duration = distance / velocityX; |
+ |
+ _animate(duration, _position, targetPosition, linear); |
+ } |
+} |
+ |
+class Drawer extends Component { |
+ |
+ static Style _style = new Style(''' |
+ position: absolute; |
+ z-index: 2; |
+ top: 0; |
+ left: 0; |
+ bottom: 0; |
+ right: 0; |
+ box-shadow: 0 10px 20px rgba(0,0,0,0.19), 0 6px 6px rgba(0,0,0,0.23);''' |
+ ); |
+ |
+ static Style _maskStyle = new Style(''' |
+ background-color: black; |
+ will-change: opacity; |
+ position: absolute; |
+ top: 0; |
+ left: 0; |
+ bottom: 0; |
+ right: 0;''' |
+ ); |
+ |
+ static Style _contentStyle = new Style(''' |
+ background-color: #FAFAFA; |
+ will-change: transform; |
+ position: absolute; |
+ width: 256px; |
+ top: 0; |
+ left: 0; |
+ bottom: 0;''' |
+ ); |
+ |
+ Stream<double> onPositionChanged; |
+ sky.EventListener handleMaskFling; |
+ sky.EventListener handleMaskTap; |
+ sky.EventListener handlePointerCancel; |
+ sky.EventListener handlePointerDown; |
+ sky.EventListener handlePointerMove; |
+ sky.EventListener handlePointerUp; |
+ List<Node> children; |
+ |
+ Drawer({ |
+ Object key, |
+ this.onPositionChanged, |
+ this.handleMaskFling, |
+ this.handleMaskTap, |
+ this.handlePointerCancel, |
+ this.handlePointerDown, |
+ this.handlePointerMove, |
+ this.handlePointerUp, |
+ this.children |
+ }) : super(key: key); |
+ |
+ double _position = -_kWidth; |
+ |
+ bool _listening = false; |
+ |
+ void _ensureListening() { |
+ if (_listening) |
+ return; |
+ |
+ _listening = true; |
+ onPositionChanged.listen((position) { |
+ setState(() { |
+ _position = position; |
+ }); |
+ }); |
+ } |
+ |
+ Node render() { |
+ _ensureListening(); |
+ |
+ bool isClosed = _position <= -_kWidth; |
+ String inlineStyle = 'display: ${isClosed ? 'none' : ''}'; |
+ String maskInlineStyle = 'opacity: ${(_position / _kWidth + 1) * 0.25}'; |
+ String contentInlineStyle = 'transform: translateX(${_position}px)'; |
+ |
+ return new Container( |
+ style: _style, |
+ inlineStyle: inlineStyle, |
+ onPointerDown: handlePointerDown, |
+ onPointerMove: handlePointerMove, |
+ onPointerUp: handlePointerUp, |
+ onPointerCancel: handlePointerCancel, |
+ |
+ children: [ |
+ new Container( |
+ key: 'Mask', |
+ style: _maskStyle, |
+ inlineStyle: maskInlineStyle, |
+ onGestureTap: handleMaskTap, |
+ onFlingStart: handleMaskFling |
+ ), |
+ new Container( |
+ key: 'Content', |
+ style: _contentStyle, |
+ inlineStyle: contentInlineStyle, |
+ children: children |
+ ) |
+ ] |
+ ); |
+ } |
+} |