// 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:async'; import 'dart:sky' as sky; import 'package:sky/animation/animated_value.dart'; import 'package:sky/animation/animation_performance.dart'; import 'package:sky/animation/forces.dart'; import 'package:sky/theme/shadows.dart'; import 'package:sky/theme/colors.dart' as colors; import 'package:sky/widgets/animated_container.dart'; import 'package:sky/widgets/basic.dart'; import 'package:sky/widgets/navigator.dart'; import 'package:sky/widgets/scrollable.dart'; import 'package:sky/widgets/theme.dart'; import 'package:sky/widgets/transitions.dart'; export 'package:sky/animation/animation_performance.dart' show AnimationStatus; // 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 = 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); typedef void DrawerDismissedCallback(); class Drawer extends StatefulComponent { Drawer({ Key key, this.children, this.showing: false, this.level: 0, this.onDismissed, this.navigator }) : super(key: key); List<Widget> children; bool showing; int level; DrawerDismissedCallback onDismissed; Navigator navigator; AnimationPerformance _performance; void initState() { _performance = new AnimationPerformance(duration: _kBaseSettleDuration); // Use a spring force for animating the drawer. We can't use curves for // this because we need a linear curve in order to track the user's finger // while dragging. _performance.attachedForce = kDefaultSpringForce; if (navigator != null) { scheduleMicrotask(() { navigator.pushState(this, (_) => _performance.reverse()); }); } } void syncFields(Drawer source) { children = source.children; level = source.level; navigator = source.navigator; showing = source.showing; onDismissed = source.onDismissed; } Widget build() { var mask = new Listener( child: new ColorTransition( performance: _performance, direction: showing ? Direction.forward : Direction.reverse, color: new AnimatedColorValue(colors.transparent, end: const Color(0x7F000000)), child: new Container() ), onGestureTap: handleMaskTap ); Widget content = new SlideTransition( performance: _performance, direction: showing ? Direction.forward : Direction.reverse, position: new AnimatedValue<Point>(_kClosedPosition, end: _kOpenPosition), onDismissed: _onDismissed, child: new AnimatedContainer( behavior: implicitlyAnimate(const Duration(milliseconds: 200)), 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 ); } void _onDismissed() { scheduleMicrotask(() { if (navigator != null && navigator.currentRoute is RouteState && (navigator.currentRoute as RouteState).owner == this) // TODO(ianh): remove cast once analyzer is cleverer navigator.pop(); if (onDismissed != null) onDismissed(); }); } bool get _isMostlyClosed => _performance.progress < 0.5; void _settle() { _isMostlyClosed ? _performance.reverse() : _performance.play(); } EventDisposition handleMaskTap(_) { _performance.reverse(); return EventDisposition.consumed; } // TODO(mpcomplete): Figure out how to generalize these handlers on a // "PannableThingy" interface. EventDisposition handlePointerDown(_) { _performance.stop(); return EventDisposition.processed; } EventDisposition handlePointerMove(sky.PointerEvent event) { if (_performance.isAnimating) return EventDisposition.ignored; _performance.progress += event.dx / _kWidth; return EventDisposition.processed; } EventDisposition handlePointerUp(_) { if (!_performance.isAnimating) { _settle(); return EventDisposition.processed; } return EventDisposition.ignored; } EventDisposition handlePointerCancel(_) { if (!_performance.isAnimating) { _settle(); return EventDisposition.processed; } return EventDisposition.ignored; } EventDisposition handleFlingStart(event) { if (event.velocityX.abs() >= _kMinFlingVelocity) { _performance.fling(velocity: event.velocityX * _kFlingVelocityScale); return EventDisposition.processed; } return EventDisposition.ignored; } }