1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5/// @docImport 'data_table.dart';
6/// @docImport 'elevated_button.dart';
7/// @docImport 'icon_button.dart';
8/// @docImport 'ink_decoration.dart';
9/// @docImport 'ink_ripple.dart';
10/// @docImport 'ink_splash.dart';
11/// @docImport 'text_button.dart';
12library;
13
14import 'dart:async';
15import 'dart:collection';
16
17import 'package:flutter/foundation.dart';
18import 'package:flutter/gestures.dart';
19import 'package:flutter/rendering.dart';
20import 'package:flutter/widgets.dart';
21
22import 'debug.dart';
23import 'ink_highlight.dart';
24import 'material.dart';
25import 'material_state.dart';
26import 'theme.dart';
27
28// Examples can assume:
29// late BuildContext context;
30
31/// An ink feature that displays a [color] "splash" in response to a user
32/// gesture that can be confirmed or canceled.
33///
34/// Subclasses call [confirm] when an input gesture is recognized. For
35/// example a press event might trigger an ink feature that's confirmed
36/// when the corresponding up event is seen.
37///
38/// Subclasses call [cancel] when an input gesture is aborted before it
39/// is recognized. For example a press event might trigger an ink feature
40/// that's canceled when the pointer is dragged out of the reference
41/// box.
42///
43/// The [InkWell] and [InkResponse] widgets generate instances of this
44/// class.
45abstract class InteractiveInkFeature extends InkFeature {
46 /// Creates an InteractiveInkFeature.
47 InteractiveInkFeature({
48 required super.controller,
49 required super.referenceBox,
50 required Color color,
51 ShapeBorder? customBorder,
52 super.onRemoved,
53 }) : _color = color,
54 _customBorder = customBorder;
55
56 /// Called when the user input that triggered this feature's appearance was confirmed.
57 ///
58 /// Typically causes the ink to propagate faster across the material. By default this
59 /// method does nothing.
60 void confirm() {}
61
62 /// Called when the user input that triggered this feature's appearance was canceled.
63 ///
64 /// Typically causes the ink to gradually disappear. By default this method does
65 /// nothing.
66 void cancel() {}
67
68 /// The ink's color.
69 Color get color => _color;
70 Color _color;
71 set color(Color value) {
72 if (value == _color) {
73 return;
74 }
75 _color = value;
76 controller.markNeedsPaint();
77 }
78
79 /// The ink's optional custom border.
80 ShapeBorder? get customBorder => _customBorder;
81 ShapeBorder? _customBorder;
82 set customBorder(ShapeBorder? value) {
83 if (value == _customBorder) {
84 return;
85 }
86 _customBorder = value;
87 controller.markNeedsPaint();
88 }
89
90 /// Draws an ink splash or ink ripple on the passed in [Canvas].
91 ///
92 /// The [transform] argument is the [Matrix4] transform that typically
93 /// shifts the coordinate space of the canvas to the space in which
94 /// the ink circle is to be painted.
95 ///
96 /// [center] is the [Offset] from origin of the canvas where the center
97 /// of the circle is drawn.
98 ///
99 /// [paint] takes a [Paint] object that describes the styles used to draw the ink circle.
100 /// For example, [paint] can specify properties like color, strokewidth, colorFilter.
101 ///
102 /// [radius] is the radius of ink circle to be drawn on canvas.
103 ///
104 /// [clipCallback] is the callback used to obtain the [Rect] used for clipping the ink effect.
105 /// If [clipCallback] is null, no clipping is performed on the ink circle.
106 ///
107 /// Clipping can happen in 3 different ways:
108 /// 1. If [customBorder] is provided, it is used to determine the path
109 /// for clipping.
110 /// 2. If [customBorder] is null, and [borderRadius] is provided, the canvas
111 /// is clipped by an [RRect] created from [clipCallback] and [borderRadius].
112 /// 3. If [borderRadius] is the default [BorderRadius.zero], then the [Rect] provided
113 /// by [clipCallback] is used for clipping.
114 ///
115 /// [textDirection] is used by [customBorder] if it is non-null. This allows the [customBorder]'s path
116 /// to be properly defined if it was the path was expressed in terms of "start" and "end" instead of
117 /// "left" and "right".
118 ///
119 /// For examples on how the function is used, see [InkSplash] and [InkRipple].
120 @protected
121 void paintInkCircle({
122 required Canvas canvas,
123 required Matrix4 transform,
124 required Paint paint,
125 required Offset center,
126 required double radius,
127 TextDirection? textDirection,
128 ShapeBorder? customBorder,
129 BorderRadius borderRadius = BorderRadius.zero,
130 RectCallback? clipCallback,
131 }) {
132 final Offset? originOffset = MatrixUtils.getAsTranslation(transform);
133 canvas.save();
134 if (originOffset == null) {
135 canvas.transform(transform.storage);
136 } else {
137 canvas.translate(originOffset.dx, originOffset.dy);
138 }
139 if (clipCallback != null) {
140 final Rect rect = clipCallback();
141 if (customBorder != null) {
142 canvas.clipPath(customBorder.getOuterPath(rect, textDirection: textDirection));
143 } else if (borderRadius != BorderRadius.zero) {
144 canvas.clipRRect(
145 RRect.fromRectAndCorners(
146 rect,
147 topLeft: borderRadius.topLeft,
148 topRight: borderRadius.topRight,
149 bottomLeft: borderRadius.bottomLeft,
150 bottomRight: borderRadius.bottomRight,
151 ),
152 );
153 } else {
154 canvas.clipRect(rect);
155 }
156 }
157 canvas.drawCircle(center, radius, paint);
158 canvas.restore();
159 }
160}
161
162/// An encapsulation of an [InteractiveInkFeature] constructor used by
163/// [InkWell], [InkResponse], and [ThemeData].
164///
165/// Interactive ink feature implementations should provide a static const
166/// `splashFactory` value that's an instance of this class. The `splashFactory`
167/// can be used to configure an [InkWell], [InkResponse] or [ThemeData].
168///
169/// See also:
170///
171/// * [InkSplash.splashFactory]
172/// * [InkRipple.splashFactory]
173abstract class InteractiveInkFeatureFactory {
174 /// Abstract const constructor. This constructor enables subclasses to provide
175 /// const constructors so that they can be used in const expressions.
176 ///
177 /// Subclasses should provide a const constructor.
178 const InteractiveInkFeatureFactory();
179
180 /// The factory method.
181 ///
182 /// Subclasses should override this method to return a new instance of an
183 /// [InteractiveInkFeature].
184 @factory
185 InteractiveInkFeature create({
186 required MaterialInkController controller,
187 required RenderBox referenceBox,
188 required Offset position,
189 required Color color,
190 required TextDirection textDirection,
191 bool containedInkWell = false,
192 RectCallback? rectCallback,
193 BorderRadius? borderRadius,
194 ShapeBorder? customBorder,
195 double? radius,
196 VoidCallback? onRemoved,
197 });
198}
199
200abstract class _ParentInkResponseState {
201 void markChildInkResponsePressed(_ParentInkResponseState childState, bool value);
202}
203
204class _ParentInkResponseProvider extends InheritedWidget {
205 const _ParentInkResponseProvider({required this.state, required super.child});
206
207 final _ParentInkResponseState state;
208
209 @override
210 bool updateShouldNotify(_ParentInkResponseProvider oldWidget) => state != oldWidget.state;
211
212 static _ParentInkResponseState? maybeOf(BuildContext context) {
213 return context.dependOnInheritedWidgetOfExactType<_ParentInkResponseProvider>()?.state;
214 }
215}
216
217typedef _GetRectCallback = RectCallback? Function(RenderBox referenceBox);
218typedef _CheckContext = bool Function(BuildContext context);
219
220/// An area of a [Material] that responds to touch. Has a configurable shape and
221/// can be configured to clip splashes that extend outside its bounds or not.
222///
223/// For a variant of this widget that is specialized for rectangular areas that
224/// always clip splashes, see [InkWell].
225///
226/// An [InkResponse] widget does two things when responding to a tap:
227///
228/// * It starts to animate a _highlight_. The shape of the highlight is
229/// determined by [highlightShape]. If it is a [BoxShape.circle], the
230/// default, then the highlight is a circle of fixed size centered in the
231/// [InkResponse]. If it is [BoxShape.rectangle], then the highlight is a box
232/// the size of the [InkResponse] itself, unless [getRectCallback] is
233/// provided, in which case that callback defines the rectangle. The color of
234/// the highlight is set by [highlightColor].
235///
236/// * Simultaneously, it starts to animate a _splash_. This is a growing circle
237/// initially centered on the tap location. If this is a [containedInkWell],
238/// the splash grows to the [radius] while remaining centered at the tap
239/// location. Otherwise, the splash migrates to the center of the box as it
240/// grows.
241///
242/// The following two diagrams show how [InkResponse] looks when tapped if the
243/// [highlightShape] is [BoxShape.circle] (the default) and [containedInkWell]
244/// is false (also the default).
245///
246/// The first diagram shows how it looks if the [InkResponse] is relatively
247/// large:
248///
249/// ![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)
250///
251/// The second diagram shows how it looks if the [InkResponse] is small:
252///
253/// ![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)
254///
255/// The main thing to notice from these diagrams is that the splashes happily
256/// exceed the bounds of the widget (because [containedInkWell] is false).
257///
258/// The following diagram shows the effect when the [InkResponse] has a
259/// [highlightShape] of [BoxShape.rectangle] with [containedInkWell] set to
260/// true. These are the values used by [InkWell].
261///
262/// ![The highlight is a rectangle the size of the box.](https://flutter.github.io/assets-for-api-docs/assets/material/ink_well.png)
263///
264/// The [InkResponse] widget must have a [Material] widget as an ancestor. The
265/// [Material] widget is where the ink reactions are actually painted. This
266/// matches the Material Design premise wherein the [Material] is what is
267/// actually reacting to touches by spreading ink.
268///
269/// If a Widget uses this class directly, it should include the following line
270/// at the top of its build function to call [debugCheckHasMaterial]:
271///
272/// ```dart
273/// assert(debugCheckHasMaterial(context));
274/// ```
275///
276/// ## Troubleshooting
277///
278/// ### The ink splashes aren't visible!
279///
280/// If there is an opaque graphic, e.g. painted using a [Container], [Image], or
281/// [DecoratedBox], between the [Material] widget and the [InkResponse] widget,
282/// then the splash won't be visible because it will be under the opaque graphic.
283/// This is because ink splashes draw on the underlying [Material] itself, as
284/// if the ink was spreading inside the material.
285///
286/// The [Ink] widget can be used as a replacement for [Image], [Container], or
287/// [DecoratedBox] to ensure that the image or decoration also paints in the
288/// [Material] itself, below the ink.
289///
290/// If this is not possible for some reason, e.g. because you are using an
291/// opaque [CustomPaint] widget, alternatively consider using a second
292/// [Material] above the opaque widget but below the [InkResponse] (as an
293/// ancestor to the ink response). The [MaterialType.transparency] material
294/// kind can be used for this purpose.
295///
296/// See also:
297///
298/// * [GestureDetector], for listening for gestures without ink splashes.
299/// * [ElevatedButton] and [TextButton], two kinds of buttons in Material Design.
300/// * [IconButton], which combines [InkResponse] with an [Icon].
301class InkResponse extends StatelessWidget {
302 /// Creates an area of a [Material] that responds to touch.
303 ///
304 /// Must have an ancestor [Material] widget in which to cause ink reactions.
305 const InkResponse({
306 super.key,
307 this.child,
308 this.onTap,
309 this.onTapDown,
310 this.onTapUp,
311 this.onTapCancel,
312 this.onDoubleTap,
313 this.onLongPress,
314 this.onSecondaryTap,
315 this.onSecondaryTapUp,
316 this.onSecondaryTapDown,
317 this.onSecondaryTapCancel,
318 this.onHighlightChanged,
319 this.onHover,
320 this.mouseCursor,
321 this.containedInkWell = false,
322 this.highlightShape = BoxShape.circle,
323 this.radius,
324 this.borderRadius,
325 this.customBorder,
326 this.focusColor,
327 this.hoverColor,
328 this.highlightColor,
329 this.overlayColor,
330 this.splashColor,
331 this.splashFactory,
332 this.enableFeedback = true,
333 this.excludeFromSemantics = false,
334 this.focusNode,
335 this.canRequestFocus = true,
336 this.onFocusChange,
337 this.autofocus = false,
338 this.statesController,
339 this.hoverDuration,
340 });
341
342 /// The widget below this widget in the tree.
343 ///
344 /// {@macro flutter.widgets.ProxyWidget.child}
345 final Widget? child;
346
347 /// Called when the user taps this part of the material.
348 final GestureTapCallback? onTap;
349
350 /// Called when the user taps down this part of the material.
351 final GestureTapDownCallback? onTapDown;
352
353 /// Called when the user releases a tap that was started on this part of the
354 /// material. [onTap] is called immediately after.
355 final GestureTapUpCallback? onTapUp;
356
357 /// Called when the user cancels a tap that was started on this part of the
358 /// material.
359 final GestureTapCallback? onTapCancel;
360
361 /// Called when the user double taps this part of the material.
362 final GestureTapCallback? onDoubleTap;
363
364 /// Called when the user long-presses on this part of the material.
365 final GestureLongPressCallback? onLongPress;
366
367 /// Called when the user taps this part of the material with a secondary button.
368 ///
369 /// See also:
370 ///
371 /// * [kSecondaryButton], the button this callback responds to.
372 final GestureTapCallback? onSecondaryTap;
373
374 /// Called when the user taps down on this part of the material with a
375 /// secondary button.
376 ///
377 /// See also:
378 ///
379 /// * [kSecondaryButton], the button this callback responds to.
380 final GestureTapDownCallback? onSecondaryTapDown;
381
382 /// Called when the user releases a secondary button tap that was started on
383 /// this part of the material. [onSecondaryTap] is called immediately after.
384 ///
385 /// See also:
386 ///
387 /// * [onSecondaryTap], a handler triggered right after this one that doesn't
388 /// pass any details about the tap.
389 /// * [kSecondaryButton], the button this callback responds to.
390 final GestureTapUpCallback? onSecondaryTapUp;
391
392 /// Called when the user cancels a secondary button tap that was started on
393 /// this part of the material.
394 ///
395 /// See also:
396 ///
397 /// * [kSecondaryButton], the button this callback responds to.
398 final GestureTapCallback? onSecondaryTapCancel;
399
400 /// Called when this part of the material either becomes highlighted or stops
401 /// being highlighted.
402 ///
403 /// The value passed to the callback is true if this part of the material has
404 /// become highlighted and false if this part of the material has stopped
405 /// being highlighted.
406 ///
407 /// If all of [onTap], [onDoubleTap], and [onLongPress] become null while a
408 /// gesture is ongoing, then [onTapCancel] will be fired and
409 /// [onHighlightChanged] will be fired with the value false _during the
410 /// build_. This means, for instance, that in that scenario [State.setState]
411 /// cannot be called.
412 final ValueChanged<bool>? onHighlightChanged;
413
414 /// Called when a pointer enters or exits the ink response area.
415 ///
416 /// The value passed to the callback is true if a pointer has entered this
417 /// part of the material and false if a pointer has exited this part of the
418 /// material.
419 final ValueChanged<bool>? onHover;
420
421 /// The cursor for a mouse pointer when it enters or is hovering over the
422 /// widget.
423 ///
424 /// If [mouseCursor] is a [WidgetStateMouseCursor],
425 /// [WidgetStateProperty.resolve] is used for the following [WidgetState]s:
426 ///
427 /// * [WidgetState.hovered].
428 /// * [WidgetState.focused].
429 /// * [WidgetState.disabled].
430 ///
431 /// If this property is null, [WidgetStateMouseCursor.clickable] will be used.
432 final MouseCursor? mouseCursor;
433
434 /// Whether this ink response should be clipped its bounds.
435 ///
436 /// This flag also controls whether the splash migrates to the center of the
437 /// [InkResponse] or not. If [containedInkWell] is true, the splash remains
438 /// centered around the tap location. If it is false, the splash migrates to
439 /// the center of the [InkResponse] as it grows.
440 ///
441 /// See also:
442 ///
443 /// * [highlightShape], the shape of the focus, hover, and pressed
444 /// highlights.
445 /// * [borderRadius], which controls the corners when the box is a rectangle.
446 /// * [getRectCallback], which controls the size and position of the box when
447 /// it is a rectangle.
448 final bool containedInkWell;
449
450 /// The shape (e.g., circle, rectangle) to use for the highlight drawn around
451 /// this part of the material when pressed, hovered over, or focused.
452 ///
453 /// The same shape is used for the pressed highlight (see [highlightColor]),
454 /// the focus highlight (see [focusColor]), and the hover highlight (see
455 /// [hoverColor]).
456 ///
457 /// If the shape is [BoxShape.circle], then the highlight is centered on the
458 /// [InkResponse]. If the shape is [BoxShape.rectangle], then the highlight
459 /// fills the [InkResponse], or the rectangle provided by [getRectCallback] if
460 /// the callback is specified.
461 ///
462 /// See also:
463 ///
464 /// * [containedInkWell], which controls clipping behavior.
465 /// * [borderRadius], which controls the corners when the box is a rectangle.
466 /// * [highlightColor], the color of the highlight.
467 /// * [getRectCallback], which controls the size and position of the box when
468 /// it is a rectangle.
469 final BoxShape highlightShape;
470
471 /// The radius of the ink splash.
472 ///
473 /// Splashes grow up to this size. By default, this size is determined from
474 /// the size of the rectangle provided by [getRectCallback], or the size of
475 /// the [InkResponse] itself.
476 ///
477 /// See also:
478 ///
479 /// * [splashColor], the color of the splash.
480 /// * [splashFactory], which defines the appearance of the splash.
481 final double? radius;
482
483 /// The border radius of the containing rectangle. This is effective only if
484 /// [highlightShape] is [BoxShape.rectangle].
485 ///
486 /// If this is null, it is interpreted as [BorderRadius.zero].
487 final BorderRadius? borderRadius;
488
489 /// The custom clip border.
490 ///
491 /// If this is null, the ink response will not clip its content.
492 final ShapeBorder? customBorder;
493
494 /// The color of the ink response when the parent widget is focused. If this
495 /// property is null then the focus color of the theme,
496 /// [ThemeData.focusColor], will be used.
497 ///
498 /// See also:
499 ///
500 /// * [highlightShape], the shape of the focus, hover, and pressed
501 /// highlights.
502 /// * [hoverColor], the color of the hover highlight.
503 /// * [splashColor], the color of the splash.
504 /// * [splashFactory], which defines the appearance of the splash.
505 final Color? focusColor;
506
507 /// The color of the ink response when a pointer is hovering over it. If this
508 /// property is null then the hover color of the theme,
509 /// [ThemeData.hoverColor], will be used.
510 ///
511 /// See also:
512 ///
513 /// * [highlightShape], the shape of the focus, hover, and pressed
514 /// highlights.
515 /// * [highlightColor], the color of the pressed highlight.
516 /// * [focusColor], the color of the focus highlight.
517 /// * [splashColor], the color of the splash.
518 /// * [splashFactory], which defines the appearance of the splash.
519 final Color? hoverColor;
520
521 /// The highlight color of the ink response when pressed. If this property is
522 /// null then the highlight color of the theme, [ThemeData.highlightColor],
523 /// will be used.
524 ///
525 /// See also:
526 ///
527 /// * [hoverColor], the color of the hover highlight.
528 /// * [focusColor], the color of the focus highlight.
529 /// * [highlightShape], the shape of the focus, hover, and pressed
530 /// highlights.
531 /// * [splashColor], the color of the splash.
532 /// * [splashFactory], which defines the appearance of the splash.
533 final Color? highlightColor;
534
535 /// Defines the ink response focus, hover, and splash colors.
536 ///
537 /// This default null property can be used as an alternative to
538 /// [focusColor], [hoverColor], [highlightColor], and
539 /// [splashColor]. If non-null, it is resolved against one of
540 /// [WidgetState.focused], [WidgetState.hovered], and
541 /// [WidgetState.pressed]. It's convenient to use when the parent
542 /// widget can pass along its own WidgetStateProperty value for
543 /// the overlay color.
544 ///
545 /// [WidgetState.pressed] triggers a ripple (an ink splash), per
546 /// the current Material Design spec. The [overlayColor] doesn't map
547 /// a state to [highlightColor] because a separate highlight is not
548 /// used by the current design guidelines. See
549 /// https://material.io/design/interaction/states.html#pressed
550 ///
551 /// If the overlay color is null or resolves to null, then [focusColor],
552 /// [hoverColor], [splashColor] and their defaults are used instead.
553 ///
554 /// See also:
555 ///
556 /// * The Material Design specification for overlay colors and how they
557 /// match a component's state:
558 /// <https://material.io/design/interaction/states.html#anatomy>.
559 final MaterialStateProperty<Color?>? overlayColor;
560
561 /// The splash color of the ink response. If this property is null then the
562 /// splash color of the theme, [ThemeData.splashColor], will be used.
563 ///
564 /// See also:
565 ///
566 /// * [splashFactory], which defines the appearance of the splash.
567 /// * [radius], the (maximum) size of the ink splash.
568 /// * [highlightColor], the color of the highlight.
569 final Color? splashColor;
570
571 /// Defines the appearance of the splash.
572 ///
573 /// Defaults to the value of the theme's splash factory: [ThemeData.splashFactory].
574 ///
575 /// See also:
576 ///
577 /// * [radius], the (maximum) size of the ink splash.
578 /// * [splashColor], the color of the splash.
579 /// * [highlightColor], the color of the highlight.
580 /// * [InkSplash.splashFactory], which defines the default splash.
581 /// * [InkRipple.splashFactory], which defines a splash that spreads out
582 /// more aggressively than the default.
583 final InteractiveInkFeatureFactory? splashFactory;
584
585 /// Whether detected gestures should provide acoustic and/or haptic feedback.
586 ///
587 /// For example, on Android a tap will produce a clicking sound and a
588 /// long-press will produce a short vibration, when feedback is enabled.
589 ///
590 /// See also:
591 ///
592 /// * [Feedback] for providing platform-specific feedback to certain actions.
593 final bool enableFeedback;
594
595 /// Whether to exclude the gestures introduced by this widget from the
596 /// semantics tree.
597 ///
598 /// For example, a long-press gesture for showing a tooltip is usually
599 /// excluded because the tooltip itself is included in the semantics
600 /// tree directly and so having a gesture to show it would result in
601 /// duplication of information.
602 final bool excludeFromSemantics;
603
604 /// {@template flutter.material.inkwell.onFocusChange}
605 /// Handler called when the focus changes.
606 ///
607 /// Called with true if this widget's node gains focus, and false if it loses
608 /// focus.
609 /// {@endtemplate}
610 final ValueChanged<bool>? onFocusChange;
611
612 /// {@macro flutter.widgets.Focus.autofocus}
613 final bool autofocus;
614
615 /// {@macro flutter.widgets.Focus.focusNode}
616 final FocusNode? focusNode;
617
618 /// {@macro flutter.widgets.Focus.canRequestFocus}
619 final bool canRequestFocus;
620
621 /// The rectangle to use for the highlight effect and for clipping
622 /// the splash effects if [containedInkWell] is true.
623 ///
624 /// This method is intended to be overridden by descendants that
625 /// specialize [InkResponse] for unusual cases. For example,
626 /// [TableRowInkWell] implements this method to return the rectangle
627 /// corresponding to the row that the widget is in.
628 ///
629 /// The default behavior returns null, which is equivalent to
630 /// returning the referenceBox argument's bounding box (though
631 /// slightly more efficient).
632 RectCallback? getRectCallback(RenderBox referenceBox) => null;
633
634 /// {@template flutter.material.inkwell.statesController}
635 /// Represents the interactive "state" of this widget in terms of
636 /// a set of [WidgetState]s, like [WidgetState.pressed] and
637 /// [WidgetState.focused].
638 ///
639 /// Classes based on this one can provide their own
640 /// [WidgetStatesController] to which they've added listeners.
641 /// They can also update the controller's [WidgetStatesController.value]
642 /// however, this may only be done when it's safe to call
643 /// [State.setState], like in an event handler.
644 /// {@endtemplate}
645 final MaterialStatesController? statesController;
646
647 /// The duration of the animation that animates the hover effect.
648 ///
649 /// The default is 50ms.
650 final Duration? hoverDuration;
651
652 @override
653 Widget build(BuildContext context) {
654 final _ParentInkResponseState? parentState = _ParentInkResponseProvider.maybeOf(context);
655 return _InkResponseStateWidget(
656 onTap: onTap,
657 onTapDown: onTapDown,
658 onTapUp: onTapUp,
659 onTapCancel: onTapCancel,
660 onDoubleTap: onDoubleTap,
661 onLongPress: onLongPress,
662 onSecondaryTap: onSecondaryTap,
663 onSecondaryTapUp: onSecondaryTapUp,
664 onSecondaryTapDown: onSecondaryTapDown,
665 onSecondaryTapCancel: onSecondaryTapCancel,
666 onHighlightChanged: onHighlightChanged,
667 onHover: onHover,
668 mouseCursor: mouseCursor,
669 containedInkWell: containedInkWell,
670 highlightShape: highlightShape,
671 radius: radius,
672 borderRadius: borderRadius,
673 customBorder: customBorder,
674 focusColor: focusColor,
675 hoverColor: hoverColor,
676 highlightColor: highlightColor,
677 overlayColor: overlayColor,
678 splashColor: splashColor,
679 splashFactory: splashFactory,
680 enableFeedback: enableFeedback,
681 excludeFromSemantics: excludeFromSemantics,
682 focusNode: focusNode,
683 canRequestFocus: canRequestFocus,
684 onFocusChange: onFocusChange,
685 autofocus: autofocus,
686 parentState: parentState,
687 getRectCallback: getRectCallback,
688 debugCheckContext: debugCheckContext,
689 statesController: statesController,
690 hoverDuration: hoverDuration,
691 child: child,
692 );
693 }
694
695 /// Asserts that the given context satisfies the prerequisites for
696 /// this class.
697 ///
698 /// This method is intended to be overridden by descendants that
699 /// specialize [InkResponse] for unusual cases. For example,
700 /// [TableRowInkWell] implements this method to verify that the widget is
701 /// in a table.
702 @mustCallSuper
703 bool debugCheckContext(BuildContext context) {
704 assert(debugCheckHasMaterial(context));
705 assert(debugCheckHasDirectionality(context));
706 return true;
707 }
708}
709
710class _InkResponseStateWidget extends StatefulWidget {
711 const _InkResponseStateWidget({
712 this.child,
713 this.onTap,
714 this.onTapDown,
715 this.onTapUp,
716 this.onTapCancel,
717 this.onDoubleTap,
718 this.onLongPress,
719 this.onSecondaryTap,
720 this.onSecondaryTapUp,
721 this.onSecondaryTapDown,
722 this.onSecondaryTapCancel,
723 this.onHighlightChanged,
724 this.onHover,
725 this.mouseCursor,
726 this.containedInkWell = false,
727 this.highlightShape = BoxShape.circle,
728 this.radius,
729 this.borderRadius,
730 this.customBorder,
731 this.focusColor,
732 this.hoverColor,
733 this.highlightColor,
734 this.overlayColor,
735 this.splashColor,
736 this.splashFactory,
737 this.enableFeedback = true,
738 this.excludeFromSemantics = false,
739 this.focusNode,
740 this.canRequestFocus = true,
741 this.onFocusChange,
742 this.autofocus = false,
743 this.parentState,
744 this.getRectCallback,
745 required this.debugCheckContext,
746 this.statesController,
747 this.hoverDuration,
748 });
749
750 final Widget? child;
751 final GestureTapCallback? onTap;
752 final GestureTapDownCallback? onTapDown;
753 final GestureTapUpCallback? onTapUp;
754 final GestureTapCallback? onTapCancel;
755 final GestureTapCallback? onDoubleTap;
756 final GestureLongPressCallback? onLongPress;
757 final GestureTapCallback? onSecondaryTap;
758 final GestureTapUpCallback? onSecondaryTapUp;
759 final GestureTapDownCallback? onSecondaryTapDown;
760 final GestureTapCallback? onSecondaryTapCancel;
761 final ValueChanged<bool>? onHighlightChanged;
762 final ValueChanged<bool>? onHover;
763 final MouseCursor? mouseCursor;
764 final bool containedInkWell;
765 final BoxShape highlightShape;
766 final double? radius;
767 final BorderRadius? borderRadius;
768 final ShapeBorder? customBorder;
769 final Color? focusColor;
770 final Color? hoverColor;
771 final Color? highlightColor;
772 final MaterialStateProperty<Color?>? overlayColor;
773 final Color? splashColor;
774 final InteractiveInkFeatureFactory? splashFactory;
775 final bool enableFeedback;
776 final bool excludeFromSemantics;
777 final ValueChanged<bool>? onFocusChange;
778 final bool autofocus;
779 final FocusNode? focusNode;
780 final bool canRequestFocus;
781 final _ParentInkResponseState? parentState;
782 final _GetRectCallback? getRectCallback;
783 final _CheckContext debugCheckContext;
784 final MaterialStatesController? statesController;
785 final Duration? hoverDuration;
786
787 @override
788 _InkResponseState createState() => _InkResponseState();
789
790 @override
791 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
792 super.debugFillProperties(properties);
793 final List<String> gestures = <String>[
794 if (onTap != null) 'tap',
795 if (onDoubleTap != null) 'double tap',
796 if (onLongPress != null) 'long press',
797 if (onTapDown != null) 'tap down',
798 if (onTapUp != null) 'tap up',
799 if (onTapCancel != null) 'tap cancel',
800 if (onSecondaryTap != null) 'secondary tap',
801 if (onSecondaryTapUp != null) 'secondary tap up',
802 if (onSecondaryTapDown != null) 'secondary tap down',
803 if (onSecondaryTapCancel != null) 'secondary tap cancel',
804 ];
805 properties.add(IterableProperty<String>('gestures', gestures, ifEmpty: '<none>'));
806 properties.add(DiagnosticsProperty<MouseCursor>('mouseCursor', mouseCursor));
807 properties.add(
808 DiagnosticsProperty<bool>('containedInkWell', containedInkWell, level: DiagnosticLevel.fine),
809 );
810 properties.add(
811 DiagnosticsProperty<BoxShape>(
812 'highlightShape',
813 highlightShape,
814 description: '${containedInkWell ? "clipped to " : ""}$highlightShape',
815 showName: false,
816 ),
817 );
818 }
819}
820
821/// Used to index the allocated highlights for the different types of highlights
822/// in [_InkResponseState].
823enum _HighlightType { pressed, hover, focus }
824
825class _InkResponseState extends State<_InkResponseStateWidget>
826 with AutomaticKeepAliveClientMixin<_InkResponseStateWidget>
827 implements _ParentInkResponseState {
828 Set<InteractiveInkFeature>? _splashes;
829 InteractiveInkFeature? _currentSplash;
830 bool _hovering = false;
831 final Map<_HighlightType, InkHighlight?> _highlights = <_HighlightType, InkHighlight?>{};
832 late final Map<Type, Action<Intent>> _actionMap = <Type, Action<Intent>>{
833 ActivateIntent: CallbackAction<ActivateIntent>(onInvoke: activateOnIntent),
834 ButtonActivateIntent: CallbackAction<ButtonActivateIntent>(onInvoke: activateOnIntent),
835 };
836 MaterialStatesController? internalStatesController;
837
838 bool get highlightsExist =>
839 _highlights.values.where((InkHighlight? highlight) => highlight != null).isNotEmpty;
840
841 final ObserverList<_ParentInkResponseState> _activeChildren =
842 ObserverList<_ParentInkResponseState>();
843
844 static const Duration _activationDuration = Duration(milliseconds: 100);
845 Timer? _activationTimer;
846
847 @override
848 void markChildInkResponsePressed(_ParentInkResponseState childState, bool value) {
849 final bool lastAnyPressed = _anyChildInkResponsePressed;
850 if (value) {
851 _activeChildren.add(childState);
852 } else {
853 _activeChildren.remove(childState);
854 }
855 final bool nowAnyPressed = _anyChildInkResponsePressed;
856 if (nowAnyPressed != lastAnyPressed) {
857 widget.parentState?.markChildInkResponsePressed(this, nowAnyPressed);
858 }
859 }
860
861 bool get _anyChildInkResponsePressed => _activeChildren.isNotEmpty;
862
863 void activateOnIntent(Intent? intent) {
864 _activationTimer?.cancel();
865 _activationTimer = null;
866 _startNewSplash(context: context);
867 _currentSplash?.confirm();
868 _currentSplash = null;
869 if (widget.onTap != null) {
870 if (widget.enableFeedback) {
871 Feedback.forTap(context);
872 }
873 widget.onTap?.call();
874 }
875 // Delay the call to `updateHighlight` to simulate a pressed delay
876 // and give MaterialStatesController listeners a chance to react.
877 _activationTimer = Timer(_activationDuration, () {
878 updateHighlight(_HighlightType.pressed, value: false);
879 });
880 }
881
882 void simulateTap([Intent? intent]) {
883 _startNewSplash(context: context);
884 handleTap();
885 }
886
887 void simulateLongPress() {
888 _startNewSplash(context: context);
889 handleLongPress();
890 }
891
892 void handleStatesControllerChange() {
893 // Force a rebuild to resolve widget.overlayColor, widget.mouseCursor
894 setState(() {});
895 }
896
897 MaterialStatesController get statesController =>
898 widget.statesController ?? internalStatesController!;
899
900 void initStatesController() {
901 if (widget.statesController == null) {
902 internalStatesController = MaterialStatesController();
903 }
904 statesController.update(MaterialState.disabled, !enabled);
905 statesController.addListener(handleStatesControllerChange);
906 }
907
908 @override
909 void initState() {
910 super.initState();
911 initStatesController();
912 FocusManager.instance.addHighlightModeListener(handleFocusHighlightModeChange);
913 }
914
915 @override
916 void didUpdateWidget(_InkResponseStateWidget oldWidget) {
917 super.didUpdateWidget(oldWidget);
918 if (widget.statesController != oldWidget.statesController) {
919 oldWidget.statesController?.removeListener(handleStatesControllerChange);
920 if (widget.statesController != null) {
921 internalStatesController?.dispose();
922 internalStatesController = null;
923 }
924 initStatesController();
925 }
926 if (widget.radius != oldWidget.radius ||
927 widget.highlightShape != oldWidget.highlightShape ||
928 widget.borderRadius != oldWidget.borderRadius) {
929 final InkHighlight? hoverHighlight = _highlights[_HighlightType.hover];
930 if (hoverHighlight != null) {
931 hoverHighlight.dispose();
932 updateHighlight(_HighlightType.hover, value: _hovering, callOnHover: false);
933 }
934 final InkHighlight? focusHighlight = _highlights[_HighlightType.focus];
935 if (focusHighlight != null) {
936 focusHighlight.dispose();
937 // Do not call updateFocusHighlights() here because it is called below
938 }
939 }
940 if (widget.customBorder != oldWidget.customBorder) {
941 _updateHighlightsAndSplashes();
942 }
943 if (enabled != isWidgetEnabled(oldWidget)) {
944 statesController.update(MaterialState.disabled, !enabled);
945 if (!enabled) {
946 statesController.update(MaterialState.pressed, false);
947 // Remove the existing hover highlight immediately when enabled is false.
948 // Do not rely on updateHighlight or InkHighlight.deactivate to not break
949 // the expected lifecycle which is updating _hovering when the mouse exit.
950 // Manually updating _hovering here or calling InkHighlight.deactivate
951 // will lead to onHover not being called or call when it is not allowed.
952 final InkHighlight? hoverHighlight = _highlights[_HighlightType.hover];
953 hoverHighlight?.dispose();
954 }
955 // Don't call widget.onHover because many widgets, including the button
956 // widgets, apply setState to an ancestor context from onHover.
957 updateHighlight(_HighlightType.hover, value: _hovering, callOnHover: false);
958 }
959 updateFocusHighlights();
960 }
961
962 @override
963 void dispose() {
964 FocusManager.instance.removeHighlightModeListener(handleFocusHighlightModeChange);
965 statesController.removeListener(handleStatesControllerChange);
966 internalStatesController?.dispose();
967 _activationTimer?.cancel();
968 _activationTimer = null;
969 super.dispose();
970 }
971
972 @override
973 bool get wantKeepAlive => highlightsExist || (_splashes != null && _splashes!.isNotEmpty);
974
975 Duration getFadeDurationForType(_HighlightType type) {
976 switch (type) {
977 case _HighlightType.pressed:
978 return const Duration(milliseconds: 200);
979 case _HighlightType.hover:
980 case _HighlightType.focus:
981 return widget.hoverDuration ?? const Duration(milliseconds: 50);
982 }
983 }
984
985 void updateHighlight(_HighlightType type, {required bool value, bool callOnHover = true}) {
986 final InkHighlight? highlight = _highlights[type];
987 void handleInkRemoval() {
988 assert(_highlights[type] != null);
989 _highlights[type] = null;
990 updateKeepAlive();
991 }
992
993 switch (type) {
994 case _HighlightType.pressed:
995 statesController.update(MaterialState.pressed, value);
996 case _HighlightType.hover:
997 if (callOnHover) {
998 statesController.update(MaterialState.hovered, value);
999 }
1000 case _HighlightType.focus:
1001 // see handleFocusUpdate()
1002 break;
1003 }
1004
1005 if (type == _HighlightType.pressed) {
1006 widget.parentState?.markChildInkResponsePressed(this, value);
1007 }
1008 if (value == (highlight != null && highlight.active)) {
1009 return;
1010 }
1011
1012 if (value) {
1013 if (highlight == null) {
1014 final Color resolvedOverlayColor =
1015 widget.overlayColor?.resolve(statesController.value) ??
1016 switch (type) {
1017 // Use the backwards compatible defaults
1018 _HighlightType.pressed => widget.highlightColor ?? Theme.of(context).highlightColor,
1019 _HighlightType.focus => widget.focusColor ?? Theme.of(context).focusColor,
1020 _HighlightType.hover => widget.hoverColor ?? Theme.of(context).hoverColor,
1021 };
1022 final RenderBox referenceBox = context.findRenderObject()! as RenderBox;
1023 _highlights[type] = InkHighlight(
1024 controller: Material.of(context),
1025 referenceBox: referenceBox,
1026 color: enabled ? resolvedOverlayColor : resolvedOverlayColor.withAlpha(0),
1027 shape: widget.highlightShape,
1028 radius: widget.radius,
1029 borderRadius: widget.borderRadius,
1030 customBorder: widget.customBorder,
1031 rectCallback: widget.getRectCallback!(referenceBox),
1032 onRemoved: handleInkRemoval,
1033 textDirection: Directionality.of(context),
1034 fadeDuration: getFadeDurationForType(type),
1035 );
1036 updateKeepAlive();
1037 } else {
1038 highlight.activate();
1039 }
1040 } else {
1041 highlight!.deactivate();
1042 }
1043 assert(value == (_highlights[type] != null && _highlights[type]!.active));
1044
1045 switch (type) {
1046 case _HighlightType.pressed:
1047 widget.onHighlightChanged?.call(value);
1048 case _HighlightType.hover:
1049 if (callOnHover) {
1050 widget.onHover?.call(value);
1051 }
1052 case _HighlightType.focus:
1053 break;
1054 }
1055 }
1056
1057 void _updateHighlightsAndSplashes() {
1058 for (final InkHighlight? highlight in _highlights.values) {
1059 highlight?.customBorder = widget.customBorder;
1060 }
1061 _currentSplash?.customBorder = widget.customBorder;
1062
1063 if (_splashes != null && _splashes!.isNotEmpty) {
1064 for (final InteractiveInkFeature inkFeature in _splashes!) {
1065 inkFeature.customBorder = widget.customBorder;
1066 }
1067 }
1068 }
1069
1070 InteractiveInkFeature _createSplash(Offset globalPosition) {
1071 final MaterialInkController inkController = Material.of(context);
1072 final RenderBox referenceBox = context.findRenderObject()! as RenderBox;
1073 final Offset position = referenceBox.globalToLocal(globalPosition);
1074 final Color color =
1075 widget.overlayColor?.resolve(statesController.value) ??
1076 widget.splashColor ??
1077 Theme.of(context).splashColor;
1078 final RectCallback? rectCallback = widget.containedInkWell
1079 ? widget.getRectCallback!(referenceBox)
1080 : null;
1081 final BorderRadius? borderRadius = widget.borderRadius;
1082 final ShapeBorder? customBorder = widget.customBorder;
1083
1084 InteractiveInkFeature? splash;
1085 void onRemoved() {
1086 if (_splashes != null) {
1087 assert(_splashes!.contains(splash));
1088 _splashes!.remove(splash);
1089 if (_currentSplash == splash) {
1090 _currentSplash = null;
1091 }
1092 updateKeepAlive();
1093 } // else we're probably in deactivate()
1094 }
1095
1096 splash = (widget.splashFactory ?? Theme.of(context).splashFactory).create(
1097 controller: inkController,
1098 referenceBox: referenceBox,
1099 position: position,
1100 color: color,
1101 containedInkWell: widget.containedInkWell,
1102 rectCallback: rectCallback,
1103 radius: widget.radius,
1104 borderRadius: borderRadius,
1105 customBorder: customBorder,
1106 onRemoved: onRemoved,
1107 textDirection: Directionality.of(context),
1108 );
1109
1110 return splash;
1111 }
1112
1113 void handleFocusHighlightModeChange(FocusHighlightMode mode) {
1114 if (!mounted) {
1115 return;
1116 }
1117 setState(() {
1118 updateFocusHighlights();
1119 });
1120 }
1121
1122 bool get _shouldShowFocus => switch (MediaQuery.maybeNavigationModeOf(context)) {
1123 NavigationMode.traditional || null => enabled && _hasFocus,
1124 NavigationMode.directional => _hasFocus,
1125 };
1126
1127 void updateFocusHighlights() {
1128 final bool showFocus = switch (FocusManager.instance.highlightMode) {
1129 FocusHighlightMode.touch => false,
1130 FocusHighlightMode.traditional => _shouldShowFocus,
1131 };
1132 updateHighlight(_HighlightType.focus, value: showFocus);
1133 }
1134
1135 bool _hasFocus = false;
1136 void handleFocusUpdate(bool hasFocus) {
1137 _hasFocus = hasFocus;
1138 // Set here rather than updateHighlight because this widget's
1139 // (MaterialState) states include MaterialState.focused if
1140 // the InkWell _has_ the focus, rather than if it's showing
1141 // the focus per FocusManager.instance.highlightMode.
1142 statesController.update(MaterialState.focused, hasFocus);
1143 updateFocusHighlights();
1144 widget.onFocusChange?.call(hasFocus);
1145 }
1146
1147 void handleAnyTapDown(TapDownDetails details) {
1148 if (_anyChildInkResponsePressed) {
1149 return;
1150 }
1151 _startNewSplash(details: details);
1152 }
1153
1154 void handleTapDown(TapDownDetails details) {
1155 handleAnyTapDown(details);
1156 widget.onTapDown?.call(details);
1157 }
1158
1159 void handleTapUp(TapUpDetails details) {
1160 widget.onTapUp?.call(details);
1161 }
1162
1163 void handleSecondaryTapDown(TapDownDetails details) {
1164 handleAnyTapDown(details);
1165 widget.onSecondaryTapDown?.call(details);
1166 }
1167
1168 void handleSecondaryTapUp(TapUpDetails details) {
1169 widget.onSecondaryTapUp?.call(details);
1170 }
1171
1172 void _startNewSplash({TapDownDetails? details, BuildContext? context}) {
1173 assert(details != null || context != null);
1174
1175 final Offset globalPosition;
1176 if (context != null) {
1177 final RenderBox referenceBox = context.findRenderObject()! as RenderBox;
1178 assert(
1179 referenceBox.hasSize,
1180 'InkResponse must be done with layout before starting a splash.',
1181 );
1182 globalPosition = referenceBox.localToGlobal(referenceBox.paintBounds.center);
1183 } else {
1184 globalPosition = details!.globalPosition;
1185 }
1186 statesController.update(MaterialState.pressed, true); // ... before creating the splash
1187 final InteractiveInkFeature splash = _createSplash(globalPosition);
1188 _splashes ??= HashSet<InteractiveInkFeature>();
1189 _splashes!.add(splash);
1190 _currentSplash?.cancel();
1191 _currentSplash = splash;
1192 updateKeepAlive();
1193 updateHighlight(_HighlightType.pressed, value: true);
1194 }
1195
1196 void handleTap() {
1197 _currentSplash?.confirm();
1198 _currentSplash = null;
1199 updateHighlight(_HighlightType.pressed, value: false);
1200 if (widget.onTap != null) {
1201 if (widget.enableFeedback) {
1202 Feedback.forTap(context);
1203 }
1204 widget.onTap?.call();
1205 }
1206 }
1207
1208 void handleTapCancel() {
1209 _currentSplash?.cancel();
1210 _currentSplash = null;
1211 widget.onTapCancel?.call();
1212 updateHighlight(_HighlightType.pressed, value: false);
1213 }
1214
1215 void handleDoubleTap() {
1216 _currentSplash?.confirm();
1217 _currentSplash = null;
1218 updateHighlight(_HighlightType.pressed, value: false);
1219 widget.onDoubleTap?.call();
1220 }
1221
1222 void handleLongPress() {
1223 _currentSplash?.confirm();
1224 _currentSplash = null;
1225 if (widget.onLongPress != null) {
1226 if (widget.enableFeedback) {
1227 Feedback.forLongPress(context);
1228 }
1229 widget.onLongPress!();
1230 }
1231 }
1232
1233 void handleSecondaryTap() {
1234 _currentSplash?.confirm();
1235 _currentSplash = null;
1236 updateHighlight(_HighlightType.pressed, value: false);
1237 widget.onSecondaryTap?.call();
1238 }
1239
1240 void handleSecondaryTapCancel() {
1241 _currentSplash?.cancel();
1242 _currentSplash = null;
1243 widget.onSecondaryTapCancel?.call();
1244 updateHighlight(_HighlightType.pressed, value: false);
1245 }
1246
1247 @override
1248 void deactivate() {
1249 if (_splashes != null) {
1250 final Set<InteractiveInkFeature> splashes = _splashes!;
1251 _splashes = null;
1252 for (final InteractiveInkFeature splash in splashes) {
1253 splash.dispose();
1254 }
1255 _currentSplash = null;
1256 }
1257 assert(_currentSplash == null);
1258 for (final _HighlightType highlight in _highlights.keys) {
1259 _highlights[highlight]?.dispose();
1260 _highlights[highlight] = null;
1261 }
1262 widget.parentState?.markChildInkResponsePressed(this, false);
1263 super.deactivate();
1264 }
1265
1266 bool isWidgetEnabled(_InkResponseStateWidget widget) {
1267 return _primaryButtonEnabled(widget) || _secondaryButtonEnabled(widget);
1268 }
1269
1270 bool _primaryButtonEnabled(_InkResponseStateWidget widget) {
1271 return widget.onTap != null ||
1272 widget.onDoubleTap != null ||
1273 widget.onLongPress != null ||
1274 widget.onTapUp != null ||
1275 widget.onTapDown != null;
1276 }
1277
1278 bool _secondaryButtonEnabled(_InkResponseStateWidget widget) {
1279 return widget.onSecondaryTap != null ||
1280 widget.onSecondaryTapUp != null ||
1281 widget.onSecondaryTapDown != null;
1282 }
1283
1284 bool get enabled => isWidgetEnabled(widget);
1285 bool get _primaryEnabled => _primaryButtonEnabled(widget);
1286 bool get _secondaryEnabled => _secondaryButtonEnabled(widget);
1287
1288 void handleMouseEnter(PointerEnterEvent event) {
1289 _hovering = true;
1290 if (enabled) {
1291 handleHoverChange();
1292 }
1293 }
1294
1295 void handleMouseExit(PointerExitEvent event) {
1296 _hovering = false;
1297 // If the exit occurs after we've been disabled, we still
1298 // want to take down the highlights and run widget.onHover.
1299 handleHoverChange();
1300 }
1301
1302 void handleHoverChange() {
1303 updateHighlight(_HighlightType.hover, value: _hovering);
1304 }
1305
1306 bool get _canRequestFocus => switch (MediaQuery.maybeNavigationModeOf(context)) {
1307 NavigationMode.traditional || null => enabled && widget.canRequestFocus,
1308 NavigationMode.directional => true,
1309 };
1310
1311 @override
1312 Widget build(BuildContext context) {
1313 assert(widget.debugCheckContext(context));
1314 super.build(context); // See AutomaticKeepAliveClientMixin.
1315
1316 final ThemeData theme = Theme.of(context);
1317 const Set<MaterialState> highlightableStates = <MaterialState>{
1318 MaterialState.focused,
1319 MaterialState.hovered,
1320 MaterialState.pressed,
1321 };
1322 final Set<MaterialState> nonHighlightableStates = statesController.value.difference(
1323 highlightableStates,
1324 );
1325 // Each highlightable state will be resolved separately to get the corresponding color.
1326 // For this resolution to be correct, the non-highlightable states should be preserved.
1327 final Set<MaterialState> pressed = <MaterialState>{
1328 ...nonHighlightableStates,
1329 MaterialState.pressed,
1330 };
1331 final Set<MaterialState> focused = <MaterialState>{
1332 ...nonHighlightableStates,
1333 MaterialState.focused,
1334 };
1335 final Set<MaterialState> hovered = <MaterialState>{
1336 ...nonHighlightableStates,
1337 MaterialState.hovered,
1338 };
1339
1340 Color getHighlightColorForType(_HighlightType type) {
1341 return switch (type) {
1342 // The pressed state triggers a ripple (ink splash), per the current
1343 // Material Design spec. A separate highlight is no longer used.
1344 // See https://material.io/design/interaction/states.html#pressed
1345 _HighlightType.pressed =>
1346 widget.overlayColor?.resolve(pressed) ?? widget.highlightColor ?? theme.highlightColor,
1347 _HighlightType.focus =>
1348 widget.overlayColor?.resolve(focused) ?? widget.focusColor ?? theme.focusColor,
1349 _HighlightType.hover =>
1350 widget.overlayColor?.resolve(hovered) ?? widget.hoverColor ?? theme.hoverColor,
1351 };
1352 }
1353
1354 for (final _HighlightType type in _highlights.keys) {
1355 _highlights[type]?.color = getHighlightColorForType(type);
1356 }
1357
1358 _currentSplash?.color =
1359 widget.overlayColor?.resolve(statesController.value) ??
1360 widget.splashColor ??
1361 Theme.of(context).splashColor;
1362
1363 final MouseCursor effectiveMouseCursor = MaterialStateProperty.resolveAs<MouseCursor>(
1364 widget.mouseCursor ?? MaterialStateMouseCursor.clickable,
1365 statesController.value,
1366 );
1367
1368 return _ParentInkResponseProvider(
1369 state: this,
1370 child: Actions(
1371 actions: _actionMap,
1372 child: Focus(
1373 focusNode: widget.focusNode,
1374 canRequestFocus: _canRequestFocus,
1375 onFocusChange: handleFocusUpdate,
1376 autofocus: widget.autofocus,
1377 child: MouseRegion(
1378 cursor: effectiveMouseCursor,
1379 onEnter: handleMouseEnter,
1380 onExit: handleMouseExit,
1381 child: DefaultSelectionStyle.merge(
1382 mouseCursor: effectiveMouseCursor,
1383 child: Semantics(
1384 onTap: widget.excludeFromSemantics || widget.onTap == null ? null : simulateTap,
1385 onLongPress: widget.excludeFromSemantics || widget.onLongPress == null
1386 ? null
1387 : simulateLongPress,
1388 child: GestureDetector(
1389 onTapDown: _primaryEnabled ? handleTapDown : null,
1390 onTapUp: _primaryEnabled ? handleTapUp : null,
1391 onTap: _primaryEnabled ? handleTap : null,
1392 onTapCancel: _primaryEnabled ? handleTapCancel : null,
1393 onDoubleTap: widget.onDoubleTap != null ? handleDoubleTap : null,
1394 onLongPress: widget.onLongPress != null ? handleLongPress : null,
1395 onSecondaryTapDown: _secondaryEnabled ? handleSecondaryTapDown : null,
1396 onSecondaryTapUp: _secondaryEnabled ? handleSecondaryTapUp : null,
1397 onSecondaryTap: _secondaryEnabled ? handleSecondaryTap : null,
1398 onSecondaryTapCancel: _secondaryEnabled ? handleSecondaryTapCancel : null,
1399 behavior: HitTestBehavior.opaque,
1400 excludeFromSemantics: true,
1401 child: widget.child,
1402 ),
1403 ),
1404 ),
1405 ),
1406 ),
1407 ),
1408 );
1409 }
1410}
1411
1412/// A rectangular area of a [Material] that responds to touch.
1413///
1414/// For a variant of this widget that does not clip splashes, see [InkResponse].
1415///
1416/// The following diagram shows how an [InkWell] looks when tapped, when using
1417/// default values.
1418///
1419/// ![The highlight is a rectangle the size of the box.](https://flutter.github.io/assets-for-api-docs/assets/material/ink_well.png)
1420///
1421/// The [InkWell] widget must have a [Material] widget as an ancestor. The
1422/// [Material] widget is where the ink reactions are actually painted. This
1423/// matches the Material Design premise wherein the [Material] is what is
1424/// actually reacting to touches by spreading ink.
1425///
1426/// If a Widget uses this class directly, it should include the following line
1427/// at the top of its build function to call [debugCheckHasMaterial]:
1428///
1429/// ```dart
1430/// assert(debugCheckHasMaterial(context));
1431/// ```
1432///
1433/// ## Troubleshooting
1434///
1435/// ### The ink splashes aren't visible!
1436///
1437/// If there is an opaque graphic, e.g. painted using a [Container], [Image], or
1438/// [DecoratedBox], between the [Material] widget and the [InkWell] widget, then
1439/// the splash won't be visible because it will be under the opaque graphic.
1440/// This is because ink splashes draw on the underlying [Material] itself, as
1441/// if the ink was spreading inside the material.
1442///
1443/// The [Ink] widget can be used as a replacement for [Image], [Container], or
1444/// [DecoratedBox] to ensure that the image or decoration also paints in the
1445/// [Material] itself, below the ink.
1446///
1447/// If this is not possible for some reason, e.g. because you are using an
1448/// opaque [CustomPaint] widget, alternatively consider using a second
1449/// [Material] above the opaque widget but below the [InkWell] (as an
1450/// ancestor to the ink well). The [MaterialType.transparency] material
1451/// kind can be used for this purpose.
1452///
1453/// ### InkWell isn't clipping properly
1454///
1455/// If you want to clip an InkWell or any [Ink] widgets you need to keep in mind
1456/// that the [Material] that the Ink will be printed on is responsible for clipping.
1457/// This means you can't wrap the [Ink] widget in a clipping widget directly,
1458/// since this will leave the [Material] not clipped (and by extension the printed
1459/// [Ink] widgets as well).
1460///
1461/// An easy solution is to deliberately wrap the [Ink] widgets you want to clip
1462/// in a [Material], and wrap that in a clipping widget instead. See [Ink] for
1463/// an example.
1464///
1465/// ### The ink splashes don't track the size of an animated container
1466/// If the size of an InkWell's [Material] ancestor changes while the InkWell's
1467/// splashes are expanding, you may notice that the splashes aren't clipped
1468/// correctly. This can't be avoided.
1469///
1470/// An example of this situation is as follows:
1471///
1472/// {@tool dartpad}
1473/// Tap the container to cause it to grow. Then, tap it again and hold before
1474/// the widget reaches its maximum size to observe the clipped ink splash.
1475///
1476/// ** See code in examples/api/lib/material/ink_well/ink_well.0.dart **
1477/// {@end-tool}
1478///
1479/// An InkWell's splashes will not properly update to conform to changes if the
1480/// size of its underlying [Material], where the splashes are rendered, changes
1481/// during animation. You should avoid using InkWells within [Material] widgets
1482/// that are changing size.
1483///
1484/// See also:
1485///
1486/// * [GestureDetector], for listening for gestures without ink splashes.
1487/// * [ElevatedButton] and [TextButton], two kinds of buttons in Material Design.
1488/// * [InkResponse], a variant of [InkWell] that doesn't force a rectangular
1489/// shape on the ink reaction.
1490class InkWell extends InkResponse {
1491 /// Creates an ink well.
1492 ///
1493 /// Must have an ancestor [Material] widget in which to cause ink reactions.
1494 const InkWell({
1495 super.key,
1496 super.child,
1497 super.onTap,
1498 super.onDoubleTap,
1499 super.onLongPress,
1500 super.onTapDown,
1501 super.onTapUp,
1502 super.onTapCancel,
1503 super.onSecondaryTap,
1504 super.onSecondaryTapUp,
1505 super.onSecondaryTapDown,
1506 super.onSecondaryTapCancel,
1507 super.onHighlightChanged,
1508 super.onHover,
1509 super.mouseCursor,
1510 super.focusColor,
1511 super.hoverColor,
1512 super.highlightColor,
1513 super.overlayColor,
1514 super.splashColor,
1515 super.splashFactory,
1516 super.radius,
1517 super.borderRadius,
1518 super.customBorder,
1519 super.enableFeedback,
1520 super.excludeFromSemantics,
1521 super.focusNode,
1522 super.canRequestFocus,
1523 super.onFocusChange,
1524 super.autofocus,
1525 super.statesController,
1526 super.hoverDuration,
1527 }) : super(containedInkWell: true, highlightShape: BoxShape.rectangle);
1528}
1529