blob: 933f942f585e1985f45faa0b5817dfe07aba4c8d [file] [log] [blame]
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:sky' as sky;
import 'package:sky/animation/animation_performance.dart';
import 'package:sky/animation/curves.dart';
import 'package:sky/theme/shadows.dart';
import 'package:sky/theme/colors.dart' as colors;
import 'package:sky/widgets/animated_component.dart';
import 'package:sky/widgets/animation_builder.dart';
import 'package:sky/widgets/basic.dart';
import 'package:sky/widgets/navigator.dart';
import 'package:sky/widgets/scrollable_viewport.dart';
import 'package:sky/widgets/theme.dart';
import 'package:vector_math/vector_math.dart';
// TODO(eseidel): Draw width should vary based on device size:
// http://www.google.com/design/spec/layout/structure.html#structure-side-nav
// Mobile:
// Width = Screen width − 56 dp
// Maximum width: 320dp
// Maximum width applies only when using a left nav. When using a right nav,
// the panel can cover the full width of the screen.
// Desktop/Tablet:
// Maximum width for a left nav is 400dp.
// The right nav can vary depending on content.
const double _kWidth = 304.0;
const double _kMinFlingVelocity = 1.2;
const Duration _kBaseSettleDuration = const Duration(milliseconds: 246);
// TODO(mpcomplete): The curve must be linear if we want the drawer to track
// the user's finger. Odeon remedies this by attaching spring forces to the
// initial timeline when animating (so it doesn't look linear).
const Point _kOpenPosition = Point.origin;
const Point _kClosedPosition = const Point(-_kWidth, 0.0);
const Curve _kAnimationCurve = linear;
typedef void DrawerStatusChangeHandler (bool showing);
enum DrawerStatus {
active,
inactive,
}
typedef void DrawerStatusChangedCallback(DrawerStatus status);
class Drawer extends AnimatedComponent {
Drawer({
String key,
this.children,
this.showing: false,
this.level: 0,
this.onStatusChanged,
this.navigator
}) : super(key: key);
List<Widget> children;
bool showing;
int level;
DrawerStatusChangedCallback onStatusChanged;
Navigator navigator;
AnimatedType<Point> _position;
AnimatedColor _maskColor;
AnimationPerformance _performance;
void initState() {
_position = new AnimatedType<Point>(_kClosedPosition, end: _kOpenPosition, curve: _kAnimationCurve);
_maskColor = new AnimatedColor(colors.transparent, end: const Color(0x7F000000));
_performance = new AnimationPerformance()
..duration = _kBaseSettleDuration
..variable = new AnimatedList([_position, _maskColor])
..addListener(_checkForStateChanged);
watch(_performance);
if (showing)
_show();
}
void syncFields(Drawer source) {
children = source.children;
level = source.level;
navigator = source.navigator;
if (showing != source.showing) {
showing = source.showing;
showing ? _show() : _hide();
}
onStatusChanged = source.onStatusChanged;
super.syncFields(source);
}
void _show() {
if (navigator != null)
navigator.pushState(this, (_) => _performance.reverse());
_performance.play();
}
void _hide() {
_performance.reverse();
}
Widget build() {
var mask = new Listener(
child: new Container(
decoration: new BoxDecoration(backgroundColor: _maskColor.value)
),
onGestureTap: handleMaskTap
);
Matrix4 transform = new Matrix4.identity();
transform.translate(_position.value.x, _position.value.y);
Widget content = new Transform(
transform: transform,
child: new Container(
decoration: new BoxDecoration(
backgroundColor: Theme.of(this).canvasColor,
boxShadow: shadows[level]),
width: _kWidth,
child: new ScrollableBlock(children)
));
return new Listener(
child: new Stack([ mask, content ]),
onPointerDown: handlePointerDown,
onPointerMove: handlePointerMove,
onPointerUp: handlePointerUp,
onPointerCancel: handlePointerCancel,
onGestureFlingStart: handleFlingStart
);
}
double get xPosition => _position.value.x;
DrawerStatus _lastStatus;
void _checkForStateChanged() {
DrawerStatus status = _status;
if (_lastStatus != null && status != _lastStatus) {
if (status == DrawerStatus.inactive &&
navigator != null &&
navigator.currentRoute.key == this)
navigator.pop();
if (onStatusChanged != null)
onStatusChanged(status);
}
_lastStatus = status;
}
DrawerStatus get _status => _performance.isDismissed ? DrawerStatus.inactive : DrawerStatus.active;
bool get _isMostlyClosed => xPosition <= -_kWidth/2;
void _settle() => _isMostlyClosed ? _performance.reverse() : _performance.play();
void handleMaskTap(_) => _performance.reverse();
// TODO(mpcomplete): Figure out how to generalize these handlers on a
// "PannableThingy" interface.
void handlePointerDown(_) => _performance.stop();
void handlePointerMove(sky.PointerEvent event) {
if (_performance.isAnimating)
return;
_performance.progress += event.dx / _kWidth;
}
void handlePointerUp(_) {
if (!_performance.isAnimating)
_settle();
}
void handlePointerCancel(_) {
if (!_performance.isAnimating)
_settle();
}
void handleFlingStart(event) {
double velocityX = event.velocityX / _kWidth;
if (velocityX.abs() >= _kMinFlingVelocity)
_performance.fling(velocity: velocityX);
}
}