ink_well.dart 9.05 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 6
import 'dart:collection';

xster's avatar
xster committed
7
import 'package:flutter/foundation.dart';
8 9 10
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
11

12
import 'debug.dart';
13 14
import 'ink_highlight.dart';
import 'ink_splash.dart';
15 16
import 'material.dart';
import 'theme.dart';
17

18
/// An area of a [Material] that responds to touch. Has a configurable shape and
19 20
/// can be configured to clip splashes that extend outside its bounds or not.
///
21
/// For a variant of this widget that is specialized for rectangular areas that
22 23 24 25 26 27 28 29
/// always clip splashes, see [InkWell].
///
/// Must have an ancestor [Material] widget in which to cause ink reactions.
///
/// If a Widget uses this class directly, it should include the following line
/// at the top of its [build] function to call [debugCheckHasMaterial]:
///
///     assert(debugCheckHasMaterial(context));
30
class InkResponse extends StatefulWidget {
31 32 33
  /// Creates an area of a [Material] that responds to touch.
  ///
  /// Must have an ancestor [Material] widget in which to cause ink reactions.
34
  const InkResponse({
35 36 37
    Key key,
    this.child,
    this.onTap,
38
    this.onDoubleTap,
39
    this.onLongPress,
40
    this.onHighlightChanged,
41
    this.containedInkWell: false,
42 43
    this.highlightShape: BoxShape.circle,
    this.radius,
44
    this.borderRadius: BorderRadius.zero,
45 46
    this.highlightColor,
    this.splashColor,
47 48
  }) : super(key: key);

49
  /// The widget below this widget in the tree.
50
  final Widget child;
51

52
  /// Called when the user taps this part of the material
53
  final GestureTapCallback onTap;
54

55
  /// Called when the user double taps this part of the material.
56
  final GestureTapCallback onDoubleTap;
57

58
  /// Called when the user long-presses on this part of the material.
59
  final GestureLongPressCallback onLongPress;
60

61 62 63 64 65
  /// Called when this part of the material either becomes highlighted or stops behing highlighted.
  ///
  /// The value passed to the callback is true if this part of the material has
  /// become highlighted and false if this part of the material has stopped
  /// being highlighted.
66
  final ValueChanged<bool> onHighlightChanged;
67

68 69
  /// Whether this ink response should be clipped its bounds.
  final bool containedInkWell;
70

71
  /// The shape (e.g., circle, rectangle) to use for the highlight drawn around this part of the material.
72
  final BoxShape highlightShape;
73

74 75 76
  /// The radius of the ink splash.
  final double radius;

77 78 79
  /// The clipping radius of the containing rect.
  final BorderRadius borderRadius;

80 81 82 83 84 85 86 87
  /// The highlight color of the ink response. If this property is null then the
  /// highlight color of the theme will be used.
  final Color highlightColor;

  /// The splash color of the ink response. If this property is null then the
  /// splash color of the theme will be used.
  final Color splashColor;

88 89 90 91 92
  /// The rectangle to use for the highlight effect and for clipping
  /// the splash effects if [containedInkWell] is true.
  ///
  /// This method is intended to be overridden by descendants that
  /// specialize [InkResponse] for unusual cases. For example,
93
  /// [TableRowInkWell] implements this method to return the rectangle
94 95 96 97 98 99 100 101 102 103 104 105
  /// corresponding to the row that the widget is in.
  ///
  /// The default behavior returns null, which is equivalent to
  /// returning the referenceBox argument's bounding box (though
  /// slightly more efficient).
  RectCallback getRectCallback(RenderBox referenceBox) => null;

  /// Asserts that the given context satisfies the prerequisites for
  /// this class.
  ///
  /// This method is intended to be overridden by descendants that
  /// specialize [InkResponse] for unusual cases. For example,
106
  /// [TableRowInkWell] implements this method to verify that the widget is
107
  /// in a table.
108
  @mustCallSuper
109 110 111 112 113
  bool debugCheckContext(BuildContext context) {
    assert(debugCheckHasMaterial(context));
    return true;
  }

114
  @override
115
  _InkResponseState<InkResponse> createState() => new _InkResponseState<InkResponse>();
116 117
}

118
class _InkResponseState<T extends InkResponse> extends State<T> {
119

120 121
  Set<InkSplash> _splashes;
  InkSplash _currentSplash;
122 123 124 125 126 127 128
  InkHighlight _lastHighlight;

  void updateHighlight(bool value) {
    if (value == (_lastHighlight != null && _lastHighlight.active))
      return;
    if (value) {
      if (_lastHighlight == null) {
129 130 131
        final RenderBox referenceBox = context.findRenderObject();
        _lastHighlight = new InkHighlight(
          controller: Material.of(context),
132
          referenceBox: referenceBox,
133 134 135 136
          color: widget.highlightColor ?? Theme.of(context).highlightColor,
          shape: widget.highlightShape,
          borderRadius: widget.borderRadius,
          rectCallback: widget.getRectCallback(referenceBox),
137 138 139
          onRemoved: () {
            assert(_lastHighlight != null);
            _lastHighlight = null;
140
          },
141 142 143 144 145 146 147
        );
      } else {
        _lastHighlight.activate();
      }
    } else {
      _lastHighlight.deactivate();
    }
148
    assert(value == (_lastHighlight != null && _lastHighlight.active));
149 150
    if (widget.onHighlightChanged != null)
      widget.onHighlightChanged(value);
151 152
  }

153
  void _handleTapDown(TapDownDetails details) {
154
    final RenderBox referenceBox = context.findRenderObject();
155
    final RectCallback rectCallback = widget.getRectCallback(referenceBox);
156
    InkSplash splash;
157 158
    splash = new InkSplash(
      controller: Material.of(context),
159
      referenceBox: referenceBox,
160
      position: referenceBox.globalToLocal(details.globalPosition),
161 162 163 164 165
      color: widget.splashColor ?? Theme.of(context).splashColor,
      containedInkWell: widget.containedInkWell,
      rectCallback: widget.containedInkWell ? rectCallback : null,
      radius: widget.radius,
      borderRadius: widget.borderRadius ?? BorderRadius.zero,
166 167 168 169 170 171 172 173 174
      onRemoved: () {
        if (_splashes != null) {
          assert(_splashes.contains(splash));
          _splashes.remove(splash);
          if (_currentSplash == splash)
            _currentSplash = null;
        } // else we're probably in deactivate()
      }
    );
175
    _splashes ??= new HashSet<InkSplash>();
176 177
    _splashes.add(splash);
    _currentSplash = splash;
178
    updateHighlight(true);
179 180
  }

181 182 183
  void _handleTap() {
    _currentSplash?.confirm();
    _currentSplash = null;
184
    updateHighlight(false);
185 186
    if (widget.onTap != null)
      widget.onTap();
187 188
  }

189 190 191
  void _handleTapCancel() {
    _currentSplash?.cancel();
    _currentSplash = null;
192
    updateHighlight(false);
193 194
  }

195 196 197
  void _handleDoubleTap() {
    _currentSplash?.confirm();
    _currentSplash = null;
198 199
    if (widget.onDoubleTap != null)
      widget.onDoubleTap();
200 201
  }

202 203 204
  void _handleLongPress() {
    _currentSplash?.confirm();
    _currentSplash = null;
205 206
    if (widget.onLongPress != null)
      widget.onLongPress();
207 208
  }

209
  @override
210 211
  void deactivate() {
    if (_splashes != null) {
212
      final Set<InkSplash> splashes = _splashes;
213 214 215 216 217 218
      _splashes = null;
      for (InkSplash splash in splashes)
        splash.dispose();
      _currentSplash = null;
    }
    assert(_currentSplash == null);
219 220
    _lastHighlight?.dispose();
    _lastHighlight = null;
221
    super.deactivate();
222 223
  }

224
  @override
225
  Widget build(BuildContext context) {
226
    assert(widget.debugCheckContext(context));
227
    final ThemeData themeData = Theme.of(context);
228 229 230
    _lastHighlight?.color = widget.highlightColor ?? themeData.highlightColor;
    _currentSplash?.color = widget.splashColor ?? themeData.splashColor;
    final bool enabled = widget.onTap != null || widget.onDoubleTap != null || widget.onLongPress != null;
231 232 233 234
    return new GestureDetector(
      onTapDown: enabled ? _handleTapDown : null,
      onTap: enabled ? _handleTap : null,
      onTapCancel: enabled ? _handleTapCancel : null,
235 236
      onDoubleTap: widget.onDoubleTap != null ? _handleDoubleTap : null,
      onLongPress: widget.onLongPress != null ? _handleLongPress : null,
237
      behavior: HitTestBehavior.opaque,
238
      child: widget.child
239
    );
240 241
  }

242
}
243

244
/// A rectangular area of a [Material] that responds to touch.
245 246 247 248 249
///
/// Must have an ancestor [Material] widget in which to cause ink reactions.
///
/// If a Widget uses this class directly, it should include the following line
/// at the top of its [build] function to call [debugCheckHasMaterial]:
250
///
251
///     assert(debugCheckHasMaterial(context));
252
class InkWell extends InkResponse {
253 254 255
  /// Creates an ink well.
  ///
  /// Must have an ancestor [Material] widget in which to cause ink reactions.
256
  const InkWell({
257 258
    Key key,
    Widget child,
259
    GestureTapCallback onTap,
260
    GestureTapCallback onDoubleTap,
261
    GestureLongPressCallback onLongPress,
262 263 264
    ValueChanged<bool> onHighlightChanged,
    Color highlightColor,
    Color splashColor,
265
    BorderRadius borderRadius,
266 267 268 269 270
  }) : super(
    key: key,
    child: child,
    onTap: onTap,
    onDoubleTap: onDoubleTap,
271 272
    onLongPress: onLongPress,
    onHighlightChanged: onHighlightChanged,
273
    containedInkWell: true,
274 275 276
    highlightShape: BoxShape.rectangle,
    highlightColor: highlightColor,
    splashColor: splashColor,
277
    borderRadius: borderRadius,
278
  );
279
}