ink_well.dart 49 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// 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
import 'feedback.dart';
14
import 'ink_highlight.dart';
15
import 'material.dart';
16
import 'material_state.dart';
17
import 'theme.dart';
18

19 20 21
// Examples can assume:
// late BuildContext context;

22 23 24 25 26 27 28 29 30
/// An ink feature that displays a [color] "splash" in response to a user
/// gesture that can be confirmed or canceled.
///
/// Subclasses call [confirm] when an input gesture is recognized. For
/// example a press event might trigger an ink feature that's confirmed
/// when the corresponding up event is seen.
///
/// Subclasses call [cancel] when an input gesture is aborted before it
/// is recognized. For example a press event might trigger an ink feature
31
/// that's canceled when the pointer is dragged out of the reference
32 33 34 35 36 37 38 39 40
/// box.
///
/// The [InkWell] and [InkResponse] widgets generate instances of this
/// class.
abstract class InteractiveInkFeature extends InkFeature {
  /// Creates an InteractiveInkFeature.
  ///
  /// The [controller] and [referenceBox] arguments must not be null.
  InteractiveInkFeature({
41 42
    required super.controller,
    required super.referenceBox,
43
    required Color color,
44
    super.onRemoved,
45 46
  }) : assert(controller != null),
       assert(referenceBox != null),
47
       _color = color;
48 49 50 51 52

  /// Called when the user input that triggered this feature's appearance was confirmed.
  ///
  /// Typically causes the ink to propagate faster across the material. By default this
  /// method does nothing.
53
  void confirm() { }
54 55 56 57 58

  /// Called when the user input that triggered this feature's appearance was canceled.
  ///
  /// Typically causes the ink to gradually disappear. By default this method does
  /// nothing.
59
  void cancel() { }
60 61 62 63 64

  /// The ink's color.
  Color get color => _color;
  Color _color;
  set color(Color value) {
65
    if (value == _color) {
66
      return;
67
    }
68 69 70
    _color = value;
    controller.markNeedsPaint();
  }
71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88

  /// Draws an ink splash or ink ripple on the passed in [Canvas].
  ///
  /// The [transform] argument is the [Matrix4] transform that typically
  /// shifts the coordinate space of the canvas to the space in which
  /// the ink circle is to be painted.
  ///
  /// [center] is the [Offset] from origin of the canvas where the center
  /// of the circle is drawn.
  ///
  /// [paint] takes a [Paint] object that describes the styles used to draw the ink circle.
  /// For example, [paint] can specify properties like color, strokewidth, colorFilter.
  ///
  /// [radius] is the radius of ink circle to be drawn on canvas.
  ///
  /// [clipCallback] is the callback used to obtain the [Rect] used for clipping the ink effect.
  /// If [clipCallback] is null, no clipping is performed on the ink circle.
  ///
89
  /// Clipping can happen in 3 different ways:
90 91 92 93 94 95 96 97 98 99 100 101 102 103
  ///  1. If [customBorder] is provided, it is used to determine the path
  ///     for clipping.
  ///  2. If [customBorder] is null, and [borderRadius] is provided, the canvas
  ///     is clipped by an [RRect] created from [clipCallback] and [borderRadius].
  ///  3. If [borderRadius] is the default [BorderRadius.zero], then the [Rect] provided
  ///      by [clipCallback] is used for clipping.
  ///
  /// [textDirection] is used by [customBorder] if it is non-null. This allows the [customBorder]'s path
  /// to be properly defined if it was the path was expressed in terms of "start" and "end" instead of
  /// "left" and "right".
  ///
  /// For examples on how the function is used, see [InkSplash] and [InkRipple].
  @protected
  void paintInkCircle({
104 105 106 107 108 109 110
    required Canvas canvas,
    required Matrix4 transform,
    required Paint paint,
    required Offset center,
    required double radius,
    TextDirection? textDirection,
    ShapeBorder? customBorder,
111
    BorderRadius borderRadius = BorderRadius.zero,
112
    RectCallback? clipCallback,
113
  }) {
114 115 116 117 118 119 120
    assert(canvas != null);
    assert(transform != null);
    assert(paint != null);
    assert(center != null);
    assert(radius != null);
    assert(borderRadius != null);

121
    final Offset? originOffset = MatrixUtils.getAsTranslation(transform);
122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144
    canvas.save();
    if (originOffset == null) {
      canvas.transform(transform.storage);
    } else {
      canvas.translate(originOffset.dx, originOffset.dy);
    }
    if (clipCallback != null) {
      final Rect rect = clipCallback();
      if (customBorder != null) {
        canvas.clipPath(customBorder.getOuterPath(rect, textDirection: textDirection));
      } else if (borderRadius != BorderRadius.zero) {
        canvas.clipRRect(RRect.fromRectAndCorners(
          rect,
          topLeft: borderRadius.topLeft, topRight: borderRadius.topRight,
          bottomLeft: borderRadius.bottomLeft, bottomRight: borderRadius.bottomRight,
        ));
      } else {
        canvas.clipRect(rect);
      }
    }
    canvas.drawCircle(center, radius, paint);
    canvas.restore();
  }
145 146
}

147 148
/// An encapsulation of an [InteractiveInkFeature] constructor used by
/// [InkWell], [InkResponse], and [ThemeData].
149 150 151 152 153 154 155 156 157 158
///
/// Interactive ink feature implementations should provide a static const
/// `splashFactory` value that's an instance of this class. The `splashFactory`
/// can be used to configure an [InkWell], [InkResponse] or [ThemeData].
///
/// See also:
///
///  * [InkSplash.splashFactory]
///  * [InkRipple.splashFactory]
abstract class InteractiveInkFeatureFactory {
159 160 161
  /// Abstract const constructor. This constructor enables subclasses to provide
  /// const constructors so that they can be used in const expressions.
  ///
162 163 164 165 166 167 168
  /// Subclasses should provide a const constructor.
  const InteractiveInkFeatureFactory();

  /// The factory method.
  ///
  /// Subclasses should override this method to return a new instance of an
  /// [InteractiveInkFeature].
169
  @factory
170
  InteractiveInkFeature create({
171 172 173 174 175
    required MaterialInkController controller,
    required RenderBox referenceBox,
    required Offset position,
    required Color color,
    required TextDirection textDirection,
176
    bool containedInkWell = false,
177 178 179 180
    RectCallback? rectCallback,
    BorderRadius? borderRadius,
    ShapeBorder? customBorder,
    double? radius,
181
    VoidCallback? onRemoved,
182 183 184
  });
}

185 186 187 188 189 190
abstract class _ParentInkResponseState {
  void markChildInkResponsePressed(_ParentInkResponseState childState, bool value);
}

class _ParentInkResponseProvider extends InheritedWidget {
  const _ParentInkResponseProvider({
191
    required this.state,
192 193
    required super.child,
  });
194 195 196 197 198 199

  final _ParentInkResponseState state;

  @override
  bool updateShouldNotify(_ParentInkResponseProvider oldWidget) => state != oldWidget.state;

200
  static _ParentInkResponseState? maybeOf(BuildContext context) {
201 202 203 204
    return context.dependOnInheritedWidgetOfExactType<_ParentInkResponseProvider>()?.state;
  }
}

205
typedef _GetRectCallback = RectCallback? Function(RenderBox referenceBox);
206 207
typedef _CheckContext = bool Function(BuildContext context);

208
/// An area of a [Material] that responds to touch. Has a configurable shape and
209 210
/// can be configured to clip splashes that extend outside its bounds or not.
///
211
/// For a variant of this widget that is specialized for rectangular areas that
212 213
/// always clip splashes, see [InkWell].
///
214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229
/// An [InkResponse] widget does two things when responding to a tap:
///
///  * It starts to animate a _highlight_. The shape of the highlight is
///    determined by [highlightShape]. If it is a [BoxShape.circle], the
///    default, then the highlight is a circle of fixed size centered in the
///    [InkResponse]. If it is [BoxShape.rectangle], then the highlight is a box
///    the size of the [InkResponse] itself, unless [getRectCallback] is
///    provided, in which case that callback defines the rectangle. The color of
///    the highlight is set by [highlightColor].
///
///  * Simultaneously, it starts to animate a _splash_. This is a growing circle
///    initially centered on the tap location. If this is a [containedInkWell],
///    the splash grows to the [radius] while remaining centered at the tap
///    location. Otherwise, the splash migrates to the center of the box as it
///    grows.
///
230 231
/// The following two diagrams show how [InkResponse] looks when tapped if the
/// [highlightShape] is [BoxShape.circle] (the default) and [containedInkWell]
232 233 234 235
/// is false (also the default).
///
/// The first diagram shows how it looks if the [InkResponse] is relatively
/// large:
236
///
237
/// ![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)
238 239 240
///
/// The second diagram shows how it looks if the [InkResponse] is small:
///
241
/// ![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)
242
///
243 244 245
/// The main thing to notice from these diagrams is that the splashes happily
/// exceed the bounds of the widget (because [containedInkWell] is false).
///
246 247 248 249
/// The following diagram shows the effect when the [InkResponse] has a
/// [highlightShape] of [BoxShape.rectangle] with [containedInkWell] set to
/// true. These are the values used by [InkWell].
///
250
/// ![The highlight is a rectangle the size of the box.](https://flutter.github.io/assets-for-api-docs/assets/material/ink_well.png)
251
///
252
/// The [InkResponse] widget must have a [Material] widget as an ancestor. The
253
/// [Material] widget is where the ink reactions are actually painted. This
254
/// matches the Material Design premise wherein the [Material] is what is
255
/// actually reacting to touches by spreading ink.
256 257
///
/// If a Widget uses this class directly, it should include the following line
258
/// at the top of its build function to call [debugCheckHasMaterial]:
259
///
260 261 262
/// ```dart
/// assert(debugCheckHasMaterial(context));
/// ```
Ian Hickson's avatar
Ian Hickson committed
263 264 265 266 267 268 269
///
/// ## Troubleshooting
///
/// ### The ink splashes aren't visible!
///
/// If there is an opaque graphic, e.g. painted using a [Container], [Image], or
/// [DecoratedBox], between the [Material] widget and the [InkResponse] widget,
270 271 272 273 274 275 276 277 278 279 280 281 282
/// then the splash won't be visible because it will be under the opaque graphic.
/// This is because ink splashes draw on the underlying [Material] itself, as
/// if the ink was spreading inside the material.
///
/// The [Ink] widget can be used as a replacement for [Image], [Container], or
/// [DecoratedBox] to ensure that the image or decoration also paints in the
/// [Material] itself, below the ink.
///
/// If this is not possible for some reason, e.g. because you are using an
/// opaque [CustomPaint] widget, alternatively consider using a second
/// [Material] above the opaque widget but below the [InkResponse] (as an
/// ancestor to the ink response). The [MaterialType.transparency] material
/// kind can be used for this purpose.
283 284 285 286
///
/// See also:
///
///  * [GestureDetector], for listening for gestures without ink splashes.
287
///  * [ElevatedButton] and [TextButton], two kinds of buttons in Material Design.
288
///  * [IconButton], which combines [InkResponse] with an [Icon].
289
class InkResponse extends StatelessWidget {
290 291 292
  /// Creates an area of a [Material] that responds to touch.
  ///
  /// Must have an ancestor [Material] widget in which to cause ink reactions.
Ian Hickson's avatar
Ian Hickson committed
293
  ///
294
  /// The [containedInkWell], [highlightShape], [enableFeedback],
295
  /// and [excludeFromSemantics] arguments must not be null.
296
  const InkResponse({
297
    super.key,
298 299
    this.child,
    this.onTap,
300
    this.onTapDown,
301
    this.onTapUp,
302
    this.onTapCancel,
303
    this.onDoubleTap,
304
    this.onLongPress,
305
    this.onHighlightChanged,
306
    this.onHover,
307
    this.mouseCursor,
308 309
    this.containedInkWell = false,
    this.highlightShape = BoxShape.circle,
310
    this.radius,
Ian Hickson's avatar
Ian Hickson committed
311
    this.borderRadius,
312
    this.customBorder,
313 314
    this.focusColor,
    this.hoverColor,
315
    this.highlightColor,
316
    this.overlayColor,
317
    this.splashColor,
318
    this.splashFactory,
319 320
    this.enableFeedback = true,
    this.excludeFromSemantics = false,
321 322 323 324
    this.focusNode,
    this.canRequestFocus = true,
    this.onFocusChange,
    this.autofocus = false,
325
    this.statesController,
326
  }) : assert(containedInkWell != null),
Ian Hickson's avatar
Ian Hickson committed
327 328 329
       assert(highlightShape != null),
       assert(enableFeedback != null),
       assert(excludeFromSemantics != null),
330
       assert(autofocus != null),
331
       assert(canRequestFocus != null);
332

333
  /// The widget below this widget in the tree.
334
  ///
335
  /// {@macro flutter.widgets.ProxyWidget.child}
336
  final Widget? child;
337

338
  /// Called when the user taps this part of the material.
339
  final GestureTapCallback? onTap;
340

341
  /// Called when the user taps down this part of the material.
342
  final GestureTapDownCallback? onTapDown;
343

344 345 346 347
  /// Called when the user releases a tap that was started on this part of the
  /// material. [onTap] is called immediately after.
  final GestureTapUpCallback? onTapUp;

348 349
  /// Called when the user cancels a tap that was started on this part of the
  /// material.
350
  final GestureTapCallback? onTapCancel;
351

352
  /// Called when the user double taps this part of the material.
353
  final GestureTapCallback? onDoubleTap;
354

355
  /// Called when the user long-presses on this part of the material.
356
  final GestureLongPressCallback? onLongPress;
357

358 359
  /// Called when this part of the material either becomes highlighted or stops
  /// being highlighted.
360 361 362 363
  ///
  /// 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.
364 365 366 367 368 369
  ///
  /// If all of [onTap], [onDoubleTap], and [onLongPress] become null while a
  /// gesture is ongoing, then [onTapCancel] will be fired and
  /// [onHighlightChanged] will be fired with the value false _during the
  /// build_. This means, for instance, that in that scenario [State.setState]
  /// cannot be called.
370
  final ValueChanged<bool>? onHighlightChanged;
371

372 373 374 375 376
  /// Called when a pointer enters or exits the ink response area.
  ///
  /// The value passed to the callback is true if a pointer has entered this
  /// part of the material and false if a pointer has exited this part of the
  /// material.
377
  final ValueChanged<bool>? onHover;
378

379
  /// The cursor for a mouse pointer when it enters or is hovering over the
380
  /// widget.
381
  ///
382 383 384 385 386 387 388 389
  /// If [mouseCursor] is a [MaterialStateProperty<MouseCursor>],
  /// [MaterialStateProperty.resolve] is used for the following [MaterialState]s:
  ///
  ///  * [MaterialState.hovered].
  ///  * [MaterialState.focused].
  ///  * [MaterialState.disabled].
  ///
  /// If this property is null, [MaterialStateMouseCursor.clickable] will be used.
390
  final MouseCursor? mouseCursor;
391

392
  /// Whether this ink response should be clipped its bounds.
393 394 395 396 397 398 399 400
  ///
  /// This flag also controls whether the splash migrates to the center of the
  /// [InkResponse] or not. If [containedInkWell] is true, the splash remains
  /// centered around the tap location. If it is false, the splash migrates to
  /// the center of the [InkResponse] as it grows.
  ///
  /// See also:
  ///
401 402
  ///  * [highlightShape], the shape of the focus, hover, and pressed
  ///    highlights.
403 404 405
  ///  * [borderRadius], which controls the corners when the box is a rectangle.
  ///  * [getRectCallback], which controls the size and position of the box when
  ///    it is a rectangle.
406
  final bool containedInkWell;
407

408
  /// The shape (e.g., circle, rectangle) to use for the highlight drawn around
409 410 411 412 413
  /// this part of the material when pressed, hovered over, or focused.
  ///
  /// The same shape is used for the pressed highlight (see [highlightColor]),
  /// the focus highlight (see [focusColor]), and the hover highlight (see
  /// [hoverColor]).
414 415 416 417 418 419 420 421 422 423 424 425 426
  ///
  /// If the shape is [BoxShape.circle], then the highlight is centered on the
  /// [InkResponse]. If the shape is [BoxShape.rectangle], then the highlight
  /// fills the [InkResponse], or the rectangle provided by [getRectCallback] if
  /// the callback is specified.
  ///
  /// See also:
  ///
  ///  * [containedInkWell], which controls clipping behavior.
  ///  * [borderRadius], which controls the corners when the box is a rectangle.
  ///  * [highlightColor], the color of the highlight.
  ///  * [getRectCallback], which controls the size and position of the box when
  ///    it is a rectangle.
427
  final BoxShape highlightShape;
428

429
  /// The radius of the ink splash.
430 431 432 433 434 435 436 437
  ///
  /// Splashes grow up to this size. By default, this size is determined from
  /// the size of the rectangle provided by [getRectCallback], or the size of
  /// the [InkResponse] itself.
  ///
  /// See also:
  ///
  ///  * [splashColor], the color of the splash.
438
  ///  * [splashFactory], which defines the appearance of the splash.
439
  final double? radius;
440

441 442
  /// The border radius of the containing rectangle. This is effective only if
  /// [highlightShape] is [BoxShape.rectangle].
Ian Hickson's avatar
Ian Hickson committed
443 444
  ///
  /// If this is null, it is interpreted as [BorderRadius.zero].
445
  final BorderRadius? borderRadius;
446

447 448 449
  /// The custom clip border.
  ///
  /// If this is null, the ink response will not clip its content.
450
  final ShapeBorder? customBorder;
451

452 453 454 455 456 457 458 459 460 461 462
  /// The color of the ink response when the parent widget is focused. If this
  /// property is null then the focus color of the theme,
  /// [ThemeData.focusColor], will be used.
  ///
  /// See also:
  ///
  ///  * [highlightShape], the shape of the focus, hover, and pressed
  ///    highlights.
  ///  * [hoverColor], the color of the hover highlight.
  ///  * [splashColor], the color of the splash.
  ///  * [splashFactory], which defines the appearance of the splash.
463
  final Color? focusColor;
464 465 466 467 468 469 470 471 472 473 474 475 476

  /// The color of the ink response when a pointer is hovering over it. If this
  /// property is null then the hover color of the theme,
  /// [ThemeData.hoverColor], will be used.
  ///
  /// See also:
  ///
  ///  * [highlightShape], the shape of the focus, hover, and pressed
  ///    highlights.
  ///  * [highlightColor], the color of the pressed highlight.
  ///  * [focusColor], the color of the focus highlight.
  ///  * [splashColor], the color of the splash.
  ///  * [splashFactory], which defines the appearance of the splash.
477
  final Color? hoverColor;
478 479 480 481

  /// The highlight color of the ink response when pressed. If this property is
  /// null then the highlight color of the theme, [ThemeData.highlightColor],
  /// will be used.
482 483 484
  ///
  /// See also:
  ///
485 486 487 488
  ///  * [hoverColor], the color of the hover highlight.
  ///  * [focusColor], the color of the focus highlight.
  ///  * [highlightShape], the shape of the focus, hover, and pressed
  ///    highlights.
489
  ///  * [splashColor], the color of the splash.
490
  ///  * [splashFactory], which defines the appearance of the splash.
491
  final Color? highlightColor;
492

493 494 495
  /// Defines the ink response focus, hover, and splash colors.
  ///
  /// This default null property can be used as an alternative to
496 497 498 499 500 501
  /// [focusColor], [hoverColor], [highlightColor], and
  /// [splashColor]. If non-null, it is resolved against one of
  /// [MaterialState.focused], [MaterialState.hovered], and
  /// [MaterialState.pressed]. It's convenient to use when the parent
  /// widget can pass along its own MaterialStateProperty value for
  /// the overlay color.
502 503 504 505
  ///
  /// [MaterialState.pressed] triggers a ripple (an ink splash), per
  /// the current Material Design spec. The [overlayColor] doesn't map
  /// a state to [highlightColor] because a separate highlight is not
506
  /// used by the current design guidelines. See
507 508 509 510 511 512 513 514
  /// https://material.io/design/interaction/states.html#pressed
  ///
  /// If the overlay color is null or resolves to null, then [focusColor],
  /// [hoverColor], [splashColor] and their defaults are used instead.
  ///
  /// See also:
  ///
  ///  * The Material Design specification for overlay colors and how they
515
  ///    match a component's state:
516
  ///    <https://material.io/design/interaction/states.html#anatomy>.
517
  final MaterialStateProperty<Color?>? overlayColor;
518

519
  /// The splash color of the ink response. If this property is null then the
520
  /// splash color of the theme, [ThemeData.splashColor], will be used.
521 522 523
  ///
  /// See also:
  ///
524
  ///  * [splashFactory], which defines the appearance of the splash.
525 526
  ///  * [radius], the (maximum) size of the ink splash.
  ///  * [highlightColor], the color of the highlight.
527
  final Color? splashColor;
528

529 530 531 532 533 534 535 536 537 538 539
  /// Defines the appearance of the splash.
  ///
  /// Defaults to the value of the theme's splash factory: [ThemeData.splashFactory].
  ///
  /// See also:
  ///
  ///  * [radius], the (maximum) size of the ink splash.
  ///  * [splashColor], the color of the splash.
  ///  * [highlightColor], the color of the highlight.
  ///  * [InkSplash.splashFactory], which defines the default splash.
  ///  * [InkRipple.splashFactory], which defines a splash that spreads out
540
  ///    more aggressively than the default.
541
  final InteractiveInkFeatureFactory? splashFactory;
542

543 544 545 546 547 548 549 550 551 552
  /// Whether detected gestures should provide acoustic and/or haptic feedback.
  ///
  /// For example, on Android a tap will produce a clicking sound and a
  /// long-press will produce a short vibration, when feedback is enabled.
  ///
  /// See also:
  ///
  ///  * [Feedback] for providing platform-specific feedback to certain actions.
  final bool enableFeedback;

553 554 555 556 557 558 559 560 561
  /// Whether to exclude the gestures introduced by this widget from the
  /// semantics tree.
  ///
  /// For example, a long-press gesture for showing a tooltip is usually
  /// excluded because the tooltip itself is included in the semantics
  /// tree directly and so having a gesture to show it would result in
  /// duplication of information.
  final bool excludeFromSemantics;

562
  /// {@template flutter.material.inkwell.onFocusChange}
563 564 565 566
  /// Handler called when the focus changes.
  ///
  /// Called with true if this widget's node gains focus, and false if it loses
  /// focus.
567
  /// {@endtemplate}
568
  final ValueChanged<bool>? onFocusChange;
569 570 571 572 573

  /// {@macro flutter.widgets.Focus.autofocus}
  final bool autofocus;

  /// {@macro flutter.widgets.Focus.focusNode}
574
  final FocusNode? focusNode;
575

576
  /// {@macro flutter.widgets.Focus.canRequestFocus}
577 578
  final bool canRequestFocus;

579 580 581 582 583
  /// 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,
584
  /// [TableRowInkWell] implements this method to return the rectangle
585 586 587 588 589
  /// 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).
590
  RectCallback? getRectCallback(RenderBox referenceBox) => null;
591

592 593 594 595 596 597 598 599 600 601 602 603 604
  /// {@template flutter.material.inkwell.statesController}
  /// Represents the interactive "state" of this widget in terms of
  /// a set of [MaterialState]s, like [MaterialState.pressed] and
  /// [MaterialState.focused].
  ///
  /// Classes based on this one can provide their own
  /// [MaterialStatesController] to which they've added listeners.
  /// They can also update the controller's [MaterialStatesController.value]
  /// however, this may only be done when it's safe to call
  /// [State.setState], like in an event handler.
  /// {@endtemplate}
  final MaterialStatesController? statesController;

605 606
  @override
  Widget build(BuildContext context) {
607
    final _ParentInkResponseState? parentState = _ParentInkResponseProvider.maybeOf(context);
608
    return _InkResponseStateWidget(
609 610
      onTap: onTap,
      onTapDown: onTapDown,
611
      onTapUp: onTapUp,
612 613 614 615 616
      onTapCancel: onTapCancel,
      onDoubleTap: onDoubleTap,
      onLongPress: onLongPress,
      onHighlightChanged: onHighlightChanged,
      onHover: onHover,
617
      mouseCursor: mouseCursor,
618 619 620 621 622 623 624 625
      containedInkWell: containedInkWell,
      highlightShape: highlightShape,
      radius: radius,
      borderRadius: borderRadius,
      customBorder: customBorder,
      focusColor: focusColor,
      hoverColor: hoverColor,
      highlightColor: highlightColor,
626
      overlayColor: overlayColor,
627 628 629 630 631 632 633 634 635 636 637
      splashColor: splashColor,
      splashFactory: splashFactory,
      enableFeedback: enableFeedback,
      excludeFromSemantics: excludeFromSemantics,
      focusNode: focusNode,
      canRequestFocus: canRequestFocus,
      onFocusChange: onFocusChange,
      autofocus: autofocus,
      parentState: parentState,
      getRectCallback: getRectCallback,
      debugCheckContext: debugCheckContext,
638
      statesController: statesController,
639
      child: child,
640 641 642
    );
  }

643 644 645 646 647
  /// 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,
648
  /// [TableRowInkWell] implements this method to verify that the widget is
649
  /// in a table.
650
  @mustCallSuper
651 652
  bool debugCheckContext(BuildContext context) {
    assert(debugCheckHasMaterial(context));
653
    assert(debugCheckHasDirectionality(context));
654 655
    return true;
  }
656 657
}

658 659
class _InkResponseStateWidget extends StatefulWidget {
  const _InkResponseStateWidget({
660 661 662
    this.child,
    this.onTap,
    this.onTapDown,
663
    this.onTapUp,
664 665 666 667 668
    this.onTapCancel,
    this.onDoubleTap,
    this.onLongPress,
    this.onHighlightChanged,
    this.onHover,
669
    this.mouseCursor,
670 671 672 673 674 675 676 677
    this.containedInkWell = false,
    this.highlightShape = BoxShape.circle,
    this.radius,
    this.borderRadius,
    this.customBorder,
    this.focusColor,
    this.hoverColor,
    this.highlightColor,
678
    this.overlayColor,
679 680 681 682 683 684 685 686 687 688
    this.splashColor,
    this.splashFactory,
    this.enableFeedback = true,
    this.excludeFromSemantics = false,
    this.focusNode,
    this.canRequestFocus = true,
    this.onFocusChange,
    this.autofocus = false,
    this.parentState,
    this.getRectCallback,
689
    required this.debugCheckContext,
690
    this.statesController,
691 692 693 694 695
  }) : assert(containedInkWell != null),
       assert(highlightShape != null),
       assert(enableFeedback != null),
       assert(excludeFromSemantics != null),
       assert(autofocus != null),
696
       assert(canRequestFocus != null);
697

698 699 700
  final Widget? child;
  final GestureTapCallback? onTap;
  final GestureTapDownCallback? onTapDown;
701
  final GestureTapUpCallback? onTapUp;
702 703 704 705 706 707
  final GestureTapCallback? onTapCancel;
  final GestureTapCallback? onDoubleTap;
  final GestureLongPressCallback? onLongPress;
  final ValueChanged<bool>? onHighlightChanged;
  final ValueChanged<bool>? onHover;
  final MouseCursor? mouseCursor;
708 709
  final bool containedInkWell;
  final BoxShape highlightShape;
710 711 712 713 714 715 716 717 718
  final double? radius;
  final BorderRadius? borderRadius;
  final ShapeBorder? customBorder;
  final Color? focusColor;
  final Color? hoverColor;
  final Color? highlightColor;
  final MaterialStateProperty<Color?>? overlayColor;
  final Color? splashColor;
  final InteractiveInkFeatureFactory? splashFactory;
719 720
  final bool enableFeedback;
  final bool excludeFromSemantics;
721
  final ValueChanged<bool>? onFocusChange;
722
  final bool autofocus;
723
  final FocusNode? focusNode;
724
  final bool canRequestFocus;
725 726
  final _ParentInkResponseState? parentState;
  final _GetRectCallback? getRectCallback;
727
  final _CheckContext debugCheckContext;
728
  final MaterialStatesController? statesController;
729

730
  @override
731
  _InkResponseState createState() => _InkResponseState();
Ian Hickson's avatar
Ian Hickson committed
732 733

  @override
734 735
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
736 737 738 739 740
    final List<String> gestures = <String>[
      if (onTap != null) 'tap',
      if (onDoubleTap != null) 'double tap',
      if (onLongPress != null) 'long press',
      if (onTapDown != null) 'tap down',
741
      if (onTapUp != null) 'tap up',
742 743
      if (onTapCancel != null) 'tap cancel',
    ];
744
    properties.add(IterableProperty<String>('gestures', gestures, ifEmpty: '<none>'));
745
    properties.add(DiagnosticsProperty<MouseCursor>('mouseCursor', mouseCursor));
746 747
    properties.add(DiagnosticsProperty<bool>('containedInkWell', containedInkWell, level: DiagnosticLevel.fine));
    properties.add(DiagnosticsProperty<BoxShape>(
748 749 750 751 752
      'highlightShape',
      highlightShape,
      description: '${containedInkWell ? "clipped to " : ""}$highlightShape',
      showName: false,
    ));
Ian Hickson's avatar
Ian Hickson committed
753
  }
754 755
}

756 757 758 759 760 761 762 763
/// Used to index the allocated highlights for the different types of highlights
/// in [_InkResponseState].
enum _HighlightType {
  pressed,
  hover,
  focus,
}

764
class _InkResponseState extends State<_InkResponseStateWidget>
765 766 767
  with AutomaticKeepAliveClientMixin<_InkResponseStateWidget>
  implements _ParentInkResponseState
{
768 769
  Set<InteractiveInkFeature>? _splashes;
  InteractiveInkFeature? _currentSplash;
770
  bool _hovering = false;
771
  final Map<_HighlightType, InkHighlight?> _highlights = <_HighlightType, InkHighlight?>{};
772
  late final Map<Type, Action<Intent>> _actionMap = <Type, Action<Intent>>{
773 774
    ActivateIntent: CallbackAction<ActivateIntent>(onInvoke: simulateTap),
    ButtonActivateIntent: CallbackAction<ButtonActivateIntent>(onInvoke: simulateTap),
775
  };
776
  MaterialStatesController? internalStatesController;
777

778
  bool get highlightsExist => _highlights.values.where((InkHighlight? highlight) => highlight != null).isNotEmpty;
779

780
  final ObserverList<_ParentInkResponseState> _activeChildren = ObserverList<_ParentInkResponseState>();
781

782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797
  @override
  void markChildInkResponsePressed(_ParentInkResponseState childState, bool value) {
    assert(childState != null);
    final bool lastAnyPressed = _anyChildInkResponsePressed;
    if (value) {
      _activeChildren.add(childState);
    } else {
      _activeChildren.remove(childState);
    }
    final bool nowAnyPressed = _anyChildInkResponsePressed;
    if (nowAnyPressed != lastAnyPressed) {
      widget.parentState?.markChildInkResponsePressed(this, nowAnyPressed);
    }
  }
  bool get _anyChildInkResponsePressed => _activeChildren.isNotEmpty;

798
  void simulateTap([Intent? intent]) {
799
    _startNewSplash(context: context);
800
    handleTap();
801 802
  }

803
  void simulateLongPress() {
804
    _startNewSplash(context: context);
805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820
    handleLongPress();
  }

  void handleStatesControllerChange() {
    // Force a rebuild to resolve widget.overlayColor, widget.mouseCursor
    setState(() { });
  }

  MaterialStatesController get statesController => widget.statesController ?? internalStatesController!;

  void initStatesController() {
    if (widget.statesController == null) {
      internalStatesController = MaterialStatesController();
    }
    statesController.update(MaterialState.disabled, !enabled);
    statesController.addListener(handleStatesControllerChange);
821 822
  }

823 824 825
  @override
  void initState() {
    super.initState();
826 827
    initStatesController();
    FocusManager.instance.addHighlightModeListener(handleFocusHighlightModeChange);
828 829
  }

830
  @override
831
  void didUpdateWidget(_InkResponseStateWidget oldWidget) {
832
    super.didUpdateWidget(oldWidget);
833 834 835 836 837
    if (widget.statesController != oldWidget.statesController) {
      oldWidget.statesController?.removeListener(handleStatesControllerChange);
      if (widget.statesController != null) {
        internalStatesController?.dispose();
        internalStatesController = null;
838
      }
839
      initStatesController();
840
    }
841 842 843 844 845 846 847 848 849 850 851 852 853 854 855
    if (widget.customBorder != oldWidget.customBorder ||
        widget.radius != oldWidget.radius ||
        widget.borderRadius != oldWidget.borderRadius ||
        widget.highlightShape != oldWidget.highlightShape) {
      final InkHighlight? hoverHighLight = _highlights[_HighlightType.hover];
      if (hoverHighLight != null) {
        hoverHighLight.dispose();
        updateHighlight(_HighlightType.hover, value: _hovering, callOnHover: false);
      }
      final InkHighlight? focusHighLight = _highlights[_HighlightType.focus];
      if (focusHighLight != null) {
        focusHighLight.dispose();
        // Do not call updateFocusHighlights() here because it is called below
      }
    }
856 857 858 859 860 861 862 863 864 865
    if (enabled != isWidgetEnabled(oldWidget)) {
      statesController.update(MaterialState.disabled, !enabled);
      if (!enabled) {
        statesController.update(MaterialState.pressed, false);
      }
      // Don't call widget.onHover because many widgets, including the button
      // widgets, apply setState to an ancestor context from onHover.
      updateHighlight(_HighlightType.hover, value: _hovering, callOnHover: false);
    }
    updateFocusHighlights();
866 867
  }

868 869
  @override
  void dispose() {
870 871
    FocusManager.instance.removeHighlightModeListener(handleFocusHighlightModeChange);
    statesController.removeListener(handleStatesControllerChange);
872
    internalStatesController?.dispose();
873 874 875 876
    super.dispose();
  }

  @override
877
  bool get wantKeepAlive => highlightsExist || (_splashes != null && _splashes!.isNotEmpty);
878 879 880 881 882 883 884 885 886 887 888

  Duration getFadeDurationForType(_HighlightType type) {
    switch (type) {
      case _HighlightType.pressed:
        return const Duration(milliseconds: 200);
      case _HighlightType.hover:
      case _HighlightType.focus:
        return const Duration(milliseconds: 50);
    }
  }

889 890
  void updateHighlight(_HighlightType type, { required bool value, bool callOnHover = true }) {
    final InkHighlight? highlight = _highlights[type];
891 892 893
    void handleInkRemoval() {
      assert(_highlights[type] != null);
      _highlights[type] = null;
894
      updateKeepAlive();
895 896
    }

897 898 899 900 901 902 903 904 905 906 907 908 909 910
    switch (type) {
      case _HighlightType.pressed:
        statesController.update(MaterialState.pressed, value);
        break;
      case _HighlightType.hover:
        if (callOnHover) {
          statesController.update(MaterialState.hovered, value);
        }
        break;
      case _HighlightType.focus:
        // see handleFocusUpdate()
        break;
    }

911 912 913
    if (type == _HighlightType.pressed) {
      widget.parentState?.markChildInkResponsePressed(this, value);
    }
914
    if (value == (highlight != null && highlight.active)) {
915
      return;
916
    }
917

918
    if (value) {
919
      if (highlight == null) {
920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935
        Color? resolvedOverlayColor = widget.overlayColor?.resolve(statesController.value);
        if (resolvedOverlayColor == null) {
          // Use the backwards compatible defaults
          final ThemeData theme = Theme.of(context);
          switch (type) {
            case _HighlightType.pressed:
              resolvedOverlayColor = widget.highlightColor ?? theme.highlightColor;
              break;
            case _HighlightType.focus:
              resolvedOverlayColor = widget.focusColor ?? theme.focusColor;
              break;
            case _HighlightType.hover:
              resolvedOverlayColor = widget.hoverColor ?? theme.hoverColor;
              break;
          }
        }
936
        final RenderBox referenceBox = context.findRenderObject()! as RenderBox;
937
        _highlights[type] = InkHighlight(
938
          controller: Material.of(context),
939
          referenceBox: referenceBox,
940
          color: resolvedOverlayColor,
941
          shape: widget.highlightShape,
942
          radius: widget.radius,
943
          borderRadius: widget.borderRadius,
944
          customBorder: widget.customBorder,
945
          rectCallback: widget.getRectCallback!(referenceBox),
946
          onRemoved: handleInkRemoval,
947
          textDirection: Directionality.of(context),
948
          fadeDuration: getFadeDurationForType(type),
949
        );
950
        updateKeepAlive();
951
      } else {
952
        highlight.activate();
953 954
      }
    } else {
955
      highlight!.deactivate();
956
    }
957
    assert(value == (_highlights[type] != null && _highlights[type]!.active));
958

959
    switch (type) {
960
      case _HighlightType.pressed:
961
        widget.onHighlightChanged?.call(value);
962 963
        break;
      case _HighlightType.hover:
964
        if (callOnHover) {
965
          widget.onHover?.call(value);
966
        }
967 968 969
        break;
      case _HighlightType.focus:
        break;
970
    }
971 972
  }

973
  InteractiveInkFeature _createInkFeature(Offset globalPosition) {
974
    final MaterialInkController inkController = Material.of(context);
975
    final RenderBox referenceBox = context.findRenderObject()! as RenderBox;
976
    final Offset position = referenceBox.globalToLocal(globalPosition);
977
    final Color color =  widget.overlayColor?.resolve(statesController.value) ?? widget.splashColor ?? Theme.of(context).splashColor;
978 979 980
    final RectCallback? rectCallback = widget.containedInkWell ? widget.getRectCallback!(referenceBox) : null;
    final BorderRadius? borderRadius = widget.borderRadius;
    final ShapeBorder? customBorder = widget.customBorder;
981

982
    InteractiveInkFeature? splash;
983 984
    void onRemoved() {
      if (_splashes != null) {
985 986
        assert(_splashes!.contains(splash));
        _splashes!.remove(splash);
987
        if (_currentSplash == splash) {
988
          _currentSplash = null;
989
        }
990 991 992 993
        updateKeepAlive();
      } // else we're probably in deactivate()
    }

994
    splash = (widget.splashFactory ?? Theme.of(context).splashFactory).create(
995
      controller: inkController,
996
      referenceBox: referenceBox,
997 998
      position: position,
      color: color,
999
      containedInkWell: widget.containedInkWell,
1000
      rectCallback: rectCallback,
1001
      radius: widget.radius,
1002
      borderRadius: borderRadius,
1003
      customBorder: customBorder,
1004
      onRemoved: onRemoved,
1005
      textDirection: Directionality.of(context),
1006
    );
1007 1008 1009 1010

    return splash;
  }

1011
  void handleFocusHighlightModeChange(FocusHighlightMode mode) {
1012 1013 1014 1015
    if (!mounted) {
      return;
    }
    setState(() {
1016
      updateFocusHighlights();
1017 1018 1019
    });
  }

1020
  bool get _shouldShowFocus {
1021
    final NavigationMode mode = MediaQuery.maybeNavigationModeOf(context) ?? NavigationMode.traditional;
1022 1023 1024 1025 1026 1027 1028 1029
    switch (mode) {
      case NavigationMode.traditional:
        return enabled && _hasFocus;
      case NavigationMode.directional:
        return _hasFocus;
    }
  }

1030
  void updateFocusHighlights() {
1031
    final bool showFocus;
1032
    switch (FocusManager.instance.highlightMode) {
1033 1034 1035 1036
      case FocusHighlightMode.touch:
        showFocus = false;
        break;
      case FocusHighlightMode.traditional:
1037
        showFocus = _shouldShowFocus;
1038 1039
        break;
    }
1040
    updateHighlight(_HighlightType.focus, value: showFocus);
1041 1042
  }

1043
  bool _hasFocus = false;
1044
  void handleFocusUpdate(bool hasFocus) {
1045
    _hasFocus = hasFocus;
1046 1047 1048 1049 1050 1051
    // Set here rather than updateHighlight because this widget's
    // (MaterialState) states include MaterialState.focused if
    // the InkWell _has_ the focus, rather than if it's showing
    // the focus per FocusManager.instance.highlightMode.
    statesController.update(MaterialState.focused, hasFocus);
    updateFocusHighlights();
1052
    widget.onFocusChange?.call(hasFocus);
1053 1054
  }

1055
  void handleTapDown(TapDownDetails details) {
1056
    if (_anyChildInkResponsePressed) {
1057
      return;
1058
    }
1059
    _startNewSplash(details: details);
1060
    widget.onTapDown?.call(details);
1061 1062
  }

1063
  void handleTapUp(TapUpDetails details) {
1064 1065 1066
    widget.onTapUp?.call(details);
  }

1067
  void _startNewSplash({TapDownDetails? details, BuildContext? context}) {
1068 1069
    assert(details != null || context != null);

1070
    final Offset globalPosition;
1071
    if (context != null) {
1072
      final RenderBox referenceBox = context.findRenderObject()! as RenderBox;
1073 1074 1075
      assert(referenceBox.hasSize, 'InkResponse must be done with layout before starting a splash.');
      globalPosition = referenceBox.localToGlobal(referenceBox.paintBounds.center);
    } else {
1076
      globalPosition = details!.globalPosition;
1077
    }
1078
    statesController.update(MaterialState.pressed, true); // ... before creating the splash
1079 1080
    final InteractiveInkFeature splash = _createInkFeature(globalPosition);
    _splashes ??= HashSet<InteractiveInkFeature>();
1081
    _splashes!.add(splash);
1082
    _currentSplash?.cancel();
1083
    _currentSplash = splash;
1084
    updateKeepAlive();
1085
    updateHighlight(_HighlightType.pressed, value: true);
1086 1087
  }

1088
  void handleTap() {
1089 1090
    _currentSplash?.confirm();
    _currentSplash = null;
1091
    updateHighlight(_HighlightType.pressed, value: false);
1092
    if (widget.onTap != null) {
1093
      if (widget.enableFeedback) {
1094
        Feedback.forTap(context);
1095
      }
1096
      widget.onTap?.call();
1097
    }
1098 1099
  }

1100
  void handleTapCancel() {
1101 1102
    _currentSplash?.cancel();
    _currentSplash = null;
1103
    widget.onTapCancel?.call();
1104
    updateHighlight(_HighlightType.pressed, value: false);
1105 1106
  }

1107
  void handleDoubleTap() {
1108 1109
    _currentSplash?.confirm();
    _currentSplash = null;
1110
    updateHighlight(_HighlightType.pressed, value: false);
1111
    widget.onDoubleTap?.call();
1112 1113
  }

1114
  void handleLongPress() {
1115 1116
    _currentSplash?.confirm();
    _currentSplash = null;
1117
    if (widget.onLongPress != null) {
1118
      if (widget.enableFeedback) {
1119
        Feedback.forLongPress(context);
1120
      }
1121
      widget.onLongPress!();
1122
    }
1123 1124
  }

1125
  @override
1126
  void deactivate() {
1127 1128 1129
    if (_splashes != null) {
      final Set<InteractiveInkFeature> splashes = _splashes!;
      _splashes = null;
1130
      for (final InteractiveInkFeature splash in splashes) {
1131
        splash.dispose();
1132
      }
1133 1134 1135 1136 1137 1138 1139 1140
      _currentSplash = null;
    }
    assert(_currentSplash == null);
    for (final _HighlightType highlight in _highlights.keys) {
      _highlights[highlight]?.dispose();
      _highlights[highlight] = null;
    }
    widget.parentState?.markChildInkResponsePressed(this, false);
1141
    super.deactivate();
1142 1143
  }

1144
  bool isWidgetEnabled(_InkResponseStateWidget widget) {
1145
    return widget.onTap != null || widget.onDoubleTap != null || widget.onLongPress != null || widget.onTapDown != null;
1146 1147
  }

1148
  bool get enabled => isWidgetEnabled(widget);
1149

1150
  void handleMouseEnter(PointerEnterEvent event) {
1151 1152
    _hovering = true;
    if (enabled) {
1153
      handleHoverChange();
1154 1155
    }
  }
1156

1157
  void handleMouseExit(PointerExitEvent event) {
1158 1159 1160
    _hovering = false;
    // If the exit occurs after we've been disabled, we still
    // want to take down the highlights and run widget.onHover.
1161
    handleHoverChange();
1162 1163
  }

1164
  void handleHoverChange() {
1165 1166 1167
    updateHighlight(_HighlightType.hover, value: _hovering);
  }

1168
  bool get _canRequestFocus {
1169
    final NavigationMode mode = MediaQuery.maybeNavigationModeOf(context) ?? NavigationMode.traditional;
1170 1171 1172 1173 1174 1175 1176 1177
    switch (mode) {
      case NavigationMode.traditional:
        return enabled && widget.canRequestFocus;
      case NavigationMode.directional:
        return true;
    }
  }

1178
  @override
1179
  Widget build(BuildContext context) {
1180
    assert(widget.debugCheckContext(context));
1181
    super.build(context); // See AutomaticKeepAliveClientMixin.
1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200

    Color getHighlightColorForType(_HighlightType type) {
      const Set<MaterialState> pressed = <MaterialState>{MaterialState.pressed};
      const Set<MaterialState> focused = <MaterialState>{MaterialState.focused};
      const Set<MaterialState> hovered = <MaterialState>{MaterialState.hovered};

      final ThemeData theme = Theme.of(context);
      switch (type) {
        // The pressed state triggers a ripple (ink splash), per the current
        // Material Design spec. A separate highlight is no longer used.
        // See https://material.io/design/interaction/states.html#pressed
        case _HighlightType.pressed:
          return widget.overlayColor?.resolve(pressed) ?? widget.highlightColor ?? theme.highlightColor;
        case _HighlightType.focus:
          return widget.overlayColor?.resolve(focused) ?? widget.focusColor ?? theme.focusColor;
        case _HighlightType.hover:
          return widget.overlayColor?.resolve(hovered) ?? widget.hoverColor ?? theme.hoverColor;
      }
    }
1201
    for (final _HighlightType type in _highlights.keys) {
1202 1203
      _highlights[type]?.color = getHighlightColorForType(type);
    }
1204

1205
    _currentSplash?.color = widget.overlayColor?.resolve(statesController.value) ?? widget.splashColor ?? Theme.of(context).splashColor;
1206

1207 1208
    final MouseCursor effectiveMouseCursor = MaterialStateProperty.resolveAs<MouseCursor>(
      widget.mouseCursor ?? MaterialStateMouseCursor.clickable,
1209
      statesController.value,
1210
    );
1211

1212 1213 1214 1215 1216 1217
    return _ParentInkResponseProvider(
      state: this,
      child: Actions(
        actions: _actionMap,
        child: Focus(
          focusNode: widget.focusNode,
1218
          canRequestFocus: _canRequestFocus,
1219
          onFocusChange: handleFocusUpdate,
1220 1221
          autofocus: widget.autofocus,
          child: MouseRegion(
1222
            cursor: effectiveMouseCursor,
1223 1224
            onEnter: handleMouseEnter,
            onExit: handleMouseExit,
1225
            child: Semantics(
1226 1227
              onTap: widget.excludeFromSemantics || widget.onTap == null ? null : simulateTap,
              onLongPress: widget.excludeFromSemantics || widget.onLongPress == null ? null : simulateLongPress,
1228
              child: GestureDetector(
1229 1230 1231 1232 1233 1234
                onTapDown: enabled ? handleTapDown : null,
                onTapUp: enabled ? handleTapUp : null,
                onTap: enabled ? handleTap : null,
                onTapCancel: enabled ? handleTapCancel : null,
                onDoubleTap: widget.onDoubleTap != null ? handleDoubleTap : null,
                onLongPress: widget.onLongPress != null ? handleLongPress : null,
1235 1236 1237 1238
                behavior: HitTestBehavior.opaque,
                excludeFromSemantics: true,
                child: widget.child,
              ),
1239
            ),
1240 1241
          ),
        ),
1242
      ),
1243
    );
1244
  }
1245
}
1246

1247
/// A rectangular area of a [Material] that responds to touch.
1248
///
1249 1250 1251 1252 1253
/// For a variant of this widget that does not clip splashes, see [InkResponse].
///
/// The following diagram shows how an [InkWell] looks when tapped, when using
/// default values.
///
1254
/// ![The highlight is a rectangle the size of the box.](https://flutter.github.io/assets-for-api-docs/assets/material/ink_well.png)
1255
///
1256
/// The [InkWell] widget must have a [Material] widget as an ancestor. The
1257
/// [Material] widget is where the ink reactions are actually painted. This
1258
/// matches the Material Design premise wherein the [Material] is what is
1259 1260
/// actually reacting to touches by spreading ink.
///
1261
/// If a Widget uses this class directly, it should include the following line
1262
/// at the top of its build function to call [debugCheckHasMaterial]:
1263
///
1264 1265 1266 1267
/// ```dart
/// assert(debugCheckHasMaterial(context));
/// ```
///
Ian Hickson's avatar
Ian Hickson committed
1268 1269 1270 1271 1272 1273
/// ## Troubleshooting
///
/// ### The ink splashes aren't visible!
///
/// If there is an opaque graphic, e.g. painted using a [Container], [Image], or
/// [DecoratedBox], between the [Material] widget and the [InkWell] widget, then
1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286
/// the splash won't be visible because it will be under the opaque graphic.
/// This is because ink splashes draw on the underlying [Material] itself, as
/// if the ink was spreading inside the material.
///
/// The [Ink] widget can be used as a replacement for [Image], [Container], or
/// [DecoratedBox] to ensure that the image or decoration also paints in the
/// [Material] itself, below the ink.
///
/// If this is not possible for some reason, e.g. because you are using an
/// opaque [CustomPaint] widget, alternatively consider using a second
/// [Material] above the opaque widget but below the [InkWell] (as an
/// ancestor to the ink well). The [MaterialType.transparency] material
/// kind can be used for this purpose.
Ian Hickson's avatar
Ian Hickson committed
1287
///
1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299
/// ### InkWell isn't clipping properly
///
/// If you want to clip an InkWell or any [Ink] widgets you need to keep in mind
/// that the [Material] that the Ink will be printed on is responsible for clipping.
/// This means you can't wrap the [Ink] widget in a clipping widget directly,
/// since this will leave the [Material] not clipped (and by extension the printed
/// [Ink] widgets as well).
///
/// An easy solution is to deliberately wrap the [Ink] widgets you want to clip
/// in a [Material], and wrap that in a clipping widget instead. See [Ink] for
/// an example.
///
1300 1301 1302 1303 1304 1305 1306
/// ### The ink splashes don't track the size of an animated container
/// If the size of an InkWell's [Material] ancestor changes while the InkWell's
/// splashes are expanding, you may notice that the splashes aren't clipped
/// correctly. This can't be avoided.
///
/// An example of this situation is as follows:
///
1307
/// {@tool dartpad}
1308 1309 1310
/// Tap the container to cause it to grow. Then, tap it again and hold before
/// the widget reaches its maximum size to observe the clipped ink splash.
///
1311
/// ** See code in examples/api/lib/material/ink_well/ink_well.0.dart **
1312 1313 1314 1315 1316 1317 1318
/// {@end-tool}
///
/// An InkWell's splashes will not properly update to conform to changes if the
/// size of its underlying [Material], where the splashes are rendered, changes
/// during animation. You should avoid using InkWells within [Material] widgets
/// that are changing size.
///
1319 1320 1321
/// See also:
///
///  * [GestureDetector], for listening for gestures without ink splashes.
1322
///  * [ElevatedButton] and [TextButton], two kinds of buttons in Material Design.
1323 1324
///  * [InkResponse], a variant of [InkWell] that doesn't force a rectangular
///    shape on the ink reaction.
1325
class InkWell extends InkResponse {
1326 1327 1328
  /// Creates an ink well.
  ///
  /// Must have an ancestor [Material] widget in which to cause ink reactions.
Ian Hickson's avatar
Ian Hickson committed
1329
  ///
1330
  /// The [enableFeedback], and [excludeFromSemantics] arguments
1331
  /// must not be null.
1332
  const InkWell({
1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352
    super.key,
    super.child,
    super.onTap,
    super.onDoubleTap,
    super.onLongPress,
    super.onTapDown,
    super.onTapUp,
    super.onTapCancel,
    super.onHighlightChanged,
    super.onHover,
    super.mouseCursor,
    super.focusColor,
    super.hoverColor,
    super.highlightColor,
    super.overlayColor,
    super.splashColor,
    super.splashFactory,
    super.radius,
    super.borderRadius,
    super.customBorder,
1353
    bool? enableFeedback = true,
1354 1355 1356 1357 1358
    super.excludeFromSemantics,
    super.focusNode,
    super.canRequestFocus,
    super.onFocusChange,
    super.autofocus,
1359
    super.statesController,
1360
  }) : super(
1361
    containedInkWell: true,
1362
    highlightShape: BoxShape.rectangle,
1363
    enableFeedback: enableFeedback ?? true,
1364
  );
1365
}