Factor DrawerController out of Drawer
This patch paves the path for refactoring the Drawer into the Scaffold to support edge swipes and peeks.
This commit is contained in:
@@ -6,8 +6,7 @@ import 'package:flutter/animation.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'colors.dart';
|
||||
import 'shadows.dart';
|
||||
import 'theme.dart';
|
||||
import 'material.dart';
|
||||
|
||||
// TODO(eseidel): Draw width should vary based on device size:
|
||||
// http://www.google.com/design/spec/layout/structure.html#structure-side-nav
|
||||
@@ -24,11 +23,7 @@ import 'theme.dart';
|
||||
|
||||
const double _kWidth = 304.0;
|
||||
const double _kMinFlingVelocity = 365.0;
|
||||
const double _kFlingVelocityScale = 1.0 / 300.0;
|
||||
const Duration _kBaseSettleDuration = const Duration(milliseconds: 246);
|
||||
const Duration _kThemeChangeDuration = const Duration(milliseconds: 200);
|
||||
const Point _kOpenPosition = Point.origin;
|
||||
const Point _kClosedPosition = const Point(-_kWidth, 0.0);
|
||||
|
||||
class _Drawer extends StatelessComponent {
|
||||
_Drawer({ Key key, this.route }) : super(key: key);
|
||||
@@ -38,101 +33,138 @@ class _Drawer extends StatelessComponent {
|
||||
Widget build(BuildContext context) {
|
||||
return new Focus(
|
||||
key: new GlobalObjectKey(route),
|
||||
autofocus: true,
|
||||
child: new GestureDetector(
|
||||
onHorizontalDragStart: (_) {
|
||||
if (route.interactive)
|
||||
route._takeControl();
|
||||
},
|
||||
onHorizontalDragUpdate: (double delta) {
|
||||
if (route.interactive)
|
||||
route._moveDrawer(delta);
|
||||
},
|
||||
onHorizontalDragEnd: (Offset velocity) {
|
||||
if (route.interactive)
|
||||
route._settle(velocity);
|
||||
},
|
||||
child: new Stack(<Widget>[
|
||||
// mask
|
||||
new GestureDetector(
|
||||
onTap: () {
|
||||
if (route.interactive)
|
||||
route._close();
|
||||
},
|
||||
child: new ColorTransition(
|
||||
performance: route.performance,
|
||||
color: new AnimatedColorValue(Colors.transparent, end: Colors.black54),
|
||||
child: new Container()
|
||||
)
|
||||
),
|
||||
new Positioned(
|
||||
top: 0.0,
|
||||
left: 0.0,
|
||||
bottom: 0.0,
|
||||
child: new SlideTransition(
|
||||
performance: route.performance,
|
||||
position: new AnimatedValue<Point>(_kClosedPosition, end: _kOpenPosition),
|
||||
child: new AnimatedContainer(
|
||||
curve: Curves.ease,
|
||||
duration: _kThemeChangeDuration,
|
||||
decoration: new BoxDecoration(
|
||||
backgroundColor: Theme.of(context).canvasColor,
|
||||
boxShadow: shadows[route.level]),
|
||||
width: _kWidth,
|
||||
child: route.child
|
||||
)
|
||||
)
|
||||
)
|
||||
])
|
||||
child: new ConstrainedBox(
|
||||
constraints: const BoxConstraints.expand(width: _kWidth),
|
||||
child: new Material(
|
||||
level: route.level,
|
||||
child: route.child
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class _DrawerRoute extends TransitionRoute {
|
||||
enum _DrawerState {
|
||||
showing,
|
||||
popped,
|
||||
closed,
|
||||
}
|
||||
|
||||
class _DrawerRoute extends OverlayRoute {
|
||||
_DrawerRoute({ this.child, this.level });
|
||||
|
||||
final Widget child;
|
||||
final int level;
|
||||
|
||||
Duration get transitionDuration => _kBaseSettleDuration;
|
||||
bool get opaque => false;
|
||||
|
||||
bool get interactive => _interactive;
|
||||
bool _interactive = true;
|
||||
|
||||
Performance _performance;
|
||||
|
||||
Performance createPerformance() {
|
||||
_performance = super.createPerformance();
|
||||
return _performance;
|
||||
}
|
||||
|
||||
List<WidgetBuilder> get builders => <WidgetBuilder>[ _build ];
|
||||
|
||||
Widget _build(BuildContext context) => new _Drawer(route: this);
|
||||
final GlobalKey<_DrawerControllerState> _drawerKey = new GlobalKey<_DrawerControllerState>();
|
||||
_DrawerState _state = _DrawerState.showing;
|
||||
|
||||
void didPop([dynamic result]) {
|
||||
assert(result == null); // because we don't do anything with it, so otherwise it'd be lost
|
||||
super.didPop(result);
|
||||
_interactive = false;
|
||||
Widget _build(BuildContext context) {
|
||||
return new _DrawerController(
|
||||
key: _drawerKey,
|
||||
settleDuration: _kBaseSettleDuration,
|
||||
onClosed: () {
|
||||
_DrawerState previousState = _state;
|
||||
_state = _DrawerState.closed;
|
||||
switch (previousState) {
|
||||
case _DrawerState.showing:
|
||||
Navigator.of(context).pop();
|
||||
break;
|
||||
case _DrawerState.popped:
|
||||
super.didPop(null);
|
||||
break;
|
||||
case _DrawerState.closed:
|
||||
assert(false);
|
||||
break;
|
||||
}
|
||||
},
|
||||
child: new _Drawer(route: this)
|
||||
);
|
||||
}
|
||||
|
||||
void _takeControl() {
|
||||
assert(_interactive);
|
||||
void didPop(dynamic result) {
|
||||
switch (_state) {
|
||||
case _DrawerState.showing:
|
||||
_drawerKey.currentState?._close();
|
||||
_state = _DrawerState.popped;
|
||||
break;
|
||||
case _DrawerState.popped:
|
||||
assert(false);
|
||||
break;
|
||||
case _DrawerState.closed:
|
||||
super.didPop(null);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _DrawerController extends StatefulComponent {
|
||||
_DrawerController({
|
||||
Key key,
|
||||
this.settleDuration,
|
||||
this.onClosed,
|
||||
this.child
|
||||
}) : super(key: key);
|
||||
|
||||
final Duration settleDuration;
|
||||
final Widget child;
|
||||
final VoidCallback onClosed;
|
||||
|
||||
_DrawerControllerState createState() => new _DrawerControllerState();
|
||||
}
|
||||
|
||||
class _DrawerControllerState extends State<_DrawerController> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
_performance = new Performance(duration: config.settleDuration)
|
||||
..addListener(_performanceChanged)
|
||||
..addStatusListener(_performanceStatusChanged)
|
||||
..play();
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
_performance
|
||||
..removeListener(_performanceChanged)
|
||||
..removeStatusListener(_performanceStatusChanged)
|
||||
..stop();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _performanceChanged() {
|
||||
setState(() {
|
||||
// The performance's state is our build state, and it changed already.
|
||||
});
|
||||
}
|
||||
|
||||
void _performanceStatusChanged(PerformanceStatus status) {
|
||||
if (status == PerformanceStatus.dismissed && config.onClosed != null)
|
||||
config.onClosed();
|
||||
}
|
||||
|
||||
Performance _performance;
|
||||
double _width;
|
||||
|
||||
final AnimatedColorValue _color = new AnimatedColorValue(Colors.transparent, end: Colors.black54);
|
||||
|
||||
void _handleSizeChanged(Size newSize) {
|
||||
setState(() {
|
||||
_width = newSize.width;
|
||||
});
|
||||
}
|
||||
|
||||
void _handlePointerDown(_) {
|
||||
_performance.stop();
|
||||
}
|
||||
|
||||
void _moveDrawer(double delta) {
|
||||
assert(_interactive);
|
||||
_performance.progress += delta / _kWidth;
|
||||
void _move(double delta) {
|
||||
_performance.progress += delta / _width;
|
||||
}
|
||||
|
||||
void _settle(Offset velocity) {
|
||||
assert(_interactive);
|
||||
if (velocity.dx.abs() >= _kMinFlingVelocity) {
|
||||
_performance.fling(velocity: velocity.dx * _kFlingVelocityScale);
|
||||
_performance.fling(velocity: velocity.dx / _width);
|
||||
} else if (_performance.progress < 0.5) {
|
||||
_close();
|
||||
} else {
|
||||
@@ -141,9 +173,43 @@ class _DrawerRoute extends TransitionRoute {
|
||||
}
|
||||
|
||||
void _close() {
|
||||
assert(_interactive);
|
||||
_performance.fling(velocity: -1.0);
|
||||
}
|
||||
|
||||
Widget build(BuildContext context) {
|
||||
_performance.updateVariable(_color);
|
||||
return new GestureDetector(
|
||||
onHorizontalDragUpdate: _move,
|
||||
onHorizontalDragEnd: _settle,
|
||||
child: new Stack(<Widget>[
|
||||
new GestureDetector(
|
||||
onTap: _close,
|
||||
child: new DecoratedBox(
|
||||
decoration: new BoxDecoration(
|
||||
backgroundColor: _color.value
|
||||
),
|
||||
child: new Container()
|
||||
)
|
||||
),
|
||||
new Positioned(
|
||||
top: 0.0,
|
||||
left: 0.0,
|
||||
bottom: 0.0,
|
||||
child: new Listener(
|
||||
onPointerDown: _handlePointerDown,
|
||||
child: new Align(
|
||||
alignment: const FractionalOffset(1.0, 0.5),
|
||||
widthFactor: _performance.progress,
|
||||
child: new SizeObserver(
|
||||
onSizeChanged: _handleSizeChanged,
|
||||
child: config.child
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void showDrawer({ BuildContext context, Widget child, int level: 3 }) {
|
||||
|
||||
@@ -197,11 +197,9 @@ class RenderPositionedBox extends RenderShiftedBox {
|
||||
|
||||
if (child != null) {
|
||||
child.layout(constraints.loosen(), parentUsesSize: true);
|
||||
final Size desiredSize = new Size(child.size.width * (_widthFactor ?? 1.0),
|
||||
child.size.height * (_heightFactor ?? 1.0));
|
||||
size = constraints.constrain(new Size(shrinkWrapWidth ? desiredSize.width : double.INFINITY,
|
||||
shrinkWrapHeight ? desiredSize.height : double.INFINITY));
|
||||
final Offset delta = size - desiredSize;
|
||||
size = constraints.constrain(new Size(shrinkWrapWidth ? child.size.width * _widthFactor : double.INFINITY,
|
||||
shrinkWrapHeight ? child.size.height * _heightFactor : double.INFINITY));
|
||||
final Offset delta = size - child.size;
|
||||
final BoxParentData childParentData = child.parentData;
|
||||
childParentData.position = delta.scale(_alignment.x, _alignment.y).toPoint();
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user