ink_well.dart 6.78 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:math' as math;

8 9 10 11
import 'package:flutter/animation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
12

13
const int _kSplashInitialOpacity = 0x30;
14
const double _kSplashCanceledVelocity = 0.7;
15
const double _kSplashConfirmedVelocity = 0.7;
16
const double _kSplashInitialSize = 0.0;
17
const double _kSplashUnconfirmedVelocity = 0.2;
18 19 20 21 22 23 24 25 26

double _getSplashTargetSize(Size bounds, Point position) {
  double d1 = (position - bounds.topLeft(Point.origin)).distance;
  double d2 = (position - bounds.topRight(Point.origin)).distance;
  double d3 = (position - bounds.bottomLeft(Point.origin)).distance;
  double d4 = (position - bounds.bottomRight(Point.origin)).distance;
  return math.max(math.max(d1, d2), math.max(d3, d4)).ceil().toDouble();
}

27 28
class _InkSplash {
  _InkSplash(this.position, this.well) {
29
    _targetRadius = _getSplashTargetSize(well.size, position);
30
    _radius = new AnimatedValue<double>(
31 32
        _kSplashInitialSize, end: _targetRadius, curve: easeOut);

33
    _performance = new ValuePerformance<double>(
34 35
      variable: _radius,
      duration: new Duration(milliseconds: (_targetRadius / _kSplashUnconfirmedVelocity).floor())
36 37 38 39
    )..addListener(_handleRadiusChange);

    // Wait kTapTimeout to avoid creating tiny splashes during scrolls.
    _startTimer = new Timer(kTapTimeout, _play);
40 41 42
  }

  final Point position;
43
  final _RenderInkWell well;
44 45 46

  double _targetRadius;
  double _pinnedRadius;
47
  AnimatedValue<double> _radius;
48
  Performance _performance;
49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
  Timer _startTimer;

  bool _cancelStartTimer() {
    if (_startTimer != null) {
      _startTimer.cancel();
      _startTimer = null;
      return true;
    }
    return false;
  }

  void _play() {
    _cancelStartTimer();
    _performance.play();
  }
64 65 66

  void _updateVelocity(double velocity) {
    int duration = (_targetRadius / velocity).floor();
67 68
    _performance.duration = new Duration(milliseconds: duration);
    _play();
69 70 71
  }

  void confirm() {
72 73
    if (_cancelStartTimer())
      return;
74
    _updateVelocity(_kSplashConfirmedVelocity);
75
    _pinnedRadius = null;
76 77 78
  }

  void cancel() {
79 80 81
    if (_cancelStartTimer())
      return;
    _updateVelocity(_kSplashCanceledVelocity);
82 83 84 85 86 87 88 89 90 91
    _pinnedRadius = _radius.value;
  }

  void _handleRadiusChange() {
    if (_radius.value == _targetRadius)
      well._splashes.remove(this);
    well.markNeedsPaint();
  }

  void paint(PaintingCanvas canvas) {
92
    int opacity = (_kSplashInitialOpacity * (1.1 - (_radius.value / _targetRadius))).floor();
93
    Paint paint = new Paint()..color = new Color(opacity << 24);
94 95 96 97 98
    double radius = _pinnedRadius == null ? _radius.value : _pinnedRadius;
    canvas.drawCircle(position, radius, paint);
  }
}

99 100
typedef _HighlightChangedCallback(bool value);

101 102
class _RenderInkWell extends RenderProxyBox {
  _RenderInkWell({
103 104
    RenderBox child,
    GestureTapCallback onTap,
105 106
    GestureLongPressCallback onLongPress,
    _HighlightChangedCallback onHighlightChanged
107 108
  }) : super(child) {
    this.onTap = onTap;
109
    this.onHighlightChanged = onHighlightChanged;
110 111 112 113 114 115 116 117 118 119
    this.onLongPress = onLongPress;
  }

  GestureTapCallback get onTap => _onTap;
  GestureTapCallback _onTap;
  void set onTap (GestureTapCallback value) {
    _onTap = value;
    _syncTapRecognizer();
  }

120 121 122 123 124 125 126
  _HighlightChangedCallback get onHighlightChanged => _onHighlightChanged;
  _HighlightChangedCallback _onHighlightChanged;
  void set onHighlightChanged (_HighlightChangedCallback value) {
    _onHighlightChanged = value;
    _syncTapRecognizer();
  }

127 128 129 130 131 132
  GestureTapCallback get onLongPress => _onLongPress;
  GestureTapCallback _onLongPress;
  void set onLongPress (GestureTapCallback value) {
    _onLongPress = value;
    _syncLongPressRecognizer();
  }
133

134
  final List<_InkSplash> _splashes = new List<_InkSplash>();
135

136 137 138
  TapGestureRecognizer _tap;
  LongPressGestureRecognizer _longPress;

139
  void handleEvent(InputEvent event, BoxHitTestEntry entry) {
140 141 142
    if (event.type == 'pointerdown' && (_tap != null || _longPress != null)) {
      _tap?.addPointer(event);
      _longPress?.addPointer(event);
143
      _splashes.add(new _InkSplash(entry.localPosition, this));
144 145 146
    }
  }

147 148 149 150
  void attach() {
    super.attach();
    _syncTapRecognizer();
    _syncLongPressRecognizer();
151 152
  }

153 154 155 156 157 158 159
  void detach() {
    _disposeTapRecognizer();
    _disposeLongPressRecognizer();
    super.detach();
  }

  void _syncTapRecognizer() {
160
    if (onTap == null && onHighlightChanged == null) {
161 162 163
      _disposeTapRecognizer();
    } else {
      _tap ??= new TapGestureRecognizer(router: FlutterBinding.instance.pointerRouter)
164
        ..onTapDown = _handleTapDown
165 166 167
        ..onTap = _handleTap
        ..onTapCancel = _handleTapCancel;
    }
168 169
  }

170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188
  void _disposeTapRecognizer() {
    _tap?.dispose();
    _tap = null;
  }

  void _syncLongPressRecognizer() {
    if (onLongPress == null) {
      _disposeLongPressRecognizer();
    } else {
      _longPress ??= new LongPressGestureRecognizer(router: FlutterBinding.instance.pointerRouter)
        ..onLongPress = _handleLongPress;
    }
  }

  void _disposeLongPressRecognizer() {
    _longPress?.dispose();
    _longPress = null;
  }

189 190 191 192 193
  void _handleTapDown() {
    if (onHighlightChanged != null)
      onHighlightChanged(true);
  }

194
  void _handleTap() {
195 196 197 198 199 200 201 202
    if (_splashes.isNotEmpty)
      _splashes.last.confirm();

    if (onHighlightChanged != null)
      onHighlightChanged(false);

    if (onTap != null)
      onTap();
203 204 205 206
  }

  void _handleTapCancel() {
    _splashes.last?.cancel();
207 208
    if (onHighlightChanged != null)
      onHighlightChanged(false);
209 210 211 212 213
  }

  void _handleLongPress() {
    _splashes.last?.confirm();
    onLongPress();
214 215
  }

216
  void paint(PaintingContext context, Offset offset) {
217
    if (!_splashes.isEmpty) {
218
      final PaintingCanvas canvas = context.canvas;
219 220 221
      canvas.save();
      canvas.translate(offset.dx, offset.dy);
      canvas.clipRect(Point.origin & size);
222
      for (_InkSplash splash in _splashes)
223 224 225
        splash.paint(canvas);
      canvas.restore();
    }
226
    super.paint(context, offset);
227 228 229
  }
}

230
class InkWell extends OneChildRenderObjectWidget {
231 232 233 234
  InkWell({
    Key key,
    Widget child,
    this.onTap,
235
    this.onHighlightChanged,
236 237
    this.onLongPress
  }) : super(key: key, child: child);
238

239
  final GestureTapCallback onTap;
240
  final _HighlightChangedCallback onHighlightChanged;
241 242
  final GestureLongPressCallback onLongPress;

243
  _RenderInkWell createRenderObject() => new _RenderInkWell(onTap: onTap, onHighlightChanged: onHighlightChanged, onLongPress: onLongPress);
244

245
  void updateRenderObject(_RenderInkWell renderObject, InkWell oldWidget) {
246
    renderObject.onTap = onTap;
247
    renderObject.onHighlightChanged = onHighlightChanged;
248 249
    renderObject.onLongPress = onLongPress;
  }
250
}