dismissable.dart 8.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:ui' as ui;
6

7
import 'package:flutter/animation.dart';
8 9 10 11 12

import 'basic.dart';
import 'transitions.dart';
import 'framework.dart';
import 'gesture_detector.dart';
13

14 15 16
const Duration _kCardDismissDuration = const Duration(milliseconds: 200);
const Duration _kCardResizeDuration = const Duration(milliseconds: 300);
const Curve _kCardResizeTimeCurve = const Interval(0.4, 1.0, curve: Curves.ease);
17 18
const double _kMinFlingVelocity = 700.0;
const double _kMinFlingVelocityDelta = 400.0;
19
const double _kFlingVelocityScale = 1.0 / 300.0;
20
const double _kDismissCardThreshold = 0.4;
21

Adam Barth's avatar
Adam Barth committed
22
/// The direction in which a [Dismissable] can be dismissed.
23
enum DismissDirection {
Adam Barth's avatar
Adam Barth committed
24
  /// The [Dismissable] can be dismissed by dragging either up or down.
25
  vertical,
Adam Barth's avatar
Adam Barth committed
26 27

  /// The [Dismissable] can be dismissed by dragging either left or right.
28
  horizontal,
Adam Barth's avatar
Adam Barth committed
29 30

  /// The [Dismissable] can be dismissed by dragging left only.
31
  left,
Adam Barth's avatar
Adam Barth committed
32 33

  /// The [Dismissable] can be dismissed by dragging right only.
34
  right,
Adam Barth's avatar
Adam Barth committed
35 36

  /// The [Dismissable] can be dismissed by dragging up only.
37
  up,
Adam Barth's avatar
Adam Barth committed
38 39

  /// The [Dismissable] can be dismissed by dragging down only.
40 41 42
  down
}

Adam Barth's avatar
Adam Barth committed
43 44 45 46 47 48
/// Can be dismissed by dragging in one or more directions.
///
/// The child is draggable in the indicated direction(s). When released (or
/// flung), the child disappears off the edge and the dismissable widget
/// animates its height (or width, whichever is perpendicular to the dismiss
/// direction) to zero.
49
class Dismissable extends StatefulComponent {
50
  Dismissable({
51
    Key key,
52
    this.child,
53
    this.onResized,
54 55
    this.onDismissed,
    this.direction: DismissDirection.horizontal
56 57
  }) : super(key: key);

58
  final Widget child;
Adam Barth's avatar
Adam Barth committed
59 60

  /// Called when the widget changes size (i.e., when contracting after being dismissed).
61
  final VoidCallback onResized;
Adam Barth's avatar
Adam Barth committed
62

63
  /// Called when the widget has been dismissed, after finishing resizing.
64
  final VoidCallback onDismissed;
Adam Barth's avatar
Adam Barth committed
65 66

  /// The direction in which the widget can be dismissed.
67
  final DismissDirection direction;
68

69
  _DismissableState createState() => new _DismissableState();
70
}
71

72
class _DismissableState extends State<Dismissable> {
73
  void initState() {
74
    super.initState();
75 76
    _dismissPerformance = new Performance(duration: _kCardDismissDuration);
    _dismissPerformance.addStatusListener((PerformanceStatus status) {
77
      if (status == PerformanceStatus.completed)
78
        _handleDismissCompleted();
79
    });
80 81
  }

82
  Performance _dismissPerformance;
83
  Performance _resizePerformance;
84 85 86 87

  Size _size;
  double _dragExtent = 0.0;
  bool _dragUnderway = false;
88

89
  void dispose() {
90
    _dismissPerformance?.stop();
91 92 93 94
    _resizePerformance?.stop();
    super.dispose();
  }

95 96
  bool get _directionIsYAxis {
    return
97 98 99
      config.direction == DismissDirection.vertical ||
      config.direction == DismissDirection.up ||
      config.direction == DismissDirection.down;
100 101
  }

102
  void _handleDismissCompleted() {
103 104
    if (!_dragUnderway)
      _startResizePerformance();
105 106 107
  }

  bool get _isActive {
108
    return _size != null && (_dragUnderway || _dismissPerformance.isAnimating);
109 110 111
  }

  void _maybeCallOnResized() {
112 113
    if (config.onResized != null)
      config.onResized();
114 115 116
  }

  void _maybeCallOnDismissed() {
117 118
    if (config.onDismissed != null)
      config.onDismissed();
119 120
  }

121 122
  void _startResizePerformance() {
    assert(_size != null);
123 124
    assert(_dismissPerformance != null);
    assert(_dismissPerformance.isCompleted);
125
    assert(_resizePerformance == null);
126
    setState(() {
127
      _resizePerformance = new Performance()
128
        ..duration = _kCardResizeDuration
129
        ..addListener(_handleResizeProgressChanged);
130
      _resizePerformance.play();
131
    });
132 133 134
  }

  void _handleResizeProgressChanged() {
135 136 137 138
    if (_resizePerformance.isCompleted)
      _maybeCallOnDismissed();
    else
      _maybeCallOnResized();
139 140
  }

141
  void _handleDragStart(_) {
142 143
    setState(() {
      _dragUnderway = true;
144 145 146 147 148 149 150
      if (_dismissPerformance.isAnimating) {
        _dragExtent = _dismissPerformance.progress * _size.width * _dragExtent.sign;
        _dismissPerformance.stop();
      } else {
        _dragExtent = 0.0;
        _dismissPerformance.progress = 0.0;
      }
151
    });
152 153
  }

154
  void _handleDragUpdate(double delta) {
155
    if (!_isActive || _dismissPerformance.isAnimating)
156
      return;
157 158

    double oldDragExtent = _dragExtent;
159
    switch (config.direction) {
160 161
      case DismissDirection.horizontal:
      case DismissDirection.vertical:
162
        _dragExtent += delta;
163 164 165 166
        break;

      case DismissDirection.up:
      case DismissDirection.left:
167 168
        if (_dragExtent + delta < 0)
          _dragExtent += delta;
169 170 171 172
        break;

      case DismissDirection.down:
      case DismissDirection.right:
173 174
        if (_dragExtent + delta > 0)
          _dragExtent += delta;
175 176 177
        break;
    }

178 179 180 181 182 183 184 185
    if (oldDragExtent.sign != _dragExtent.sign) {
      setState(() {
        // Rebuild to update the new drag endpoint.
        // The sign of _dragExtent is part of our build state;
        // the actual value is not, it's just used to configure
        // the performances.
      });
    }
186 187
    if (!_dismissPerformance.isAnimating)
      _dismissPerformance.progress = _dragExtent.abs() / _size.width;
188 189
  }

190
  bool _isFlingGesture(ui.Offset velocity) {
191 192
    double vx = velocity.dx;
    double vy = velocity.dy;
193 194 195
    if (_directionIsYAxis) {
      if (vy.abs() - vx.abs() < _kMinFlingVelocityDelta)
        return false;
196
      switch(config.direction) {
197 198 199 200 201 202 203 204 205 206
        case DismissDirection.vertical:
          return vy.abs() > _kMinFlingVelocity;
        case DismissDirection.up:
          return -vy > _kMinFlingVelocity;
        default:
          return vy > _kMinFlingVelocity;
      }
    } else {
      if (vx.abs() - vy.abs() < _kMinFlingVelocityDelta)
        return false;
207
      switch(config.direction) {
208 209 210 211 212 213 214 215 216
        case DismissDirection.horizontal:
          return vx.abs() > _kMinFlingVelocity;
        case DismissDirection.left:
          return -vx > _kMinFlingVelocity;
        default:
          return vx > _kMinFlingVelocity;
      }
    }
    return false;
217 218
  }

219
  void _handleDragEnd(ui.Offset velocity) {
220
    if (!_isActive || _dismissPerformance.isAnimating)
221
      return;
222 223
    setState(() {
      _dragUnderway = false;
224
      if (_dismissPerformance.isCompleted) {
225 226 227 228
        _startResizePerformance();
      } else if (_isFlingGesture(velocity)) {
        double flingVelocity = _directionIsYAxis ? velocity.dy : velocity.dx;
        _dragExtent = flingVelocity.sign;
229 230 231
        _dismissPerformance.fling(velocity: flingVelocity.abs() * _kFlingVelocityScale);
      } else if (_dismissPerformance.progress > _kDismissCardThreshold) {
        _dismissPerformance.forward();
232
      } else {
233
        _dismissPerformance.reverse();
234 235
      }
    });
236 237
  }

238
  void _handleSizeChanged(Size newSize) {
239 240 241
    setState(() {
      _size = new Size.copy(newSize);
    });
242 243
  }

244
  FractionalOffset get _activeCardDragEndPoint {
245
    if (!_isActive)
246 247 248 249
      return FractionalOffset.zero;
    if (_directionIsYAxis)
      return new FractionalOffset(0.0, _dragExtent.sign);
    return new FractionalOffset(_dragExtent.sign, 0.0);
250 251
  }

252
  Widget build(BuildContext context) {
Adam Barth's avatar
Adam Barth committed
253
    if (_resizePerformance != null) {
254
      // make sure you remove this widget once it's been dismissed!
255
      assert(_resizePerformance.status == PerformanceStatus.forward);
256

257 258
      AnimatedValue<double> squashAxisExtent = new AnimatedValue<double>(
        _directionIsYAxis ? _size.width : _size.height,
259
        end: 0.0,
260
        curve: _kCardResizeTimeCurve
261 262 263
      );

      return new SquashTransition(
264
        performance: _resizePerformance.view,
265 266 267
        width: _directionIsYAxis ? squashAxisExtent : null,
        height: !_directionIsYAxis ? squashAxisExtent : null
      );
Adam Barth's avatar
Adam Barth committed
268
    }
269

270
    return new GestureDetector(
271 272 273 274 275 276
      onHorizontalDragStart: _directionIsYAxis ? null : _handleDragStart,
      onHorizontalDragUpdate: _directionIsYAxis ? null : _handleDragUpdate,
      onHorizontalDragEnd: _directionIsYAxis ? null : _handleDragEnd,
      onVerticalDragStart: _directionIsYAxis ? _handleDragStart : null,
      onVerticalDragUpdate: _directionIsYAxis ? _handleDragUpdate : null,
      onVerticalDragEnd: _directionIsYAxis ? _handleDragEnd : null,
Hixie's avatar
Hixie committed
277
      behavior: HitTestBehavior.opaque,
278
      child: new SizeObserver(
279
        onSizeChanged: _handleSizeChanged,
280 281 282 283 284 285 286
        child: new SlideTransition(
          performance: _dismissPerformance.view,
          position: new AnimatedValue<FractionalOffset>(
            FractionalOffset.zero,
            end: _activeCardDragEndPoint
          ),
          child: config.child
287 288 289 290 291
        )
      )
    );
  }
}