blob: 11a161ce52efaffc06e700cdcbc48ce7c7369816 [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:math' as math;
import 'dart:sky' as sky;
import 'package:vector_math/vector_math.dart';
import '../framework/animation/animated_value.dart';
import '../framework/animation/curves.dart';
import '../theme2/colors.dart';
import '../theme2/shadows.dart';
import 'animated_component.dart';
import 'basic.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 = 0.4;
const double _kBaseSettleDurationMS = 246.0;
const double _kMaxSettleDurationMS = 600.0;
const Curve _kAnimationCurve = parabolicRise;
typedef void DrawerStatusChangeHandler (bool showing);
class DrawerController {
DrawerController(this.onStatusChange) {
position = new AnimatedValue(-_kWidth, onChange: _checkValue);
}
final DrawerStatusChangeHandler onStatusChange;
AnimatedValue position;
bool _oldClosedState = true;
void _checkValue() {
var newClosedState = isClosed;
if (onStatusChange != null && _oldClosedState != newClosedState) {
onStatusChange(!newClosedState);
_oldClosedState = newClosedState;
}
}
bool get isClosed => position.value == -_kWidth;
bool get _isMostlyClosed => position.value <= -_kWidth / 2;
void toggle() => _isMostlyClosed ? _open() : _close();
void handleMaskTap(_) => _close();
void handlePointerDown(_) => position.stop();
void handlePointerMove(sky.PointerEvent event) {
if (position.isAnimating)
return;
position.value = math.min(0.0, math.max(position.value + event.dx, -_kWidth));
}
void handlePointerUp(_) {
if (!position.isAnimating)
_settle();
}
void handlePointerCancel(_) {
if (!position.isAnimating)
_settle();
}
void _open() => _animateToPosition(0.0);
void _close() => _animateToPosition(-_kWidth);
void _settle() => _isMostlyClosed ? _close() : _open();
void _animateToPosition(double targetPosition) {
double distance = (targetPosition - position.value).abs();
if (distance != 0) {
double targetDuration = distance / _kWidth * _kBaseSettleDurationMS;
double duration = math.min(targetDuration, _kMaxSettleDurationMS);
position.animateTo(targetPosition, duration, curve: _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.value).abs();
double duration = distance / velocityX;
if (distance > 0)
position.animateTo(targetPosition, duration, curve: linear);
}
}
class Drawer extends AnimatedComponent {
Drawer({
String key,
this.controller,
this.children,
this.level: 0
}) : super(key: key) {
animate(controller.position, (double value) {
_position = value;
});
}
List<Widget> children;
int level;
DrawerController controller;
void syncFields(Drawer source) {
children = source.children;
level = source.level;
controller = source.controller;
super.syncFields(source);
}
double _position;
Widget build() {
Matrix4 transform = new Matrix4.identity();
transform.translate(_position);
double scaler = _position / _kWidth + 1;
Color maskColor = new Color.fromARGB((0x7F * scaler).floor(), 0, 0, 0);
var mask = new Listener(
child: new Container(decoration: new BoxDecoration(backgroundColor: maskColor)),
onGestureTap: controller.handleMaskTap,
onGestureFlingStart: controller.handleFlingStart
);
Container content = new Container(
decoration: new BoxDecoration(
backgroundColor: Grey[50],
boxShadow: shadows[level]),
width: _kWidth,
transform: transform,
child: new Block(children)
);
return new Listener(
child: new Stack([ mask, content ]),
onPointerDown: controller.handlePointerDown,
onPointerMove: controller.handlePointerMove,
onPointerUp: controller.handlePointerUp,
onPointerCancel: controller.handlePointerCancel
);
}
}