| OLD | NEW |
| 1 // Copyright 2015 The Chromium Authors. All rights reserved. | 1 // Copyright 2015 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | 4 |
| 5 import 'dart:sky' as sky; | 5 import 'dart:sky' as sky; |
| 6 | 6 |
| 7 import 'package:sky/animation/animation_performance.dart'; | 7 import 'package:sky/animation/animation_performance.dart'; |
| 8 import 'package:sky/animation/curves.dart'; | 8 import 'package:sky/animation/curves.dart'; |
| 9 import 'package:sky/theme/shadows.dart'; | 9 import 'package:sky/theme/shadows.dart'; |
| 10 import 'package:sky/widgets/animated_component.dart'; | 10 import 'package:sky/widgets/animated_component.dart'; |
| 11 import 'package:sky/widgets/animation_builder.dart'; | 11 import 'package:sky/widgets/animation_builder.dart'; |
| 12 import 'package:sky/widgets/basic.dart'; | 12 import 'package:sky/widgets/basic.dart'; |
| 13 import 'package:sky/widgets/navigator.dart'; |
| 13 import 'package:sky/widgets/scrollable_viewport.dart'; | 14 import 'package:sky/widgets/scrollable_viewport.dart'; |
| 14 import 'package:sky/widgets/theme.dart'; | 15 import 'package:sky/widgets/theme.dart'; |
| 15 | 16 |
| 16 // TODO(eseidel): Draw width should vary based on device size: | 17 // TODO(eseidel): Draw width should vary based on device size: |
| 17 // http://www.google.com/design/spec/layout/structure.html#structure-side-nav | 18 // http://www.google.com/design/spec/layout/structure.html#structure-side-nav |
| 18 | 19 |
| 19 // Mobile: | 20 // Mobile: |
| 20 // Width = Screen width − 56 dp | 21 // Width = Screen width − 56 dp |
| 21 // Maximum width: 320dp | 22 // Maximum width: 320dp |
| 22 // Maximum width applies only when using a left nav. When using a right nav, | 23 // Maximum width applies only when using a left nav. When using a right nav, |
| 23 // the panel can cover the full width of the screen. | 24 // the panel can cover the full width of the screen. |
| 24 | 25 |
| 25 // Desktop/Tablet: | 26 // Desktop/Tablet: |
| 26 // Maximum width for a left nav is 400dp. | 27 // Maximum width for a left nav is 400dp. |
| 27 // The right nav can vary depending on content. | 28 // The right nav can vary depending on content. |
| 28 | 29 |
| 29 const double _kWidth = 304.0; | 30 const double _kWidth = 304.0; |
| 30 const double _kMinFlingVelocity = 0.4; | 31 const double _kMinFlingVelocity = 0.4; |
| 31 const Duration _kBaseSettleDuration = const Duration(milliseconds: 246); | 32 const Duration _kBaseSettleDuration = const Duration(milliseconds: 246); |
| 32 // TODO(mpcomplete): The curve must be linear if we want the drawer to track | 33 // TODO(mpcomplete): The curve must be linear if we want the drawer to track |
| 33 // the user's finger. Odeon remedies this by attaching spring forces to the | 34 // the user's finger. Odeon remedies this by attaching spring forces to the |
| 34 // initial timeline when animating (so it doesn't look linear). | 35 // initial timeline when animating (so it doesn't look linear). |
| 35 const Curve _kAnimationCurve = linear; | 36 const Curve _kAnimationCurve = linear; |
| 36 | 37 |
| 37 typedef void DrawerStatusChangeHandler (bool showing); | 38 typedef void DrawerStatusChangeHandler (bool showing); |
| 38 | 39 |
| 39 class DrawerController { | 40 enum DrawerStatus { |
| 40 DrawerController(this.onStatusChange) { | 41 active, |
| 41 builder = new AnimationBuilder() | 42 inactive, |
| 42 ..position = new AnimatedType<Point>( | 43 } |
| 43 new Point(-_kWidth, 0.0), end: Point.origin, curve: _kAnimationCurve); | |
| 44 performance = builder.createPerformance([builder.position], | |
| 45 duration: _kBaseSettleDuration) | |
| 46 ..addListener(_checkValue); | |
| 47 } | |
| 48 final DrawerStatusChangeHandler onStatusChange; | |
| 49 | 44 |
| 50 AnimationPerformance performance; | 45 typedef void DrawerStatusChangedCallback(DrawerStatus status); |
| 51 AnimationBuilder builder; | |
| 52 | |
| 53 double get xPosition => builder.position.value.x; | |
| 54 | |
| 55 bool _oldClosedState = true; | |
| 56 void _checkValue() { | |
| 57 var newClosedState = isClosed; | |
| 58 if (onStatusChange != null && _oldClosedState != newClosedState) { | |
| 59 onStatusChange(!newClosedState); | |
| 60 _oldClosedState = newClosedState; | |
| 61 } | |
| 62 } | |
| 63 | |
| 64 bool get isClosed => performance.isDismissed; | |
| 65 bool get _isMostlyClosed => xPosition <= -_kWidth/2; | |
| 66 | |
| 67 void open() => performance.play(); | |
| 68 | |
| 69 void close() => performance.reverse(); | |
| 70 | |
| 71 void _settle() => _isMostlyClosed ? close() : open(); | |
| 72 | |
| 73 void handleMaskTap(_) => close(); | |
| 74 | |
| 75 // TODO(mpcomplete): Figure out how to generalize these handlers on a | |
| 76 // "PannableThingy" interface. | |
| 77 void handlePointerDown(_) => performance.stop(); | |
| 78 | |
| 79 void handlePointerMove(sky.PointerEvent event) { | |
| 80 if (performance.isAnimating) | |
| 81 return; | |
| 82 performance.progress += event.dx / _kWidth; | |
| 83 } | |
| 84 | |
| 85 void handlePointerUp(_) { | |
| 86 if (!performance.isAnimating) | |
| 87 _settle(); | |
| 88 } | |
| 89 | |
| 90 void handlePointerCancel(_) { | |
| 91 if (!performance.isAnimating) | |
| 92 _settle(); | |
| 93 } | |
| 94 | |
| 95 void handleFlingStart(event) { | |
| 96 double velocityX = event.velocityX / 1000; | |
| 97 if (velocityX.abs() >= _kMinFlingVelocity) | |
| 98 performance.fling(velocity: velocityX / _kWidth); | |
| 99 } | |
| 100 } | |
| 101 | 46 |
| 102 class Drawer extends AnimatedComponent { | 47 class Drawer extends AnimatedComponent { |
| 103 Drawer({ | 48 Drawer({ |
| 104 String key, | 49 String key, |
| 105 this.controller, | |
| 106 this.children, | 50 this.children, |
| 107 this.level: 0 | 51 this.showing: false, |
| 52 this.level: 0, |
| 53 this.onStatusChanged, |
| 54 this.navigator |
| 108 }) : super(key: key); | 55 }) : super(key: key); |
| 109 | 56 |
| 110 List<Widget> children; | 57 List<Widget> children; |
| 58 bool showing; |
| 111 int level; | 59 int level; |
| 112 DrawerController controller; | 60 DrawerStatusChangedCallback onStatusChanged; |
| 61 Navigator navigator; |
| 62 |
| 63 AnimationPerformance _performance; |
| 64 AnimationBuilder _builder; |
| 113 | 65 |
| 114 void initState() { | 66 void initState() { |
| 115 watch(controller.performance); | 67 _builder = new AnimationBuilder() |
| 68 ..position = new AnimatedType<Point>( |
| 69 new Point(-_kWidth, 0.0), end: Point.origin, curve: _kAnimationCurve); |
| 70 _performance = _builder.createPerformance([_builder.position], |
| 71 duration: _kBaseSettleDuration) |
| 72 ..addListener(_checkForStateChanged); |
| 73 watch(_performance); |
| 74 if (showing) |
| 75 _performance.play(); |
| 116 } | 76 } |
| 117 | 77 |
| 118 void syncFields(Drawer source) { | 78 void syncFields(Drawer source) { |
| 79 const String kDrawerRouteName = "[open drawer]"; |
| 119 children = source.children; | 80 children = source.children; |
| 120 level = source.level; | 81 level = source.level; |
| 121 controller = source.controller; | 82 navigator = source.navigator; |
| 83 if (showing != source.showing) { |
| 84 showing = source.showing; |
| 85 if (showing) { |
| 86 if (navigator != null) { |
| 87 navigator.pushState(kDrawerRouteName, (_) { |
| 88 onStatusChanged(DrawerStatus.inactive); |
| 89 }); |
| 90 } |
| 91 _performance.play(); |
| 92 } else { |
| 93 if (navigator != null && navigator.currentRoute.name == kDrawerRouteName
) |
| 94 navigator.pop(); |
| 95 _performance.reverse(); |
| 96 } |
| 97 } |
| 98 onStatusChanged = source.onStatusChanged; |
| 122 super.syncFields(source); | 99 super.syncFields(source); |
| 123 } | 100 } |
| 124 | 101 |
| 125 // TODO(mpcomplete): the animation system should handle building, maybe? Or | 102 // TODO(mpcomplete): the animation system should handle building, maybe? Or |
| 126 // at least setting the transform. Figure out how this could work for things | 103 // at least setting the transform. Figure out how this could work for things |
| 127 // like fades, slides, rotates, pinch, etc. | 104 // like fades, slides, rotates, pinch, etc. |
| 128 Widget build() { | 105 Widget build() { |
| 129 // TODO(mpcomplete): animate as a fade-in. | 106 // TODO(mpcomplete): animate as a fade-in. |
| 130 double scaler = controller.performance.progress; | 107 double scaler = _performance.progress; |
| 131 Color maskColor = new Color.fromARGB((0x7F * scaler).floor(), 0, 0, 0); | 108 Color maskColor = new Color.fromARGB((0x7F * scaler).floor(), 0, 0, 0); |
| 132 | 109 |
| 133 var mask = new Listener( | 110 var mask = new Listener( |
| 134 child: new Container(decoration: new BoxDecoration(backgroundColor: maskCo
lor)), | 111 child: new Container(decoration: new BoxDecoration(backgroundColor: maskCo
lor)), |
| 135 onGestureTap: controller.handleMaskTap | 112 onGestureTap: handleMaskTap |
| 136 ); | 113 ); |
| 137 | 114 |
| 138 Widget content = controller.builder.build( | 115 Widget content = _builder.build( |
| 139 new Container( | 116 new Container( |
| 140 decoration: new BoxDecoration( | 117 decoration: new BoxDecoration( |
| 141 backgroundColor: Theme.of(this).canvasColor, | 118 backgroundColor: Theme.of(this).canvasColor, |
| 142 boxShadow: shadows[level]), | 119 boxShadow: shadows[level]), |
| 143 width: _kWidth, | 120 width: _kWidth, |
| 144 child: new ScrollableBlock(children) | 121 child: new ScrollableBlock(children) |
| 145 )); | 122 )); |
| 146 | 123 |
| 147 return new Listener( | 124 return new Listener( |
| 148 child: new Stack([ mask, content ]), | 125 child: new Stack([ mask, content ]), |
| 149 onPointerDown: controller.handlePointerDown, | 126 onPointerDown: handlePointerDown, |
| 150 onPointerMove: controller.handlePointerMove, | 127 onPointerMove: handlePointerMove, |
| 151 onPointerUp: controller.handlePointerUp, | 128 onPointerUp: handlePointerUp, |
| 152 onPointerCancel: controller.handlePointerCancel, | 129 onPointerCancel: handlePointerCancel, |
| 153 onGestureFlingStart: controller.handleFlingStart | 130 onGestureFlingStart: handleFlingStart |
| 154 ); | 131 ); |
| 155 } | 132 } |
| 156 | 133 |
| 134 double get xPosition => _builder.position.value.x; |
| 135 |
| 136 DrawerStatus _lastStatus; |
| 137 void _checkForStateChanged() { |
| 138 DrawerStatus status = _status; |
| 139 if (_lastStatus != null && status != _lastStatus && onStatusChanged != null) |
| 140 onStatusChanged(status); |
| 141 _lastStatus = status; |
| 142 } |
| 143 |
| 144 DrawerStatus get _status => _performance.isDismissed ? DrawerStatus.inactive :
DrawerStatus.active; |
| 145 bool get _isMostlyClosed => xPosition <= -_kWidth/2; |
| 146 |
| 147 void _settle() => _isMostlyClosed ? _performance.reverse() : _performance.play
(); |
| 148 |
| 149 void handleMaskTap(_) => _performance.reverse(); |
| 150 |
| 151 // TODO(mpcomplete): Figure out how to generalize these handlers on a |
| 152 // "PannableThingy" interface. |
| 153 void handlePointerDown(_) => _performance.stop(); |
| 154 |
| 155 void handlePointerMove(sky.PointerEvent event) { |
| 156 if (_performance.isAnimating) |
| 157 return; |
| 158 _performance.progress += event.dx / _kWidth; |
| 159 } |
| 160 |
| 161 void handlePointerUp(_) { |
| 162 if (!_performance.isAnimating) |
| 163 _settle(); |
| 164 } |
| 165 |
| 166 void handlePointerCancel(_) { |
| 167 if (!_performance.isAnimating) |
| 168 _settle(); |
| 169 } |
| 170 |
| 171 void handleFlingStart(event) { |
| 172 double velocityX = event.velocityX / 1000; |
| 173 if (velocityX.abs() >= _kMinFlingVelocity) |
| 174 _performance.fling(velocity: velocityX / _kWidth); |
| 175 } |
| 157 } | 176 } |
| OLD | NEW |