• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2015 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'dart:collection';
6
7import 'package:flutter/foundation.dart';
8import 'package:flutter/gestures.dart';
9import 'package:flutter/rendering.dart';
10import 'package:flutter/widgets.dart';
11
12import 'debug.dart';
13import 'feedback.dart';
14import 'ink_highlight.dart';
15import 'material.dart';
16import 'theme.dart';
17
18/// An ink feature that displays a [color] "splash" in response to a user
19/// gesture that can be confirmed or canceled.
20///
21/// Subclasses call [confirm] when an input gesture is recognized. For
22/// example a press event might trigger an ink feature that's confirmed
23/// when the corresponding up event is seen.
24///
25/// Subclasses call [cancel] when an input gesture is aborted before it
26/// is recognized. For example a press event might trigger an ink feature
27/// that's canceled when the pointer is dragged out of the reference
28/// box.
29///
30/// The [InkWell] and [InkResponse] widgets generate instances of this
31/// class.
32abstract class InteractiveInkFeature extends InkFeature {
33  /// Creates an InteractiveInkFeature.
34  ///
35  /// The [controller] and [referenceBox] arguments must not be null.
36  InteractiveInkFeature({
37    @required MaterialInkController controller,
38    @required RenderBox referenceBox,
39    Color color,
40    VoidCallback onRemoved,
41  }) : assert(controller != null),
42       assert(referenceBox != null),
43       _color = color,
44       super(controller: controller, referenceBox: referenceBox, onRemoved: onRemoved);
45
46  /// Called when the user input that triggered this feature's appearance was confirmed.
47  ///
48  /// Typically causes the ink to propagate faster across the material. By default this
49  /// method does nothing.
50  void confirm() { }
51
52  /// Called when the user input that triggered this feature's appearance was canceled.
53  ///
54  /// Typically causes the ink to gradually disappear. By default this method does
55  /// nothing.
56  void cancel() { }
57
58  /// The ink's color.
59  Color get color => _color;
60  Color _color;
61  set color(Color value) {
62    if (value == _color)
63      return;
64    _color = value;
65    controller.markNeedsPaint();
66  }
67}
68
69/// An encapsulation of an [InteractiveInkFeature] constructor used by
70/// [InkWell], [InkResponse], and [ThemeData].
71///
72/// Interactive ink feature implementations should provide a static const
73/// `splashFactory` value that's an instance of this class. The `splashFactory`
74/// can be used to configure an [InkWell], [InkResponse] or [ThemeData].
75///
76/// See also:
77///
78///  * [InkSplash.splashFactory]
79///  * [InkRipple.splashFactory]
80abstract class InteractiveInkFeatureFactory {
81  /// Subclasses should provide a const constructor.
82  const InteractiveInkFeatureFactory();
83
84  /// The factory method.
85  ///
86  /// Subclasses should override this method to return a new instance of an
87  /// [InteractiveInkFeature].
88  InteractiveInkFeature create({
89    @required MaterialInkController controller,
90    @required RenderBox referenceBox,
91    @required Offset position,
92    @required Color color,
93    @required TextDirection textDirection,
94    bool containedInkWell = false,
95    RectCallback rectCallback,
96    BorderRadius borderRadius,
97    ShapeBorder customBorder,
98    double radius,
99    VoidCallback onRemoved,
100  });
101}
102
103/// An area of a [Material] that responds to touch. Has a configurable shape and
104/// can be configured to clip splashes that extend outside its bounds or not.
105///
106/// For a variant of this widget that is specialized for rectangular areas that
107/// always clip splashes, see [InkWell].
108///
109/// An [InkResponse] widget does two things when responding to a tap:
110///
111///  * It starts to animate a _highlight_. The shape of the highlight is
112///    determined by [highlightShape]. If it is a [BoxShape.circle], the
113///    default, then the highlight is a circle of fixed size centered in the
114///    [InkResponse]. If it is [BoxShape.rectangle], then the highlight is a box
115///    the size of the [InkResponse] itself, unless [getRectCallback] is
116///    provided, in which case that callback defines the rectangle. The color of
117///    the highlight is set by [highlightColor].
118///
119///  * Simultaneously, it starts to animate a _splash_. This is a growing circle
120///    initially centered on the tap location. If this is a [containedInkWell],
121///    the splash grows to the [radius] while remaining centered at the tap
122///    location. Otherwise, the splash migrates to the center of the box as it
123///    grows.
124///
125/// The following two diagrams show how [InkResponse] looks when tapped if the
126/// [highlightShape] is [BoxShape.circle] (the default) and [containedInkWell]
127/// is false (also the default).
128///
129/// The first diagram shows how it looks if the [InkResponse] is relatively
130/// large:
131///
132/// ![The highlight is a disc centered in the box, smaller than the child widget.](https://flutter.github.io/assets-for-api-docs/assets/material/ink_response_large.png)
133///
134/// The second diagram shows how it looks if the [InkResponse] is small:
135///
136/// ![The highlight is a disc overflowing the box, centered on the child.](https://flutter.github.io/assets-for-api-docs/assets/material/ink_response_small.png)
137///
138/// The main thing to notice from these diagrams is that the splashes happily
139/// exceed the bounds of the widget (because [containedInkWell] is false).
140///
141/// The following diagram shows the effect when the [InkResponse] has a
142/// [highlightShape] of [BoxShape.rectangle] with [containedInkWell] set to
143/// true. These are the values used by [InkWell].
144///
145/// ![The highlight is a rectangle the size of the box.](https://flutter.github.io/assets-for-api-docs/assets/material/ink_well.png)
146///
147/// The [InkResponse] widget must have a [Material] widget as an ancestor. The
148/// [Material] widget is where the ink reactions are actually painted. This
149/// matches the material design premise wherein the [Material] is what is
150/// actually reacting to touches by spreading ink.
151///
152/// If a Widget uses this class directly, it should include the following line
153/// at the top of its build function to call [debugCheckHasMaterial]:
154///
155/// ```dart
156/// assert(debugCheckHasMaterial(context));
157/// ```
158///
159/// ## Troubleshooting
160///
161/// ### The ink splashes aren't visible!
162///
163/// If there is an opaque graphic, e.g. painted using a [Container], [Image], or
164/// [DecoratedBox], between the [Material] widget and the [InkResponse] widget,
165/// then the splash won't be visible because it will be under the opaque graphic.
166/// This is because ink splashes draw on the underlying [Material] itself, as
167/// if the ink was spreading inside the material.
168///
169/// The [Ink] widget can be used as a replacement for [Image], [Container], or
170/// [DecoratedBox] to ensure that the image or decoration also paints in the
171/// [Material] itself, below the ink.
172///
173/// If this is not possible for some reason, e.g. because you are using an
174/// opaque [CustomPaint] widget, alternatively consider using a second
175/// [Material] above the opaque widget but below the [InkResponse] (as an
176/// ancestor to the ink response). The [MaterialType.transparency] material
177/// kind can be used for this purpose.
178///
179/// See also:
180///
181///  * [GestureDetector], for listening for gestures without ink splashes.
182///  * [RaisedButton] and [FlatButton], two kinds of buttons in material design.
183///  * [IconButton], which combines [InkResponse] with an [Icon].
184class InkResponse extends StatefulWidget {
185  /// Creates an area of a [Material] that responds to touch.
186  ///
187  /// Must have an ancestor [Material] widget in which to cause ink reactions.
188  ///
189  /// The [containedInkWell], [highlightShape], [enableFeedback], and
190  /// [excludeFromSemantics] arguments must not be null.
191  const InkResponse({
192    Key key,
193    this.child,
194    this.onTap,
195    this.onTapDown,
196    this.onTapCancel,
197    this.onDoubleTap,
198    this.onLongPress,
199    this.onHighlightChanged,
200    this.onHover,
201    this.containedInkWell = false,
202    this.highlightShape = BoxShape.circle,
203    this.radius,
204    this.borderRadius,
205    this.customBorder,
206    this.focusColor,
207    this.hoverColor,
208    this.highlightColor,
209    this.splashColor,
210    this.splashFactory,
211    this.enableFeedback = true,
212    this.excludeFromSemantics = false,
213  }) : assert(containedInkWell != null),
214       assert(highlightShape != null),
215       assert(enableFeedback != null),
216       assert(excludeFromSemantics != null),
217       super(key: key);
218
219  /// The widget below this widget in the tree.
220  ///
221  /// {@macro flutter.widgets.child}
222  final Widget child;
223
224  /// Called when the user taps this part of the material.
225  final GestureTapCallback onTap;
226
227  /// Called when the user taps down this part of the material.
228  final GestureTapDownCallback onTapDown;
229
230  /// Called when the user cancels a tap that was started on this part of the
231  /// material.
232  final GestureTapCallback onTapCancel;
233
234  /// Called when the user double taps this part of the material.
235  final GestureTapCallback onDoubleTap;
236
237  /// Called when the user long-presses on this part of the material.
238  final GestureLongPressCallback onLongPress;
239
240  /// Called when this part of the material either becomes highlighted or stops
241  /// being highlighted.
242  ///
243  /// The value passed to the callback is true if this part of the material has
244  /// become highlighted and false if this part of the material has stopped
245  /// being highlighted.
246  ///
247  /// If all of [onTap], [onDoubleTap], and [onLongPress] become null while a
248  /// gesture is ongoing, then [onTapCancel] will be fired and
249  /// [onHighlightChanged] will be fired with the value false _during the
250  /// build_. This means, for instance, that in that scenario [State.setState]
251  /// cannot be called.
252  final ValueChanged<bool> onHighlightChanged;
253
254  /// Called when a pointer enters or exits the ink response area.
255  ///
256  /// The value passed to the callback is true if a pointer has entered this
257  /// part of the material and false if a pointer has exited this part of the
258  /// material.
259  final ValueChanged<bool> onHover;
260
261  /// Whether this ink response should be clipped its bounds.
262  ///
263  /// This flag also controls whether the splash migrates to the center of the
264  /// [InkResponse] or not. If [containedInkWell] is true, the splash remains
265  /// centered around the tap location. If it is false, the splash migrates to
266  /// the center of the [InkResponse] as it grows.
267  ///
268  /// See also:
269  ///
270  ///  * [highlightShape], the shape of the focus, hover, and pressed
271  ///    highlights.
272  ///  * [borderRadius], which controls the corners when the box is a rectangle.
273  ///  * [getRectCallback], which controls the size and position of the box when
274  ///    it is a rectangle.
275  final bool containedInkWell;
276
277  /// The shape (e.g., circle, rectangle) to use for the highlight drawn around
278  /// this part of the material when pressed, hovered over, or focused.
279  ///
280  /// The same shape is used for the pressed highlight (see [highlightColor]),
281  /// the focus highlight (see [focusColor]), and the hover highlight (see
282  /// [hoverColor]).
283  ///
284  /// If the shape is [BoxShape.circle], then the highlight is centered on the
285  /// [InkResponse]. If the shape is [BoxShape.rectangle], then the highlight
286  /// fills the [InkResponse], or the rectangle provided by [getRectCallback] if
287  /// the callback is specified.
288  ///
289  /// See also:
290  ///
291  ///  * [containedInkWell], which controls clipping behavior.
292  ///  * [borderRadius], which controls the corners when the box is a rectangle.
293  ///  * [highlightColor], the color of the highlight.
294  ///  * [getRectCallback], which controls the size and position of the box when
295  ///    it is a rectangle.
296  final BoxShape highlightShape;
297
298  /// The radius of the ink splash.
299  ///
300  /// Splashes grow up to this size. By default, this size is determined from
301  /// the size of the rectangle provided by [getRectCallback], or the size of
302  /// the [InkResponse] itself.
303  ///
304  /// See also:
305  ///
306  ///  * [splashColor], the color of the splash.
307  ///  * [splashFactory], which defines the appearance of the splash.
308  final double radius;
309
310  /// The clipping radius of the containing rect. This is effective only if
311  /// [customBorder] is null.
312  ///
313  /// If this is null, it is interpreted as [BorderRadius.zero].
314  final BorderRadius borderRadius;
315
316  /// The custom clip border which overrides [borderRadius].
317  final ShapeBorder customBorder;
318
319  /// The color of the ink response when the parent widget is focused. If this
320  /// property is null then the focus color of the theme,
321  /// [ThemeData.focusColor], will be used.
322  ///
323  /// See also:
324  ///
325  ///  * [highlightShape], the shape of the focus, hover, and pressed
326  ///    highlights.
327  ///  * [hoverColor], the color of the hover highlight.
328  ///  * [splashColor], the color of the splash.
329  ///  * [splashFactory], which defines the appearance of the splash.
330  final Color focusColor;
331
332  /// The color of the ink response when a pointer is hovering over it. If this
333  /// property is null then the hover color of the theme,
334  /// [ThemeData.hoverColor], will be used.
335  ///
336  /// See also:
337  ///
338  ///  * [highlightShape], the shape of the focus, hover, and pressed
339  ///    highlights.
340  ///  * [highlightColor], the color of the pressed highlight.
341  ///  * [focusColor], the color of the focus highlight.
342  ///  * [splashColor], the color of the splash.
343  ///  * [splashFactory], which defines the appearance of the splash.
344  final Color hoverColor;
345
346  /// The highlight color of the ink response when pressed. If this property is
347  /// null then the highlight color of the theme, [ThemeData.highlightColor],
348  /// will be used.
349  ///
350  /// See also:
351  ///
352  ///  * [hoverColor], the color of the hover highlight.
353  ///  * [focusColor], the color of the focus highlight.
354  ///  * [highlightShape], the shape of the focus, hover, and pressed
355  ///    highlights.
356  ///  * [splashColor], the color of the splash.
357  ///  * [splashFactory], which defines the appearance of the splash.
358  final Color highlightColor;
359
360  /// The splash color of the ink response. If this property is null then the
361  /// splash color of the theme, [ThemeData.splashColor], will be used.
362  ///
363  /// See also:
364  ///
365  ///  * [splashFactory], which defines the appearance of the splash.
366  ///  * [radius], the (maximum) size of the ink splash.
367  ///  * [highlightColor], the color of the highlight.
368  final Color splashColor;
369
370  /// Defines the appearance of the splash.
371  ///
372  /// Defaults to the value of the theme's splash factory: [ThemeData.splashFactory].
373  ///
374  /// See also:
375  ///
376  ///  * [radius], the (maximum) size of the ink splash.
377  ///  * [splashColor], the color of the splash.
378  ///  * [highlightColor], the color of the highlight.
379  ///  * [InkSplash.splashFactory], which defines the default splash.
380  ///  * [InkRipple.splashFactory], which defines a splash that spreads out
381  ///    more aggressively than the default.
382  final InteractiveInkFeatureFactory splashFactory;
383
384  /// Whether detected gestures should provide acoustic and/or haptic feedback.
385  ///
386  /// For example, on Android a tap will produce a clicking sound and a
387  /// long-press will produce a short vibration, when feedback is enabled.
388  ///
389  /// See also:
390  ///
391  ///  * [Feedback] for providing platform-specific feedback to certain actions.
392  final bool enableFeedback;
393
394  /// Whether to exclude the gestures introduced by this widget from the
395  /// semantics tree.
396  ///
397  /// For example, a long-press gesture for showing a tooltip is usually
398  /// excluded because the tooltip itself is included in the semantics
399  /// tree directly and so having a gesture to show it would result in
400  /// duplication of information.
401  final bool excludeFromSemantics;
402
403  /// The rectangle to use for the highlight effect and for clipping
404  /// the splash effects if [containedInkWell] is true.
405  ///
406  /// This method is intended to be overridden by descendants that
407  /// specialize [InkResponse] for unusual cases. For example,
408  /// [TableRowInkWell] implements this method to return the rectangle
409  /// corresponding to the row that the widget is in.
410  ///
411  /// The default behavior returns null, which is equivalent to
412  /// returning the referenceBox argument's bounding box (though
413  /// slightly more efficient).
414  RectCallback getRectCallback(RenderBox referenceBox) => null;
415
416  /// Asserts that the given context satisfies the prerequisites for
417  /// this class.
418  ///
419  /// This method is intended to be overridden by descendants that
420  /// specialize [InkResponse] for unusual cases. For example,
421  /// [TableRowInkWell] implements this method to verify that the widget is
422  /// in a table.
423  @mustCallSuper
424  bool debugCheckContext(BuildContext context) {
425    assert(debugCheckHasMaterial(context));
426    assert(debugCheckHasDirectionality(context));
427    return true;
428  }
429
430  @override
431  _InkResponseState<InkResponse> createState() => _InkResponseState<InkResponse>();
432
433  @override
434  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
435    super.debugFillProperties(properties);
436    final List<String> gestures = <String>[];
437    if (onTap != null)
438      gestures.add('tap');
439    if (onDoubleTap != null)
440      gestures.add('double tap');
441    if (onLongPress != null)
442      gestures.add('long press');
443    if (onTapDown != null)
444      gestures.add('tap down');
445    if (onTapCancel != null)
446      gestures.add('tap cancel');
447    properties.add(IterableProperty<String>('gestures', gestures, ifEmpty: '<none>'));
448    properties.add(DiagnosticsProperty<bool>('containedInkWell', containedInkWell, level: DiagnosticLevel.fine));
449    properties.add(DiagnosticsProperty<BoxShape>(
450      'highlightShape',
451      highlightShape,
452      description: '${containedInkWell ? "clipped to " : ""}$highlightShape',
453      showName: false,
454    ));
455  }
456}
457
458/// Used to index the allocated highlights for the different types of highlights
459/// in [_InkResponseState].
460enum _HighlightType {
461  pressed,
462  hover,
463  focus,
464}
465
466class _InkResponseState<T extends InkResponse> extends State<T> with AutomaticKeepAliveClientMixin<T> {
467  Set<InteractiveInkFeature> _splashes;
468  InteractiveInkFeature _currentSplash;
469  FocusNode _focusNode;
470  bool _hovering = false;
471  final Map<_HighlightType, InkHighlight> _highlights = <_HighlightType, InkHighlight>{};
472
473  bool get highlightsExist => _highlights.values.where((InkHighlight highlight) => highlight != null).isNotEmpty;
474
475  @override
476  void didChangeDependencies() {
477    super.didChangeDependencies();
478    _focusNode?.removeListener(_handleFocusUpdate);
479    _focusNode = Focus.of(context, nullOk: true);
480    _focusNode?.addListener(_handleFocusUpdate);
481    WidgetsBinding.instance.focusManager.addHighlightModeListener(_handleFocusHighlightModeChange);
482  }
483
484  @override
485  void didUpdateWidget(InkResponse oldWidget) {
486    super.didUpdateWidget(oldWidget);
487    if (_isWidgetEnabled(widget) != _isWidgetEnabled(oldWidget)) {
488      _handleHoverChange(_hovering);
489      _handleFocusUpdate();
490    }
491  }
492
493  @override
494  void dispose() {
495    WidgetsBinding.instance.focusManager.removeHighlightModeListener(_handleFocusHighlightModeChange);
496    _focusNode?.removeListener(_handleFocusUpdate);
497    super.dispose();
498  }
499
500  @override
501  bool get wantKeepAlive => highlightsExist || (_splashes != null && _splashes.isNotEmpty);
502
503  Color getHighlightColorForType(_HighlightType type) {
504    switch (type) {
505      case _HighlightType.pressed:
506        return widget.highlightColor ?? Theme.of(context).highlightColor;
507      case _HighlightType.focus:
508        return widget.focusColor ?? Theme.of(context).focusColor;
509      case _HighlightType.hover:
510        return widget.hoverColor ?? Theme.of(context).hoverColor;
511    }
512    assert(false, 'Unhandled $_HighlightType $type');
513    return null;
514  }
515
516  Duration getFadeDurationForType(_HighlightType type) {
517    switch (type) {
518      case _HighlightType.pressed:
519        return const Duration(milliseconds: 200);
520      case _HighlightType.hover:
521      case _HighlightType.focus:
522        return const Duration(milliseconds: 50);
523    }
524    assert(false, 'Unhandled $_HighlightType $type');
525    return null;
526  }
527
528  void updateHighlight(_HighlightType type, {@required bool value}) {
529    final InkHighlight highlight = _highlights[type];
530    void handleInkRemoval() {
531      assert(_highlights[type] != null);
532      _highlights[type] = null;
533      updateKeepAlive();
534    }
535
536    if (value == (highlight != null && highlight.active))
537      return;
538    if (value) {
539      if (highlight == null) {
540        final RenderBox referenceBox = context.findRenderObject();
541        _highlights[type] = InkHighlight(
542          controller: Material.of(context),
543          referenceBox: referenceBox,
544          color: getHighlightColorForType(type),
545          shape: widget.highlightShape,
546          borderRadius: widget.borderRadius,
547          customBorder: widget.customBorder,
548          rectCallback: widget.getRectCallback(referenceBox),
549          onRemoved: handleInkRemoval,
550          textDirection: Directionality.of(context),
551          fadeDuration: getFadeDurationForType(type),
552        );
553        updateKeepAlive();
554      } else {
555        highlight.activate();
556      }
557    } else {
558      highlight.deactivate();
559    }
560    assert(value == (_highlights[type] != null && _highlights[type].active));
561
562    switch(type) {
563      case _HighlightType.pressed:
564        if (widget.onHighlightChanged != null)
565          widget.onHighlightChanged(value);
566        break;
567      case _HighlightType.hover:
568        if (widget.onHover != null)
569          widget.onHover(value);
570        break;
571      case _HighlightType.focus:
572        break;
573    }
574  }
575
576  InteractiveInkFeature _createInkFeature(TapDownDetails details) {
577    final MaterialInkController inkController = Material.of(context);
578    final RenderBox referenceBox = context.findRenderObject();
579    final Offset position = referenceBox.globalToLocal(details.globalPosition);
580    final Color color = widget.splashColor ?? Theme.of(context).splashColor;
581    final RectCallback rectCallback = widget.containedInkWell ? widget.getRectCallback(referenceBox) : null;
582    final BorderRadius borderRadius = widget.borderRadius;
583    final ShapeBorder customBorder = widget.customBorder;
584
585    InteractiveInkFeature splash;
586    void onRemoved() {
587      if (_splashes != null) {
588        assert(_splashes.contains(splash));
589        _splashes.remove(splash);
590        if (_currentSplash == splash)
591          _currentSplash = null;
592        updateKeepAlive();
593      } // else we're probably in deactivate()
594    }
595
596    splash = (widget.splashFactory ?? Theme.of(context).splashFactory).create(
597      controller: inkController,
598      referenceBox: referenceBox,
599      position: position,
600      color: color,
601      containedInkWell: widget.containedInkWell,
602      rectCallback: rectCallback,
603      radius: widget.radius,
604      borderRadius: borderRadius,
605      customBorder: customBorder,
606      onRemoved: onRemoved,
607      textDirection: Directionality.of(context),
608    );
609
610    return splash;
611  }
612
613  void _handleFocusHighlightModeChange(FocusHighlightMode mode) {
614    if (!mounted) {
615      return;
616    }
617    setState(() {
618      _handleFocusUpdate();
619    });
620  }
621
622  void _handleFocusUpdate() {
623    bool showFocus;
624    switch (WidgetsBinding.instance.focusManager.highlightMode) {
625      case FocusHighlightMode.touch:
626        showFocus = false;
627        break;
628      case FocusHighlightMode.traditional:
629        showFocus = enabled && (Focus.of(context, nullOk: true)?.hasPrimaryFocus ?? false);
630        break;
631    }
632    updateHighlight(_HighlightType.focus, value: showFocus);
633  }
634
635  void _handleTapDown(TapDownDetails details) {
636    final InteractiveInkFeature splash = _createInkFeature(details);
637    _splashes ??= HashSet<InteractiveInkFeature>();
638    _splashes.add(splash);
639    _currentSplash = splash;
640    if (widget.onTapDown != null) {
641      widget.onTapDown(details);
642    }
643    updateKeepAlive();
644    updateHighlight(_HighlightType.pressed, value: true);
645  }
646
647  void _handleTap(BuildContext context) {
648    _currentSplash?.confirm();
649    _currentSplash = null;
650    updateHighlight(_HighlightType.pressed, value: false);
651    if (widget.onTap != null) {
652      if (widget.enableFeedback)
653        Feedback.forTap(context);
654      widget.onTap();
655    }
656  }
657
658  void _handleTapCancel() {
659    _currentSplash?.cancel();
660    _currentSplash = null;
661    if (widget.onTapCancel != null) {
662      widget.onTapCancel();
663    }
664    updateHighlight(_HighlightType.pressed, value: false);
665  }
666
667  void _handleDoubleTap() {
668    _currentSplash?.confirm();
669    _currentSplash = null;
670    if (widget.onDoubleTap != null)
671      widget.onDoubleTap();
672  }
673
674  void _handleLongPress(BuildContext context) {
675    _currentSplash?.confirm();
676    _currentSplash = null;
677    if (widget.onLongPress != null) {
678      if (widget.enableFeedback)
679        Feedback.forLongPress(context);
680      widget.onLongPress();
681    }
682  }
683
684  @override
685  void deactivate() {
686    if (_splashes != null) {
687      final Set<InteractiveInkFeature> splashes = _splashes;
688      _splashes = null;
689      for (InteractiveInkFeature splash in splashes)
690        splash.dispose();
691      _currentSplash = null;
692    }
693    assert(_currentSplash == null);
694    for (_HighlightType highlight in _highlights.keys) {
695      _highlights[highlight]?.dispose();
696      _highlights[highlight] = null;
697    }
698    super.deactivate();
699  }
700
701  bool _isWidgetEnabled(InkResponse widget) {
702    return widget.onTap != null || widget.onDoubleTap != null || widget.onLongPress != null;
703  }
704
705  bool get enabled => _isWidgetEnabled(widget);
706
707  void _handleMouseEnter(PointerEnterEvent event) => _handleHoverChange(true);
708  void _handleMouseExit(PointerExitEvent event) => _handleHoverChange(false);
709  void _handleHoverChange(bool hovering) {
710    if (_hovering != hovering) {
711      _hovering = hovering;
712      updateHighlight(_HighlightType.hover, value: enabled && _hovering);
713    }
714  }
715
716  @override
717  Widget build(BuildContext context) {
718    assert(widget.debugCheckContext(context));
719    super.build(context); // See AutomaticKeepAliveClientMixin.
720    for (_HighlightType type in _highlights.keys) {
721      _highlights[type]?.color = getHighlightColorForType(type);
722    }
723    _currentSplash?.color = widget.splashColor ?? Theme.of(context).splashColor;
724    return MouseRegion(
725      onEnter: enabled ? _handleMouseEnter : null,
726      onExit: enabled ? _handleMouseExit : null,
727      child: GestureDetector(
728        onTapDown: enabled ? _handleTapDown : null,
729        onTap: enabled ? () => _handleTap(context) : null,
730        onTapCancel: enabled ? _handleTapCancel : null,
731        onDoubleTap: widget.onDoubleTap != null ? _handleDoubleTap : null,
732        onLongPress: widget.onLongPress != null ? () => _handleLongPress(context) : null,
733        behavior: HitTestBehavior.opaque,
734        child: widget.child,
735        excludeFromSemantics: widget.excludeFromSemantics,
736      ),
737    );
738  }
739}
740
741/// A rectangular area of a [Material] that responds to touch.
742///
743/// For a variant of this widget that does not clip splashes, see [InkResponse].
744///
745/// The following diagram shows how an [InkWell] looks when tapped, when using
746/// default values.
747///
748/// ![The highlight is a rectangle the size of the box.](https://flutter.github.io/assets-for-api-docs/assets/material/ink_well.png)
749///
750/// The [InkWell] widget must have a [Material] widget as an ancestor. The
751/// [Material] widget is where the ink reactions are actually painted. This
752/// matches the material design premise wherein the [Material] is what is
753/// actually reacting to touches by spreading ink.
754///
755/// If a Widget uses this class directly, it should include the following line
756/// at the top of its build function to call [debugCheckHasMaterial]:
757///
758/// ```dart
759/// assert(debugCheckHasMaterial(context));
760/// ```
761///
762/// ## Troubleshooting
763///
764/// ### The ink splashes aren't visible!
765///
766/// If there is an opaque graphic, e.g. painted using a [Container], [Image], or
767/// [DecoratedBox], between the [Material] widget and the [InkWell] widget, then
768/// the splash won't be visible because it will be under the opaque graphic.
769/// This is because ink splashes draw on the underlying [Material] itself, as
770/// if the ink was spreading inside the material.
771///
772/// The [Ink] widget can be used as a replacement for [Image], [Container], or
773/// [DecoratedBox] to ensure that the image or decoration also paints in the
774/// [Material] itself, below the ink.
775///
776/// If this is not possible for some reason, e.g. because you are using an
777/// opaque [CustomPaint] widget, alternatively consider using a second
778/// [Material] above the opaque widget but below the [InkWell] (as an
779/// ancestor to the ink well). The [MaterialType.transparency] material
780/// kind can be used for this purpose.
781///
782/// ### The ink splashes don't track the size of an animated container
783/// If the size of an InkWell's [Material] ancestor changes while the InkWell's
784/// splashes are expanding, you may notice that the splashes aren't clipped
785/// correctly. This can't be avoided.
786///
787/// An example of this situation is as follows:
788///
789/// {@tool snippet --template=stateful_widget_scaffold}
790///
791/// Tap the container to cause it to grow. Then, tap it again and hold before
792/// the widget reaches its maximum size to observe the clipped ink splash.
793///
794/// ```dart
795/// double sideLength = 50;
796///
797/// Widget build(BuildContext context) {
798///   return Center(
799///     child: AnimatedContainer(
800///       height: sideLength,
801///       width: sideLength,
802///       duration: Duration(seconds: 2),
803///       curve: Curves.easeIn,
804///       child: Material(
805///         color: Colors.yellow,
806///         child: InkWell(
807///           onTap: () {
808///             setState(() {
809///               sideLength == 50 ? sideLength = 100 : sideLength = 50;
810///             });
811///           },
812///         ),
813///       ),
814///     ),
815///   );
816/// }
817/// ```
818/// {@end-tool}
819///
820/// An InkWell's splashes will not properly update to conform to changes if the
821/// size of its underlying [Material], where the splashes are rendered, changes
822/// during animation. You should avoid using InkWells within [Material] widgets
823/// that are changing size.
824///
825/// See also:
826///
827///  * [GestureDetector], for listening for gestures without ink splashes.
828///  * [RaisedButton] and [FlatButton], two kinds of buttons in material design.
829///  * [InkResponse], a variant of [InkWell] that doesn't force a rectangular
830///    shape on the ink reaction.
831class InkWell extends InkResponse {
832  /// Creates an ink well.
833  ///
834  /// Must have an ancestor [Material] widget in which to cause ink reactions.
835  ///
836  /// The [enableFeedback] and [excludeFromSemantics] arguments must not be
837  /// null.
838  const InkWell({
839    Key key,
840    Widget child,
841    GestureTapCallback onTap,
842    GestureTapCallback onDoubleTap,
843    GestureLongPressCallback onLongPress,
844    GestureTapDownCallback onTapDown,
845    GestureTapCancelCallback onTapCancel,
846    ValueChanged<bool> onHighlightChanged,
847    ValueChanged<bool> onHover,
848    Color focusColor,
849    Color hoverColor,
850    Color highlightColor,
851    Color splashColor,
852    InteractiveInkFeatureFactory splashFactory,
853    double radius,
854    BorderRadius borderRadius,
855    ShapeBorder customBorder,
856    bool enableFeedback = true,
857    bool excludeFromSemantics = false,
858  }) : super(
859    key: key,
860    child: child,
861    onTap: onTap,
862    onDoubleTap: onDoubleTap,
863    onLongPress: onLongPress,
864    onTapDown: onTapDown,
865    onTapCancel: onTapCancel,
866    onHighlightChanged: onHighlightChanged,
867    onHover: onHover,
868    containedInkWell: true,
869    highlightShape: BoxShape.rectangle,
870    focusColor: focusColor,
871    hoverColor: hoverColor,
872    highlightColor: highlightColor,
873    splashColor: splashColor,
874    splashFactory: splashFactory,
875    radius: radius,
876    borderRadius: borderRadius,
877    customBorder: customBorder,
878    enableFeedback: enableFeedback ?? true,
879    excludeFromSemantics: excludeFromSemantics ?? false,
880  );
881}
882