drawer.dart 5.56 KB
Newer Older
1 2 3 4
// 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.

5
import 'dart:async';
6 7
import 'dart:sky' as sky;

8
import 'package:sky/animation/animated_value.dart';
9
import 'package:sky/animation/animation_performance.dart';
10
import 'package:sky/animation/forces.dart';
11 12
import 'package:sky/theme/shadows.dart';
import 'package:sky/theme/colors.dart' as colors;
13
import 'package:sky/widgets/animated_container.dart';
14 15
import 'package:sky/widgets/basic.dart';
import 'package:sky/widgets/navigator.dart';
16
import 'package:sky/widgets/scrollable.dart';
17
import 'package:sky/widgets/theme.dart';
18
import 'package:sky/widgets/transitions.dart';
19

20 21
export 'package:sky/animation/animation_performance.dart' show AnimationStatus;

22 23 24 25 26 27 28 29 30 31 32 33 34 35
// 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;
Matt Perry's avatar
Matt Perry committed
36
const double _kMinFlingVelocity = 365.0;
37
const double _kFlingVelocityScale = 1.0 / 300.0;
38
const Duration _kBaseSettleDuration = const Duration(milliseconds: 246);
39
const Duration _kThemeChangeDuration = const Duration(milliseconds: 200);
40 41 42
const Point _kOpenPosition = Point.origin;
const Point _kClosedPosition = const Point(-_kWidth, 0.0);

43
typedef void DrawerDismissedCallback();
44

45
class Drawer extends StatefulComponent {
46
  Drawer({
47
    Key key,
48 49 50
    this.children,
    this.showing: false,
    this.level: 0,
51
    this.onDismissed,
52 53 54 55 56 57
    this.navigator
  }) : super(key: key);

  List<Widget> children;
  bool showing;
  int level;
58
  DrawerDismissedCallback onDismissed;
59 60
  Navigator navigator;

61
  AnimationPerformance _performance;
62 63

  void initState() {
64
    _performance = new AnimationPerformance(duration: _kBaseSettleDuration);
65 66 67 68 69 70

    // 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;

Adam Barth's avatar
Adam Barth committed
71 72 73 74 75
    if (navigator != null) {
      scheduleMicrotask(() {
        navigator.pushState(this, (_) => _performance.reverse());
      });
    }
76 77 78 79 80 81
  }

  void syncFields(Drawer source) {
    children = source.children;
    level = source.level;
    navigator = source.navigator;
82
    showing = source.showing;
83
    onDismissed = source.onDismissed;
84 85 86 87
  }

  Widget build() {
    var mask = new Listener(
88 89 90
      child: new ColorTransition(
        performance: _performance,
        direction: showing ? Direction.forward : Direction.reverse,
Adam Barth's avatar
Adam Barth committed
91 92
        color: new AnimatedColorValue(colors.transparent, end: const Color(0x7F000000)),
        child: new Container()
93 94 95 96
      ),
      onGestureTap: handleMaskTap
    );

97
    Widget content = new SlideTransition(
98 99 100 101 102
      performance: _performance,
      direction: showing ? Direction.forward : Direction.reverse,
      position: new AnimatedValue<Point>(_kClosedPosition, end: _kOpenPosition),
      onDismissed: _onDismissed,
      child: new AnimatedContainer(
103
        behavior: implicitlyAnimate(const Duration(milliseconds: 200)),
104 105 106 107 108 109
        decoration: new BoxDecoration(
          backgroundColor: Theme.of(this).canvasColor,
          boxShadow: shadows[level]),
        width: _kWidth,
        child: new ScrollableBlock(children)
      )
110
    );
111 112 113 114 115 116 117 118 119 120 121

    return new Listener(
      child: new Stack([ mask, content ]),
      onPointerDown: handlePointerDown,
      onPointerMove: handlePointerMove,
      onPointerUp: handlePointerUp,
      onPointerCancel: handlePointerCancel,
      onGestureFlingStart: handleFlingStart
    );
  }

122
  void _onDismissed() {
123
    scheduleMicrotask(() {
124
      if (navigator != null &&
125 126 127
          navigator.currentRoute is RouteState &&
          (navigator.currentRoute as RouteState).owner == this) // TODO(ianh): remove cast once analyzer is cleverer
        navigator.pop();
128 129
      if (onDismissed != null)
        onDismissed();
130
    });
131 132
  }

133
  bool get _isMostlyClosed => _performance.progress < 0.5;
134

135
  void _settle() { _isMostlyClosed ? _performance.reverse() : _performance.play(); }
136

137 138 139 140
  EventDisposition handleMaskTap(_) {
    _performance.reverse();
    return EventDisposition.consumed;
  }
141 142 143

  // TODO(mpcomplete): Figure out how to generalize these handlers on a
  // "PannableThingy" interface.
144 145 146 147
  EventDisposition handlePointerDown(_) {
    _performance.stop();
    return EventDisposition.processed;
 }
148

149
  EventDisposition handlePointerMove(sky.PointerEvent event) {
150
    if (_performance.isAnimating)
151
      return EventDisposition.ignored;
152
    _performance.progress += event.dx / _kWidth;
153
    return EventDisposition.processed;
154 155
  }

156 157
  EventDisposition handlePointerUp(_) {
    if (!_performance.isAnimating) {
158
      _settle();
159 160 161
      return EventDisposition.processed;
    }
    return EventDisposition.ignored;
162 163
  }

164 165
  EventDisposition handlePointerCancel(_) {
    if (!_performance.isAnimating) {
166
      _settle();
167 168 169
      return EventDisposition.processed;
    }
    return EventDisposition.ignored;
170 171
  }

172
  EventDisposition handleFlingStart(event) {
173 174 175 176
    if (event.velocityX.abs() >= _kMinFlingVelocity) {
      _performance.fling(
          event.velocityX < 0.0 ? Direction.reverse : Direction.forward,
          velocity: event.velocityX.abs() * _kFlingVelocityScale);
177
      return EventDisposition.processed;
178
    }
179
    return EventDisposition.ignored;
180 181
  }
}