ink_well.dart 7.99 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';

7 8 9
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
10

11
import 'debug.dart';
12 13
import 'material.dart';
import 'theme.dart';
14

15
/// An area of a [Material] that responds to touch. Has a configurable shape and
16 17
/// can be configured to clip splashes that extend outside its bounds or not.
///
18
/// For a variant of this widget that is specialized for rectangular areas that
19 20 21 22 23 24 25 26
/// 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));
27
class InkResponse extends StatefulWidget {
28 29 30
  /// Creates an area of a [Material] that responds to touch.
  ///
  /// Must have an ancestor [Material] widget in which to cause ink reactions.
31
  InkResponse({
32 33 34
    Key key,
    this.child,
    this.onTap,
35
    this.onDoubleTap,
36
    this.onLongPress,
37
    this.onHighlightChanged,
38
    this.containedInkWell: false,
39 40
    this.highlightShape: BoxShape.circle,
    this.radius,
41 42
  }) : super(key: key);

43
  /// The widget below this widget in the tree.
44
  final Widget child;
45

46
  /// Called when the user taps this part of the material
47
  final GestureTapCallback onTap;
48

49
  /// Called when the user double taps this part of the material.
50
  final GestureTapCallback onDoubleTap;
51

52
  /// Called when the user long-presses on this part of the material.
53
  final GestureLongPressCallback onLongPress;
54

55 56 57 58 59
  /// 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.
60
  final ValueChanged<bool> onHighlightChanged;
61

62 63
  /// Whether this ink response should be clipped its bounds.
  final bool containedInkWell;
64

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

68 69 70
  /// The radius of the ink splash.
  final double radius;

71 72 73 74 75
  /// 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,
76
  /// [TableRowInkWell] implements this method to return the rectangle
77 78 79 80 81 82 83 84 85 86 87 88
  /// 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,
89
  /// [TableRowInkWell] implements this method to verify that the widget is
90 91 92 93 94 95
  /// in a table.
  bool debugCheckContext(BuildContext context) {
    assert(debugCheckHasMaterial(context));
    return true;
  }

96
  @override
97
  _InkResponseState<InkResponse> createState() => new _InkResponseState<InkResponse>();
98 99
}

100
class _InkResponseState<T extends InkResponse> extends State<T> {
101

102 103
  Set<InkSplash> _splashes;
  InkSplash _currentSplash;
104 105 106 107 108 109 110 111 112 113 114 115 116
  InkHighlight _lastHighlight;

  void updateHighlight(bool value) {
    if (value == (_lastHighlight != null && _lastHighlight.active))
      return;
    if (value) {
      if (_lastHighlight == null) {
        RenderBox referenceBox = context.findRenderObject();
        assert(Material.of(context) != null);
        _lastHighlight = Material.of(context).highlightAt(
          referenceBox: referenceBox,
          color: Theme.of(context).highlightColor,
          shape: config.highlightShape,
117
          rectCallback: config.getRectCallback(referenceBox),
118 119 120 121 122 123 124 125 126 127 128
          onRemoved: () {
            assert(_lastHighlight != null);
            _lastHighlight = null;
          }
        );
      } else {
        _lastHighlight.activate();
      }
    } else {
      _lastHighlight.deactivate();
    }
129
    assert(value == (_lastHighlight != null && _lastHighlight.active));
130
    if (config.onHighlightChanged != null)
131
      config.onHighlightChanged(value);
132 133
  }

134
  void _handleTapDown(TapDownDetails details) {
135 136 137
    RenderBox referenceBox = context.findRenderObject();
    assert(Material.of(context) != null);
    InkSplash splash;
138
    RectCallback rectCallback = config.getRectCallback(referenceBox);
139 140
    splash = Material.of(context).splashAt(
      referenceBox: referenceBox,
141
      position: referenceBox.globalToLocal(details.globalPosition),
142
      color: Theme.of(context).splashColor,
143 144
      containedInkWell: config.containedInkWell,
      rectCallback: config.containedInkWell ? rectCallback : null,
145
      radius: config.radius,
146 147 148 149 150 151 152 153 154
      onRemoved: () {
        if (_splashes != null) {
          assert(_splashes.contains(splash));
          _splashes.remove(splash);
          if (_currentSplash == splash)
            _currentSplash = null;
        } // else we're probably in deactivate()
      }
    );
155
    _splashes ??= new HashSet<InkSplash>();
156 157
    _splashes.add(splash);
    _currentSplash = splash;
158
    updateHighlight(true);
159 160
  }

161 162 163
  void _handleTap() {
    _currentSplash?.confirm();
    _currentSplash = null;
164
    updateHighlight(false);
165 166
    if (config.onTap != null)
      config.onTap();
167 168
  }

169 170 171
  void _handleTapCancel() {
    _currentSplash?.cancel();
    _currentSplash = null;
172
    updateHighlight(false);
173 174
  }

175 176 177 178 179
  void _handleDoubleTap() {
    _currentSplash?.confirm();
    _currentSplash = null;
    if (config.onDoubleTap != null)
      config.onDoubleTap();
180 181
  }

182 183 184 185 186 187 188
  void _handleLongPress() {
    _currentSplash?.confirm();
    _currentSplash = null;
    if (config.onLongPress != null)
      config.onLongPress();
  }

189
  @override
190 191 192 193 194 195 196 197 198
  void deactivate() {
    if (_splashes != null) {
      Set<InkSplash> splashes = _splashes;
      _splashes = null;
      for (InkSplash splash in splashes)
        splash.dispose();
      _currentSplash = null;
    }
    assert(_currentSplash == null);
199 200
    _lastHighlight?.dispose();
    _lastHighlight = null;
201
    super.deactivate();
202 203
  }

204
  @override
205
  Widget build(BuildContext context) {
206
    assert(config.debugCheckContext(context));
207
    _lastHighlight?.color = Theme.of(context).highlightColor;
208 209 210 211 212 213 214 215 216 217
    final bool enabled = config.onTap != null || config.onDoubleTap != null || config.onLongPress != null;
    return new GestureDetector(
      onTapDown: enabled ? _handleTapDown : null,
      onTap: enabled ? _handleTap : null,
      onTapCancel: enabled ? _handleTapCancel : null,
      onDoubleTap: config.onDoubleTap != null ? _handleDoubleTap : null,
      onLongPress: config.onLongPress != null ? _handleLongPress : null,
      behavior: HitTestBehavior.opaque,
      child: config.child
    );
218 219
  }

220
}
221

222 223 224 225 226 227
/// A rectangular area of a Material that responds to touch.
///
/// 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]:
228
///
229
///     assert(debugCheckHasMaterial(context));
230
class InkWell extends InkResponse {
231 232 233
  /// Creates an ink well.
  ///
  /// Must have an ancestor [Material] widget in which to cause ink reactions.
234 235 236
  InkWell({
    Key key,
    Widget child,
237
    GestureTapCallback onTap,
238
    GestureTapCallback onDoubleTap,
239
    GestureLongPressCallback onLongPress,
240
    ValueChanged<bool> onHighlightChanged
241 242 243 244 245
  }) : super(
    key: key,
    child: child,
    onTap: onTap,
    onDoubleTap: onDoubleTap,
246 247
    onLongPress: onLongPress,
    onHighlightChanged: onHighlightChanged,
248
    containedInkWell: true,
249
    highlightShape: BoxShape.rectangle
250
  );
251
}