ink_well.dart 46 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 22 23 24 25 26 27
/// 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
28
/// that's canceled when the pointer is dragged out of the reference
29 30 31 32 33 34 35 36 37
/// 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({
38 39 40 41
    required MaterialInkController controller,
    required RenderBox referenceBox,
    required Color color,
    VoidCallback? onRemoved,
42 43 44 45 46 47 48 49 50
  }) : assert(controller != null),
       assert(referenceBox != null),
       _color = color,
       super(controller: controller, referenceBox: referenceBox, onRemoved: onRemoved);

  /// 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.
51
  void confirm() { }
52 53 54 55 56

  /// 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.
57
  void cancel() { }
58 59 60 61 62 63 64 65 66 67

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

  /// 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.
  ///
86
  /// Clipping can happen in 3 different ways:
87 88 89 90 91 92 93 94 95 96 97 98 99 100
  ///  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({
101 102 103 104 105 106 107
    required Canvas canvas,
    required Matrix4 transform,
    required Paint paint,
    required Offset center,
    required double radius,
    TextDirection? textDirection,
    ShapeBorder? customBorder,
108
    BorderRadius borderRadius = BorderRadius.zero,
109
    RectCallback? clipCallback,
110
  }) {
111 112 113 114 115 116 117
    assert(canvas != null);
    assert(transform != null);
    assert(paint != null);
    assert(center != null);
    assert(radius != null);
    assert(borderRadius != null);

118
    final Offset? originOffset = MatrixUtils.getAsTranslation(transform);
119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141
    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();
  }
142 143
}

144 145
/// An encapsulation of an [InteractiveInkFeature] constructor used by
/// [InkWell], [InkResponse], and [ThemeData].
146 147 148 149 150 151 152 153 154 155
///
/// 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 {
156 157 158
  /// Abstract const constructor. This constructor enables subclasses to provide
  /// const constructors so that they can be used in const expressions.
  ///
159 160 161 162 163 164 165
  /// Subclasses should provide a const constructor.
  const InteractiveInkFeatureFactory();

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

182 183 184 185 186 187
abstract class _ParentInkResponseState {
  void markChildInkResponsePressed(_ParentInkResponseState childState, bool value);
}

class _ParentInkResponseProvider extends InheritedWidget {
  const _ParentInkResponseProvider({
188 189
    required this.state,
    required Widget child,
190 191 192 193 194 195 196
  }) : super(child: child);

  final _ParentInkResponseState state;

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

197
  static _ParentInkResponseState? of(BuildContext context) {
198 199 200 201
    return context.dependOnInheritedWidgetOfExactType<_ParentInkResponseProvider>()?.state;
  }
}

202
typedef _GetRectCallback = RectCallback? Function(RenderBox referenceBox);
203 204
typedef _CheckContext = bool Function(BuildContext context);

205
/// An area of a [Material] that responds to touch. Has a configurable shape and
206 207
/// can be configured to clip splashes that extend outside its bounds or not.
///
208
/// For a variant of this widget that is specialized for rectangular areas that
209 210
/// always clip splashes, see [InkWell].
///
211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226
/// 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.
///
227 228
/// The following two diagrams show how [InkResponse] looks when tapped if the
/// [highlightShape] is [BoxShape.circle] (the default) and [containedInkWell]
229 230 231 232
/// is false (also the default).
///
/// The first diagram shows how it looks if the [InkResponse] is relatively
/// large:
233
///
234
/// ![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)
235 236 237
///
/// The second diagram shows how it looks if the [InkResponse] is small:
///
238
/// ![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)
239
///
240 241 242
/// The main thing to notice from these diagrams is that the splashes happily
/// exceed the bounds of the widget (because [containedInkWell] is false).
///
243 244 245 246
/// 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].
///
247
/// ![The highlight is a rectangle the size of the box.](https://flutter.github.io/assets-for-api-docs/assets/material/ink_well.png)
248
///
249
/// The [InkResponse] widget must have a [Material] widget as an ancestor. The
250 251 252
/// [Material] widget is where the ink reactions are actually painted. This
/// matches the material design premise wherein the [Material] is what is
/// actually reacting to touches by spreading ink.
253 254
///
/// If a Widget uses this class directly, it should include the following line
255
/// at the top of its build function to call [debugCheckHasMaterial]:
256
///
257 258 259
/// ```dart
/// assert(debugCheckHasMaterial(context));
/// ```
Ian Hickson's avatar
Ian Hickson committed
260 261 262 263 264 265 266
///
/// ## 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,
267 268 269 270 271 272 273 274 275 276 277 278 279
/// 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.
280 281 282 283
///
/// See also:
///
///  * [GestureDetector], for listening for gestures without ink splashes.
284
///  * [ElevatedButton] and [TextButton], two kinds of buttons in material design.
285
///  * [IconButton], which combines [InkResponse] with an [Icon].
286
class InkResponse extends StatelessWidget {
287 288 289
  /// 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
290
  ///
291
  /// The [containedInkWell], [highlightShape], [enableFeedback],
292
  /// and [excludeFromSemantics] arguments must not be null.
293
  const InkResponse({
294
    Key? key,
295 296
    this.child,
    this.onTap,
297
    this.onTapDown,
298
    this.onTapUp,
299
    this.onTapCancel,
300
    this.onDoubleTap,
301
    this.onLongPress,
302
    this.onHighlightChanged,
303
    this.onHover,
304
    this.mouseCursor,
305 306
    this.containedInkWell = false,
    this.highlightShape = BoxShape.circle,
307
    this.radius,
Ian Hickson's avatar
Ian Hickson committed
308
    this.borderRadius,
309
    this.customBorder,
310 311
    this.focusColor,
    this.hoverColor,
312
    this.highlightColor,
313
    this.overlayColor,
314
    this.splashColor,
315
    this.splashFactory,
316 317
    this.enableFeedback = true,
    this.excludeFromSemantics = false,
318 319 320 321
    this.focusNode,
    this.canRequestFocus = true,
    this.onFocusChange,
    this.autofocus = false,
322
  }) : assert(containedInkWell != null),
Ian Hickson's avatar
Ian Hickson committed
323 324 325
       assert(highlightShape != null),
       assert(enableFeedback != null),
       assert(excludeFromSemantics != null),
326 327
       assert(autofocus != null),
       assert(canRequestFocus != null),
Ian Hickson's avatar
Ian Hickson committed
328
       super(key: key);
329

330
  /// The widget below this widget in the tree.
331
  ///
332
  /// {@macro flutter.widgets.ProxyWidget.child}
333
  final Widget? child;
334

335
  /// Called when the user taps this part of the material.
336
  final GestureTapCallback? onTap;
337

338
  /// Called when the user taps down this part of the material.
339
  final GestureTapDownCallback? onTapDown;
340

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

345 346
  /// Called when the user cancels a tap that was started on this part of the
  /// material.
347
  final GestureTapCallback? onTapCancel;
348

349
  /// Called when the user double taps this part of the material.
350
  final GestureTapCallback? onDoubleTap;
351

352
  /// Called when the user long-presses on this part of the material.
353
  final GestureLongPressCallback? onLongPress;
354

355 356
  /// Called when this part of the material either becomes highlighted or stops
  /// being highlighted.
357 358 359 360
  ///
  /// 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.
361 362 363 364 365 366
  ///
  /// 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.
367
  final ValueChanged<bool>? onHighlightChanged;
368

369 370 371 372 373
  /// 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.
374
  final ValueChanged<bool>? onHover;
375

376
  /// The cursor for a mouse pointer when it enters or is hovering over the
377
  /// widget.
378
  ///
379 380 381 382 383 384 385 386
  /// 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.
387
  final MouseCursor? mouseCursor;
388

389
  /// Whether this ink response should be clipped its bounds.
390 391 392 393 394 395 396 397
  ///
  /// 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:
  ///
398 399
  ///  * [highlightShape], the shape of the focus, hover, and pressed
  ///    highlights.
400 401 402
  ///  * [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.
403
  final bool containedInkWell;
404

405
  /// The shape (e.g., circle, rectangle) to use for the highlight drawn around
406 407 408 409 410
  /// 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]).
411 412 413 414 415 416 417 418 419 420 421 422 423
  ///
  /// 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.
424
  final BoxShape highlightShape;
425

426
  /// The radius of the ink splash.
427 428 429 430 431 432 433 434
  ///
  /// 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.
435
  ///  * [splashFactory], which defines the appearance of the splash.
436
  final double? radius;
437

438 439
  /// The clipping radius of the containing rect. This is effective only if
  /// [customBorder] is null.
Ian Hickson's avatar
Ian Hickson committed
440 441
  ///
  /// If this is null, it is interpreted as [BorderRadius.zero].
442
  final BorderRadius? borderRadius;
443

444
  /// The custom clip border which overrides [borderRadius].
445
  final ShapeBorder? customBorder;
446

447 448 449 450 451 452 453 454 455 456 457
  /// 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.
458
  final Color? focusColor;
459 460 461 462 463 464 465 466 467 468 469 470 471

  /// 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.
472
  final Color? hoverColor;
473 474 475 476

  /// 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.
477 478 479
  ///
  /// See also:
  ///
480 481 482 483
  ///  * [hoverColor], the color of the hover highlight.
  ///  * [focusColor], the color of the focus highlight.
  ///  * [highlightShape], the shape of the focus, hover, and pressed
  ///    highlights.
484
  ///  * [splashColor], the color of the splash.
485
  ///  * [splashFactory], which defines the appearance of the splash.
486
  final Color? highlightColor;
487

488 489 490
  /// Defines the ink response focus, hover, and splash colors.
  ///
  /// This default null property can be used as an alternative to
491 492 493 494 495 496
  /// [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.
497 498 499 500 501 502 503 504 505 506 507 508 509
  ///
  /// [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
  /// used by the current design guidelines.  See
  /// 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
510
  ///    match a component's state:
511
  ///    <https://material.io/design/interaction/states.html#anatomy>.
512
  final MaterialStateProperty<Color?>? overlayColor;
513

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

524 525 526 527 528 529 530 531 532 533 534
  /// 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
535
  ///    more aggressively than the default.
536
  final InteractiveInkFeatureFactory? splashFactory;
537

538 539 540 541 542 543 544 545 546 547
  /// 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;

548 549 550 551 552 553 554 555 556
  /// 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;

557 558 559 560
  /// Handler called when the focus changes.
  ///
  /// Called with true if this widget's node gains focus, and false if it loses
  /// focus.
561
  final ValueChanged<bool>? onFocusChange;
562 563 564 565 566

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

  /// {@macro flutter.widgets.Focus.focusNode}
567
  final FocusNode? focusNode;
568

569
  /// {@macro flutter.widgets.Focus.canRequestFocus}
570 571
  final bool canRequestFocus;

572 573 574 575 576
  /// 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,
577
  /// [TableRowInkWell] implements this method to return the rectangle
578 579 580 581 582
  /// 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).
583
  RectCallback? getRectCallback(RenderBox referenceBox) => null;
584

585 586
  @override
  Widget build(BuildContext context) {
587
    final _ParentInkResponseState? parentState = _ParentInkResponseProvider.of(context);
588
    return _InkResponseStateWidget(
589 590
      onTap: onTap,
      onTapDown: onTapDown,
591
      onTapUp: onTapUp,
592 593 594 595 596
      onTapCancel: onTapCancel,
      onDoubleTap: onDoubleTap,
      onLongPress: onLongPress,
      onHighlightChanged: onHighlightChanged,
      onHover: onHover,
597
      mouseCursor: mouseCursor,
598 599 600 601 602 603 604 605
      containedInkWell: containedInkWell,
      highlightShape: highlightShape,
      radius: radius,
      borderRadius: borderRadius,
      customBorder: customBorder,
      focusColor: focusColor,
      hoverColor: hoverColor,
      highlightColor: highlightColor,
606
      overlayColor: overlayColor,
607 608 609 610 611 612 613 614 615 616 617
      splashColor: splashColor,
      splashFactory: splashFactory,
      enableFeedback: enableFeedback,
      excludeFromSemantics: excludeFromSemantics,
      focusNode: focusNode,
      canRequestFocus: canRequestFocus,
      onFocusChange: onFocusChange,
      autofocus: autofocus,
      parentState: parentState,
      getRectCallback: getRectCallback,
      debugCheckContext: debugCheckContext,
618
      child: child,
619 620 621
    );
  }

622 623 624 625 626
  /// 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,
627
  /// [TableRowInkWell] implements this method to verify that the widget is
628
  /// in a table.
629
  @mustCallSuper
630 631
  bool debugCheckContext(BuildContext context) {
    assert(debugCheckHasMaterial(context));
632
    assert(debugCheckHasDirectionality(context));
633 634
    return true;
  }
635 636
}

637 638
class _InkResponseStateWidget extends StatefulWidget {
  const _InkResponseStateWidget({
639 640 641
    this.child,
    this.onTap,
    this.onTapDown,
642
    this.onTapUp,
643 644 645 646 647
    this.onTapCancel,
    this.onDoubleTap,
    this.onLongPress,
    this.onHighlightChanged,
    this.onHover,
648
    this.mouseCursor,
649 650 651 652 653 654 655 656
    this.containedInkWell = false,
    this.highlightShape = BoxShape.circle,
    this.radius,
    this.borderRadius,
    this.customBorder,
    this.focusColor,
    this.hoverColor,
    this.highlightColor,
657
    this.overlayColor,
658 659 660 661 662 663 664 665 666 667
    this.splashColor,
    this.splashFactory,
    this.enableFeedback = true,
    this.excludeFromSemantics = false,
    this.focusNode,
    this.canRequestFocus = true,
    this.onFocusChange,
    this.autofocus = false,
    this.parentState,
    this.getRectCallback,
668
    required this.debugCheckContext,
669 670 671 672 673
  }) : assert(containedInkWell != null),
       assert(highlightShape != null),
       assert(enableFeedback != null),
       assert(excludeFromSemantics != null),
       assert(autofocus != null),
674
       assert(canRequestFocus != null);
675

676 677 678
  final Widget? child;
  final GestureTapCallback? onTap;
  final GestureTapDownCallback? onTapDown;
679
  final GestureTapUpCallback? onTapUp;
680 681 682 683 684 685
  final GestureTapCallback? onTapCancel;
  final GestureTapCallback? onDoubleTap;
  final GestureLongPressCallback? onLongPress;
  final ValueChanged<bool>? onHighlightChanged;
  final ValueChanged<bool>? onHover;
  final MouseCursor? mouseCursor;
686 687
  final bool containedInkWell;
  final BoxShape highlightShape;
688 689 690 691 692 693 694 695 696
  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;
697 698
  final bool enableFeedback;
  final bool excludeFromSemantics;
699
  final ValueChanged<bool>? onFocusChange;
700
  final bool autofocus;
701
  final FocusNode? focusNode;
702
  final bool canRequestFocus;
703 704
  final _ParentInkResponseState? parentState;
  final _GetRectCallback? getRectCallback;
705
  final _CheckContext debugCheckContext;
706

707
  @override
708
  _InkResponseState createState() => _InkResponseState();
Ian Hickson's avatar
Ian Hickson committed
709 710

  @override
711 712
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
713 714 715 716 717
    final List<String> gestures = <String>[
      if (onTap != null) 'tap',
      if (onDoubleTap != null) 'double tap',
      if (onLongPress != null) 'long press',
      if (onTapDown != null) 'tap down',
718
      if (onTapUp != null) 'tap up',
719 720
      if (onTapCancel != null) 'tap cancel',
    ];
721
    properties.add(IterableProperty<String>('gestures', gestures, ifEmpty: '<none>'));
722
    properties.add(DiagnosticsProperty<MouseCursor>('mouseCursor', mouseCursor));
723 724
    properties.add(DiagnosticsProperty<bool>('containedInkWell', containedInkWell, level: DiagnosticLevel.fine));
    properties.add(DiagnosticsProperty<BoxShape>(
725 726 727 728 729
      'highlightShape',
      highlightShape,
      description: '${containedInkWell ? "clipped to " : ""}$highlightShape',
      showName: false,
    ));
Ian Hickson's avatar
Ian Hickson committed
730
  }
731 732
}

733 734 735 736 737 738 739 740
/// Used to index the allocated highlights for the different types of highlights
/// in [_InkResponseState].
enum _HighlightType {
  pressed,
  hover,
  focus,
}

741 742
class _InkResponseState extends State<_InkResponseStateWidget>
    with AutomaticKeepAliveClientMixin<_InkResponseStateWidget>
743
    implements _ParentInkResponseState {
744 745
  Set<InteractiveInkFeature>? _splashes;
  InteractiveInkFeature? _currentSplash;
746
  bool _hovering = false;
747
  final Map<_HighlightType, InkHighlight?> _highlights = <_HighlightType, InkHighlight?>{};
748 749
  late final Map<Type, Action<Intent>> _actionMap = <Type, Action<Intent>>{
    ActivateIntent: CallbackAction<ActivateIntent>(onInvoke: _simulateTap),
750
    ButtonActivateIntent: CallbackAction<ButtonActivateIntent>(onInvoke: _simulateTap),
751
  };
752

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

755
  final ObserverList<_ParentInkResponseState> _activeChildren = ObserverList<_ParentInkResponseState>();
756

757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772
  @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;

773
  void _simulateTap([Intent? intent]) {
774 775 776 777 778
    _startSplash(context: context);
    _handleTap();
  }

  void _simulateLongPress() {
779
    _startSplash(context: context);
780
    _handleLongPress();
781 782
  }

783 784 785
  @override
  void initState() {
    super.initState();
786
    FocusManager.instance.addHighlightModeListener(_handleFocusHighlightModeChange);
787 788
  }

789
  @override
790
  void didUpdateWidget(_InkResponseStateWidget oldWidget) {
791
    super.didUpdateWidget(oldWidget);
792
    if (_isWidgetEnabled(widget) != _isWidgetEnabled(oldWidget)) {
793
      if (enabled) {
794
        // Don't call widget.onHover because many widgets, including the button
795 796 797
        // widgets, apply setState to an ancestor context from onHover.
        updateHighlight(_HighlightType.hover, value: _hovering, callOnHover: false);
      }
798
      _updateFocusHighlights();
799 800 801
    }
  }

802 803
  @override
  void dispose() {
804
    FocusManager.instance.removeHighlightModeListener(_handleFocusHighlightModeChange);
805 806 807 808
    super.dispose();
  }

  @override
809
  bool get wantKeepAlive => highlightsExist || (_splashes != null && _splashes!.isNotEmpty);
810 811

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

816
    final ThemeData theme = Theme.of(context);
817
    switch (type) {
818 819 820
      // 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
821
      case _HighlightType.pressed:
822
        return widget.overlayColor?.resolve(pressed) ?? widget.highlightColor ?? theme.highlightColor;
823
      case _HighlightType.focus:
824
        return widget.overlayColor?.resolve(focused) ?? widget.focusColor ?? theme.focusColor;
825
      case _HighlightType.hover:
826
        return widget.overlayColor?.resolve(hovered) ?? widget.hoverColor ?? theme.hoverColor;
827 828 829 830 831 832 833 834 835 836 837 838 839
    }
  }

  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);
    }
  }

840 841
  void updateHighlight(_HighlightType type, { required bool value, bool callOnHover = true }) {
    final InkHighlight? highlight = _highlights[type];
842 843 844
    void handleInkRemoval() {
      assert(_highlights[type] != null);
      _highlights[type] = null;
845
      updateKeepAlive();
846 847
    }

848 849 850
    if (type == _HighlightType.pressed) {
      widget.parentState?.markChildInkResponsePressed(this, value);
    }
851
    if (value == (highlight != null && highlight.active))
852 853
      return;
    if (value) {
854
      if (highlight == null) {
855
        final RenderBox referenceBox = context.findRenderObject()! as RenderBox;
856
        _highlights[type] = InkHighlight(
857
          controller: Material.of(context)!,
858
          referenceBox: referenceBox,
859
          color: getHighlightColorForType(type),
860
          shape: widget.highlightShape,
861
          radius: widget.radius,
862
          borderRadius: widget.borderRadius,
863
          customBorder: widget.customBorder,
864
          rectCallback: widget.getRectCallback!(referenceBox),
865
          onRemoved: handleInkRemoval,
866
          textDirection: Directionality.of(context),
867
          fadeDuration: getFadeDurationForType(type),
868
        );
869
        updateKeepAlive();
870
      } else {
871
        highlight.activate();
872 873
      }
    } else {
874
      highlight!.deactivate();
875
    }
876
    assert(value == (_highlights[type] != null && _highlights[type]!.active));
877

878
    switch (type) {
879
      case _HighlightType.pressed:
880
        widget.onHighlightChanged?.call(value);
881 882
        break;
      case _HighlightType.hover:
883 884
        if (callOnHover)
          widget.onHover?.call(value);
885 886 887
        break;
      case _HighlightType.focus:
        break;
888
    }
889 890
  }

891
  InteractiveInkFeature _createInkFeature(Offset globalPosition) {
892 893
    final MaterialInkController inkController = Material.of(context)!;
    final RenderBox referenceBox = context.findRenderObject()! as RenderBox;
894
    final Offset position = referenceBox.globalToLocal(globalPosition);
895
    const Set<MaterialState> pressed = <MaterialState>{MaterialState.pressed};
896
    final Color color =  widget.overlayColor?.resolve(pressed) ?? widget.splashColor ?? Theme.of(context).splashColor;
897 898 899
    final RectCallback? rectCallback = widget.containedInkWell ? widget.getRectCallback!(referenceBox) : null;
    final BorderRadius? borderRadius = widget.borderRadius;
    final ShapeBorder? customBorder = widget.customBorder;
900

901
    InteractiveInkFeature? splash;
902 903
    void onRemoved() {
      if (_splashes != null) {
904 905
        assert(_splashes!.contains(splash));
        _splashes!.remove(splash);
906 907 908 909 910 911
        if (_currentSplash == splash)
          _currentSplash = null;
        updateKeepAlive();
      } // else we're probably in deactivate()
    }

912
    splash = (widget.splashFactory ?? Theme.of(context).splashFactory).create(
913
      controller: inkController,
914
      referenceBox: referenceBox,
915 916
      position: position,
      color: color,
917
      containedInkWell: widget.containedInkWell,
918
      rectCallback: rectCallback,
919
      radius: widget.radius,
920
      borderRadius: borderRadius,
921
      customBorder: customBorder,
922
      onRemoved: onRemoved,
923
      textDirection: Directionality.of(context),
924
    );
925 926 927 928

    return splash;
  }

929 930 931 932 933
  void _handleFocusHighlightModeChange(FocusHighlightMode mode) {
    if (!mounted) {
      return;
    }
    setState(() {
934
      _updateFocusHighlights();
935 936 937
    });
  }

938
  bool get _shouldShowFocus {
939
    final NavigationMode mode = MediaQuery.maybeOf(context)?.navigationMode ?? NavigationMode.traditional;
940 941 942 943 944 945 946 947
    switch (mode) {
      case NavigationMode.traditional:
        return enabled && _hasFocus;
      case NavigationMode.directional:
        return _hasFocus;
    }
  }

948
  void _updateFocusHighlights() {
949
    final bool showFocus;
950
    switch (FocusManager.instance.highlightMode) {
951 952 953 954
      case FocusHighlightMode.touch:
        showFocus = false;
        break;
      case FocusHighlightMode.traditional:
955
        showFocus = _shouldShowFocus;
956 957
        break;
    }
958
    updateHighlight(_HighlightType.focus, value: showFocus);
959 960
  }

961 962 963 964
  bool _hasFocus = false;
  void _handleFocusUpdate(bool hasFocus) {
    _hasFocus = hasFocus;
    _updateFocusHighlights();
965
    widget.onFocusChange?.call(hasFocus);
966 967
  }

968
  void _handleTapDown(TapDownDetails details) {
969 970
    if (_anyChildInkResponsePressed)
      return;
971
    _startSplash(details: details);
972
    widget.onTapDown?.call(details);
973 974
  }

975 976 977 978
  void _handleTapUp(TapUpDetails details) {
    widget.onTapUp?.call(details);
  }

979
  void _startSplash({TapDownDetails? details, BuildContext? context}) {
980 981
    assert(details != null || context != null);

982
    final Offset globalPosition;
983
    if (context != null) {
984
      final RenderBox referenceBox = context.findRenderObject()! as RenderBox;
985 986 987
      assert(referenceBox.hasSize, 'InkResponse must be done with layout before starting a splash.');
      globalPosition = referenceBox.localToGlobal(referenceBox.paintBounds.center);
    } else {
988
      globalPosition = details!.globalPosition;
989 990 991
    }
    final InteractiveInkFeature splash = _createInkFeature(globalPosition);
    _splashes ??= HashSet<InteractiveInkFeature>();
992
    _splashes!.add(splash);
993
    _currentSplash = splash;
994
    updateKeepAlive();
995
    updateHighlight(_HighlightType.pressed, value: true);
996 997
  }

998
  void _handleTap() {
999 1000
    _currentSplash?.confirm();
    _currentSplash = null;
1001
    updateHighlight(_HighlightType.pressed, value: false);
1002 1003 1004
    if (widget.onTap != null) {
      if (widget.enableFeedback)
        Feedback.forTap(context);
1005
      widget.onTap?.call();
1006
    }
1007 1008
  }

1009 1010 1011
  void _handleTapCancel() {
    _currentSplash?.cancel();
    _currentSplash = null;
1012
    widget.onTapCancel?.call();
1013
    updateHighlight(_HighlightType.pressed, value: false);
1014 1015
  }

1016 1017 1018
  void _handleDoubleTap() {
    _currentSplash?.confirm();
    _currentSplash = null;
1019
    widget.onDoubleTap?.call();
1020 1021
  }

1022
  void _handleLongPress() {
1023 1024
    _currentSplash?.confirm();
    _currentSplash = null;
1025 1026 1027
    if (widget.onLongPress != null) {
      if (widget.enableFeedback)
        Feedback.forLongPress(context);
1028
      widget.onLongPress!();
1029
    }
1030 1031
  }

1032
  @override
1033
  void deactivate() {
1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046
    if (_splashes != null) {
      final Set<InteractiveInkFeature> splashes = _splashes!;
      _splashes = null;
      for (final InteractiveInkFeature splash in splashes)
        splash.dispose();
      _currentSplash = null;
    }
    assert(_currentSplash == null);
    for (final _HighlightType highlight in _highlights.keys) {
      _highlights[highlight]?.dispose();
      _highlights[highlight] = null;
    }
    widget.parentState?.markChildInkResponsePressed(this, false);
1047
    super.deactivate();
1048 1049
  }

1050
  bool _isWidgetEnabled(_InkResponseStateWidget widget) {
1051
    return widget.onTap != null || widget.onDoubleTap != null || widget.onLongPress != null || widget.onTapDown != null;
1052 1053 1054
  }

  bool get enabled => _isWidgetEnabled(widget);
1055

1056 1057 1058 1059
  void _handleMouseEnter(PointerEnterEvent event) {
    _hovering = true;
    if (enabled) {
      _handleHoverChange();
1060 1061
    }
  }
1062

1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073
  void _handleMouseExit(PointerExitEvent event) {
    _hovering = false;
    // If the exit occurs after we've been disabled, we still
    // want to take down the highlights and run widget.onHover.
    _handleHoverChange();
  }

  void _handleHoverChange() {
    updateHighlight(_HighlightType.hover, value: _hovering);
  }

1074
  bool get _canRequestFocus {
1075
    final NavigationMode mode = MediaQuery.maybeOf(context)?.navigationMode ?? NavigationMode.traditional;
1076 1077 1078 1079 1080 1081 1082 1083
    switch (mode) {
      case NavigationMode.traditional:
        return enabled && widget.canRequestFocus;
      case NavigationMode.directional:
        return true;
    }
  }

1084
  @override
1085
  Widget build(BuildContext context) {
1086
    assert(widget.debugCheckContext(context));
1087
    super.build(context); // See AutomaticKeepAliveClientMixin.
1088
    for (final _HighlightType type in _highlights.keys) {
1089 1090
      _highlights[type]?.color = getHighlightColorForType(type);
    }
1091 1092

    const Set<MaterialState> pressed = <MaterialState>{MaterialState.pressed};
1093
    _currentSplash?.color = widget.overlayColor?.resolve(pressed) ?? widget.splashColor ?? Theme.of(context).splashColor;
1094

1095 1096 1097 1098
    final MouseCursor effectiveMouseCursor = MaterialStateProperty.resolveAs<MouseCursor>(
      widget.mouseCursor ?? MaterialStateMouseCursor.clickable,
      <MaterialState>{
        if (!enabled) MaterialState.disabled,
1099
        if (_hovering && enabled) MaterialState.hovered,
1100 1101 1102
        if (_hasFocus) MaterialState.focused,
      },
    );
1103

1104 1105 1106 1107 1108 1109
    return _ParentInkResponseProvider(
      state: this,
      child: Actions(
        actions: _actionMap,
        child: Focus(
          focusNode: widget.focusNode,
1110
          canRequestFocus: _canRequestFocus,
1111 1112 1113
          onFocusChange: _handleFocusUpdate,
          autofocus: widget.autofocus,
          child: MouseRegion(
1114
            cursor: effectiveMouseCursor,
1115 1116
            onEnter: _handleMouseEnter,
            onExit: _handleMouseExit,
1117 1118 1119 1120 1121
            child: Semantics(
              onTap: widget.excludeFromSemantics || widget.onTap == null ? null : _simulateTap,
              onLongPress: widget.excludeFromSemantics || widget.onLongPress == null ? null : _simulateLongPress,
              child: GestureDetector(
                onTapDown: enabled ? _handleTapDown : null,
1122
                onTapUp: enabled ? _handleTapUp : null,
1123 1124 1125 1126 1127 1128 1129 1130
                onTap: enabled ? _handleTap : null,
                onTapCancel: enabled ? _handleTapCancel : null,
                onDoubleTap: widget.onDoubleTap != null ? _handleDoubleTap : null,
                onLongPress: widget.onLongPress != null ? _handleLongPress : null,
                behavior: HitTestBehavior.opaque,
                excludeFromSemantics: true,
                child: widget.child,
              ),
1131
            ),
1132 1133
          ),
        ),
1134
      ),
1135
    );
1136
  }
1137
}
1138

1139
/// A rectangular area of a [Material] that responds to touch.
1140
///
1141 1142 1143 1144 1145
/// 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.
///
1146
/// ![The highlight is a rectangle the size of the box.](https://flutter.github.io/assets-for-api-docs/assets/material/ink_well.png)
1147
///
1148
/// The [InkWell] widget must have a [Material] widget as an ancestor. The
1149 1150 1151 1152
/// [Material] widget is where the ink reactions are actually painted. This
/// matches the material design premise wherein the [Material] is what is
/// actually reacting to touches by spreading ink.
///
1153
/// If a Widget uses this class directly, it should include the following line
1154
/// at the top of its build function to call [debugCheckHasMaterial]:
1155
///
1156 1157 1158 1159
/// ```dart
/// assert(debugCheckHasMaterial(context));
/// ```
///
Ian Hickson's avatar
Ian Hickson committed
1160 1161 1162 1163 1164 1165
/// ## 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
1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178
/// 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
1179
///
1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191
/// ### 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.
///
1192 1193 1194 1195 1196 1197 1198
/// ### 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:
///
1199
/// {@tool dartpad}
1200 1201 1202
/// 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.
///
1203
/// ** See code in examples/api/lib/material/ink_well/ink_well.0.dart **
1204 1205 1206 1207 1208 1209 1210
/// {@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.
///
1211 1212 1213
/// See also:
///
///  * [GestureDetector], for listening for gestures without ink splashes.
1214
///  * [ElevatedButton] and [TextButton], two kinds of buttons in material design.
1215 1216
///  * [InkResponse], a variant of [InkWell] that doesn't force a rectangular
///    shape on the ink reaction.
1217
class InkWell extends InkResponse {
1218 1219 1220
  /// Creates an ink well.
  ///
  /// Must have an ancestor [Material] widget in which to cause ink reactions.
Ian Hickson's avatar
Ian Hickson committed
1221
  ///
1222
  /// The [enableFeedback], and [excludeFromSemantics] arguments
1223
  /// must not be null.
1224
  const InkWell({
1225 1226 1227 1228 1229 1230
    Key? key,
    Widget? child,
    GestureTapCallback? onTap,
    GestureTapCallback? onDoubleTap,
    GestureLongPressCallback? onLongPress,
    GestureTapDownCallback? onTapDown,
1231
    GestureTapUpCallback? onTapUp,
1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245
    GestureTapCancelCallback? onTapCancel,
    ValueChanged<bool>? onHighlightChanged,
    ValueChanged<bool>? onHover,
    MouseCursor? mouseCursor,
    Color? focusColor,
    Color? hoverColor,
    Color? highlightColor,
    MaterialStateProperty<Color?>? overlayColor,
    Color? splashColor,
    InteractiveInkFeatureFactory? splashFactory,
    double? radius,
    BorderRadius? borderRadius,
    ShapeBorder? customBorder,
    bool? enableFeedback = true,
1246
    bool excludeFromSemantics = false,
1247
    FocusNode? focusNode,
1248
    bool canRequestFocus = true,
1249
    ValueChanged<bool>? onFocusChange,
1250
    bool autofocus = false,
1251 1252 1253 1254 1255
  }) : super(
    key: key,
    child: child,
    onTap: onTap,
    onDoubleTap: onDoubleTap,
1256
    onLongPress: onLongPress,
1257
    onTapDown: onTapDown,
1258
    onTapUp: onTapUp,
1259
    onTapCancel: onTapCancel,
1260
    onHighlightChanged: onHighlightChanged,
1261
    onHover: onHover,
1262
    mouseCursor: mouseCursor,
1263
    containedInkWell: true,
1264
    highlightShape: BoxShape.rectangle,
1265 1266
    focusColor: focusColor,
    hoverColor: hoverColor,
1267
    highlightColor: highlightColor,
1268
    overlayColor: overlayColor,
1269
    splashColor: splashColor,
1270 1271
    splashFactory: splashFactory,
    radius: radius,
1272
    borderRadius: borderRadius,
1273
    customBorder: customBorder,
1274
    enableFeedback: enableFeedback ?? true,
1275
    excludeFromSemantics: excludeFromSemantics,
1276
    focusNode: focusNode,
1277
    canRequestFocus: canRequestFocus,
1278
    onFocusChange: onFocusChange,
1279
    autofocus: autofocus,
1280
  );
1281
}