// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:collection'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'debug.dart'; import 'feedback.dart'; import 'ink_highlight.dart'; import 'material.dart'; import 'theme.dart'; /// 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 /// that's canceled when the pointer is dragged out of the reference /// 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({ @required MaterialInkController controller, @required RenderBox referenceBox, Color color, VoidCallback onRemoved, }) : 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. void confirm() { } /// 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. void cancel() { } /// The ink's color. Color get color => _color; Color _color; set color(Color value) { if (value == _color) return; _color = value; controller.markNeedsPaint(); } /// 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. /// /// Clipping can happen in 3 different ways - /// 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({ @required Canvas canvas, @required Matrix4 transform, @required Paint paint, @required Offset center, @required double radius, TextDirection textDirection, ShapeBorder customBorder, BorderRadius borderRadius = BorderRadius.zero, RectCallback clipCallback, }) { assert(canvas != null); assert(transform != null); assert(paint != null); assert(center != null); assert(radius != null); assert(borderRadius != null); final Offset originOffset = MatrixUtils.getAsTranslation(transform); 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(); } } /// An encapsulation of an [InteractiveInkFeature] constructor used by /// [InkWell], [InkResponse], and [ThemeData]. /// /// 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 { /// Subclasses should provide a const constructor. const InteractiveInkFeatureFactory(); /// The factory method. /// /// Subclasses should override this method to return a new instance of an /// [InteractiveInkFeature]. InteractiveInkFeature create({ @required MaterialInkController controller, @required RenderBox referenceBox, @required Offset position, @required Color color, @required TextDirection textDirection, bool containedInkWell = false, RectCallback rectCallback, BorderRadius borderRadius, ShapeBorder customBorder, double radius, VoidCallback onRemoved, }); } /// An area of a [Material] that responds to touch. Has a configurable shape and /// can be configured to clip splashes that extend outside its bounds or not. /// /// For a variant of this widget that is specialized for rectangular areas that /// always clip splashes, see [InkWell]. /// /// 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. /// /// The following two diagrams show how [InkResponse] looks when tapped if the /// [highlightShape] is [BoxShape.circle] (the default) and [containedInkWell] /// is false (also the default). /// /// The first diagram shows how it looks if the [InkResponse] is relatively /// large: /// /// ![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) /// /// The second diagram shows how it looks if the [InkResponse] is small: /// /// ![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) /// /// The main thing to notice from these diagrams is that the splashes happily /// exceed the bounds of the widget (because [containedInkWell] is false). /// /// 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]. /// /// ![The highlight is a rectangle the size of the box.](https://flutter.github.io/assets-for-api-docs/assets/material/ink_well.png) /// /// The [InkResponse] widget must have a [Material] widget as an ancestor. The /// [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. /// /// If a Widget uses this class directly, it should include the following line /// at the top of its build function to call [debugCheckHasMaterial]: /// /// ```dart /// assert(debugCheckHasMaterial(context)); /// ``` /// /// ## 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, /// 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. /// /// See also: /// /// * [GestureDetector], for listening for gestures without ink splashes. /// * [RaisedButton] and [FlatButton], two kinds of buttons in material design. /// * [IconButton], which combines [InkResponse] with an [Icon]. class InkResponse extends StatefulWidget { /// Creates an area of a [Material] that responds to touch. /// /// Must have an ancestor [Material] widget in which to cause ink reactions. /// /// The [containedInkWell], [highlightShape], [enableFeedback], and /// [excludeFromSemantics] arguments must not be null. const InkResponse({ Key key, this.child, this.onTap, this.onTapDown, this.onTapCancel, this.onDoubleTap, this.onLongPress, this.onHighlightChanged, this.onHover, this.containedInkWell = false, this.highlightShape = BoxShape.circle, this.radius, this.borderRadius, this.customBorder, this.focusColor, this.hoverColor, this.highlightColor, this.splashColor, this.splashFactory, this.enableFeedback = true, this.excludeFromSemantics = false, this.focusNode, this.canRequestFocus = true, this.onFocusChange, this.autofocus = false, }) : assert(containedInkWell != null), assert(highlightShape != null), assert(enableFeedback != null), assert(excludeFromSemantics != null), assert(autofocus != null), assert(canRequestFocus != null), super(key: key); /// The widget below this widget in the tree. /// /// {@macro flutter.widgets.child} final Widget child; /// Called when the user taps this part of the material. final GestureTapCallback onTap; /// Called when the user taps down this part of the material. final GestureTapDownCallback onTapDown; /// Called when the user cancels a tap that was started on this part of the /// material. final GestureTapCallback onTapCancel; /// Called when the user double taps this part of the material. final GestureTapCallback onDoubleTap; /// Called when the user long-presses on this part of the material. final GestureLongPressCallback onLongPress; /// Called when this part of the material either becomes highlighted or stops /// being highlighted. /// /// The value passed to the callback is true if this part of the material has /// become highlighted and false if this part of the material has stopped /// being highlighted. /// /// 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. final ValueChanged<bool> onHighlightChanged; /// 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. final ValueChanged<bool> onHover; /// Whether this ink response should be clipped its bounds. /// /// 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: /// /// * [highlightShape], the shape of the focus, hover, and pressed /// highlights. /// * [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. final bool containedInkWell; /// The shape (e.g., circle, rectangle) to use for the highlight drawn around /// 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]). /// /// 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. final BoxShape highlightShape; /// The radius of the ink splash. /// /// 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. /// * [splashFactory], which defines the appearance of the splash. final double radius; /// The clipping radius of the containing rect. This is effective only if /// [customBorder] is null. /// /// If this is null, it is interpreted as [BorderRadius.zero]. final BorderRadius borderRadius; /// The custom clip border which overrides [borderRadius]. final ShapeBorder customBorder; /// 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. final Color focusColor; /// 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. final Color hoverColor; /// 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. /// /// See also: /// /// * [hoverColor], the color of the hover highlight. /// * [focusColor], the color of the focus highlight. /// * [highlightShape], the shape of the focus, hover, and pressed /// highlights. /// * [splashColor], the color of the splash. /// * [splashFactory], which defines the appearance of the splash. final Color highlightColor; /// The splash color of the ink response. If this property is null then the /// splash color of the theme, [ThemeData.splashColor], will be used. /// /// See also: /// /// * [splashFactory], which defines the appearance of the splash. /// * [radius], the (maximum) size of the ink splash. /// * [highlightColor], the color of the highlight. final Color splashColor; /// 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 /// more aggressively than the default. final InteractiveInkFeatureFactory splashFactory; /// 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; /// 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; /// Handler called when the focus changes. /// /// Called with true if this widget's node gains focus, and false if it loses /// focus. final ValueChanged<bool> onFocusChange; /// {@macro flutter.widgets.Focus.autofocus} final bool autofocus; /// {@macro flutter.widgets.Focus.focusNode} final FocusNode focusNode; /// {@template flutter.widgets.Focus.canRequestFocus} final bool canRequestFocus; /// 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, /// [TableRowInkWell] implements this method to return the rectangle /// corresponding to the row that the widget is in. /// /// The default behavior returns null, which is equivalent to /// returning the referenceBox argument's bounding box (though /// slightly more efficient). RectCallback getRectCallback(RenderBox referenceBox) => null; /// Asserts that the given context satisfies the prerequisites for /// this class. /// /// This method is intended to be overridden by descendants that /// specialize [InkResponse] for unusual cases. For example, /// [TableRowInkWell] implements this method to verify that the widget is /// in a table. @mustCallSuper bool debugCheckContext(BuildContext context) { assert(debugCheckHasMaterial(context)); assert(debugCheckHasDirectionality(context)); return true; } @override _InkResponseState<InkResponse> createState() => _InkResponseState<InkResponse>(); @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); final List<String> gestures = <String>[ if (onTap != null) 'tap', if (onDoubleTap != null) 'double tap', if (onLongPress != null) 'long press', if (onTapDown != null) 'tap down', if (onTapCancel != null) 'tap cancel', ]; properties.add(IterableProperty<String>('gestures', gestures, ifEmpty: '<none>')); properties.add(DiagnosticsProperty<bool>('containedInkWell', containedInkWell, level: DiagnosticLevel.fine)); properties.add(DiagnosticsProperty<BoxShape>( 'highlightShape', highlightShape, description: '${containedInkWell ? "clipped to " : ""}$highlightShape', showName: false, )); } } /// Used to index the allocated highlights for the different types of highlights /// in [_InkResponseState]. enum _HighlightType { pressed, hover, focus, } class _InkResponseState<T extends InkResponse> extends State<T> with AutomaticKeepAliveClientMixin<T> { Set<InteractiveInkFeature> _splashes; InteractiveInkFeature _currentSplash; bool _hovering = false; final Map<_HighlightType, InkHighlight> _highlights = <_HighlightType, InkHighlight>{}; Map<LocalKey, ActionFactory> _actionMap; bool get highlightsExist => _highlights.values.where((InkHighlight highlight) => highlight != null).isNotEmpty; void _handleAction(FocusNode node, Intent intent) { _startSplash(context: node.context); _handleTap(node.context); } Action _createAction() { return CallbackAction( ActivateAction.key, onInvoke: _handleAction, ); } @override void initState() { super.initState(); _actionMap = <LocalKey, ActionFactory>{ ActivateAction.key: _createAction, }; FocusManager.instance.addHighlightModeListener(_handleFocusHighlightModeChange); } @override void didUpdateWidget(T oldWidget) { super.didUpdateWidget(oldWidget); if (_isWidgetEnabled(widget) != _isWidgetEnabled(oldWidget)) { _handleHoverChange(_hovering); _updateFocusHighlights(); } } @override void dispose() { FocusManager.instance.removeHighlightModeListener(_handleFocusHighlightModeChange); super.dispose(); } @override bool get wantKeepAlive => highlightsExist || (_splashes != null && _splashes.isNotEmpty); Color getHighlightColorForType(_HighlightType type) { switch (type) { case _HighlightType.pressed: return widget.highlightColor ?? Theme.of(context).highlightColor; case _HighlightType.focus: return widget.focusColor ?? Theme.of(context).focusColor; case _HighlightType.hover: return widget.hoverColor ?? Theme.of(context).hoverColor; } assert(false, 'Unhandled $_HighlightType $type'); return null; } 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); } assert(false, 'Unhandled $_HighlightType $type'); return null; } void updateHighlight(_HighlightType type, {@required bool value}) { final InkHighlight highlight = _highlights[type]; void handleInkRemoval() { assert(_highlights[type] != null); _highlights[type] = null; updateKeepAlive(); } if (value == (highlight != null && highlight.active)) return; if (value) { if (highlight == null) { final RenderBox referenceBox = context.findRenderObject() as RenderBox; _highlights[type] = InkHighlight( controller: Material.of(context), referenceBox: referenceBox, color: getHighlightColorForType(type), shape: widget.highlightShape, borderRadius: widget.borderRadius, customBorder: widget.customBorder, rectCallback: widget.getRectCallback(referenceBox), onRemoved: handleInkRemoval, textDirection: Directionality.of(context), fadeDuration: getFadeDurationForType(type), ); updateKeepAlive(); } else { highlight.activate(); } } else { highlight.deactivate(); } assert(value == (_highlights[type] != null && _highlights[type].active)); switch (type) { case _HighlightType.pressed: if (widget.onHighlightChanged != null) widget.onHighlightChanged(value); break; case _HighlightType.hover: if (widget.onHover != null) widget.onHover(value); break; case _HighlightType.focus: break; } } InteractiveInkFeature _createInkFeature(Offset globalPosition) { final MaterialInkController inkController = Material.of(context); final RenderBox referenceBox = context.findRenderObject() as RenderBox; final Offset position = referenceBox.globalToLocal(globalPosition); final Color color = widget.splashColor ?? Theme.of(context).splashColor; final RectCallback rectCallback = widget.containedInkWell ? widget.getRectCallback(referenceBox) : null; final BorderRadius borderRadius = widget.borderRadius; final ShapeBorder customBorder = widget.customBorder; InteractiveInkFeature splash; void onRemoved() { if (_splashes != null) { assert(_splashes.contains(splash)); _splashes.remove(splash); if (_currentSplash == splash) _currentSplash = null; updateKeepAlive(); } // else we're probably in deactivate() } splash = (widget.splashFactory ?? Theme.of(context).splashFactory).create( controller: inkController, referenceBox: referenceBox, position: position, color: color, containedInkWell: widget.containedInkWell, rectCallback: rectCallback, radius: widget.radius, borderRadius: borderRadius, customBorder: customBorder, onRemoved: onRemoved, textDirection: Directionality.of(context), ); return splash; } void _handleFocusHighlightModeChange(FocusHighlightMode mode) { if (!mounted) { return; } setState(() { _updateFocusHighlights(); }); } void _updateFocusHighlights() { bool showFocus; switch (FocusManager.instance.highlightMode) { case FocusHighlightMode.touch: showFocus = false; break; case FocusHighlightMode.traditional: showFocus = enabled && _hasFocus; break; } updateHighlight(_HighlightType.focus, value: showFocus); } bool _hasFocus = false; void _handleFocusUpdate(bool hasFocus) { _hasFocus = hasFocus; _updateFocusHighlights(); if (widget.onFocusChange != null) { widget.onFocusChange(hasFocus); } } void _handleTapDown(TapDownDetails details) { _startSplash(details: details); if (widget.onTapDown != null) { widget.onTapDown(details); } } void _startSplash({TapDownDetails details, BuildContext context}) { assert(details != null || context != null); Offset globalPosition; if (context != null) { final RenderBox referenceBox = context.findRenderObject() as RenderBox; assert(referenceBox.hasSize, 'InkResponse must be done with layout before starting a splash.'); globalPosition = referenceBox.localToGlobal(referenceBox.paintBounds.center); } else { globalPosition = details.globalPosition; } final InteractiveInkFeature splash = _createInkFeature(globalPosition); _splashes ??= HashSet<InteractiveInkFeature>(); _splashes.add(splash); _currentSplash = splash; updateKeepAlive(); updateHighlight(_HighlightType.pressed, value: true); } void _handleTap(BuildContext context) { _currentSplash?.confirm(); _currentSplash = null; updateHighlight(_HighlightType.pressed, value: false); if (widget.onTap != null) { if (widget.enableFeedback) Feedback.forTap(context); widget.onTap(); } } void _handleTapCancel() { _currentSplash?.cancel(); _currentSplash = null; if (widget.onTapCancel != null) { widget.onTapCancel(); } updateHighlight(_HighlightType.pressed, value: false); } void _handleDoubleTap() { _currentSplash?.confirm(); _currentSplash = null; if (widget.onDoubleTap != null) widget.onDoubleTap(); } void _handleLongPress(BuildContext context) { _currentSplash?.confirm(); _currentSplash = null; if (widget.onLongPress != null) { if (widget.enableFeedback) Feedback.forLongPress(context); widget.onLongPress(); } } @override void deactivate() { 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; } super.deactivate(); } bool _isWidgetEnabled(InkResponse widget) { return widget.onTap != null || widget.onDoubleTap != null || widget.onLongPress != null; } bool get enabled => _isWidgetEnabled(widget); void _handleMouseEnter(PointerEnterEvent event) => _handleHoverChange(true); void _handleMouseExit(PointerExitEvent event) => _handleHoverChange(false); void _handleHoverChange(bool hovering) { if (_hovering != hovering) { _hovering = hovering; updateHighlight(_HighlightType.hover, value: enabled && _hovering); } } @override Widget build(BuildContext context) { assert(widget.debugCheckContext(context)); super.build(context); // See AutomaticKeepAliveClientMixin. for (final _HighlightType type in _highlights.keys) { _highlights[type]?.color = getHighlightColorForType(type); } _currentSplash?.color = widget.splashColor ?? Theme.of(context).splashColor; final bool canRequestFocus = enabled && widget.canRequestFocus; return Actions( actions: _actionMap, child: Focus( focusNode: widget.focusNode, canRequestFocus: canRequestFocus, onFocusChange: _handleFocusUpdate, autofocus: widget.autofocus, child: MouseRegion( onEnter: enabled ? _handleMouseEnter : null, onExit: enabled ? _handleMouseExit : null, child: GestureDetector( onTapDown: enabled ? _handleTapDown : null, onTap: enabled ? () => _handleTap(context) : null, onTapCancel: enabled ? _handleTapCancel : null, onDoubleTap: widget.onDoubleTap != null ? _handleDoubleTap : null, onLongPress: widget.onLongPress != null ? () => _handleLongPress(context) : null, behavior: HitTestBehavior.opaque, excludeFromSemantics: widget.excludeFromSemantics, child: widget.child, ), ), ), ); } } /// A rectangular area of a [Material] that responds to touch. /// /// 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. /// /// ![The highlight is a rectangle the size of the box.](https://flutter.github.io/assets-for-api-docs/assets/material/ink_well.png) /// /// The [InkWell] widget must have a [Material] widget as an ancestor. The /// [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. /// /// If a Widget uses this class directly, it should include the following line /// at the top of its build function to call [debugCheckHasMaterial]: /// /// ```dart /// assert(debugCheckHasMaterial(context)); /// ``` /// /// ## 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 /// 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. /// /// ### 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: /// /// {@tool dartpad --template=stateful_widget_scaffold_center} /// /// 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. /// /// ```dart /// double sideLength = 50; /// /// Widget build(BuildContext context) { /// return AnimatedContainer( /// height: sideLength, /// width: sideLength, /// duration: Duration(seconds: 2), /// curve: Curves.easeIn, /// child: Material( /// color: Colors.yellow, /// child: InkWell( /// onTap: () { /// setState(() { /// sideLength == 50 ? sideLength = 100 : sideLength = 50; /// }); /// }, /// ), /// ), /// ); /// } /// ``` /// {@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. /// /// See also: /// /// * [GestureDetector], for listening for gestures without ink splashes. /// * [RaisedButton] and [FlatButton], two kinds of buttons in material design. /// * [InkResponse], a variant of [InkWell] that doesn't force a rectangular /// shape on the ink reaction. class InkWell extends InkResponse { /// Creates an ink well. /// /// Must have an ancestor [Material] widget in which to cause ink reactions. /// /// The [enableFeedback] and [excludeFromSemantics] arguments must not be /// null. const InkWell({ Key key, Widget child, GestureTapCallback onTap, GestureTapCallback onDoubleTap, GestureLongPressCallback onLongPress, GestureTapDownCallback onTapDown, GestureTapCancelCallback onTapCancel, ValueChanged<bool> onHighlightChanged, ValueChanged<bool> onHover, Color focusColor, Color hoverColor, Color highlightColor, Color splashColor, InteractiveInkFeatureFactory splashFactory, double radius, BorderRadius borderRadius, ShapeBorder customBorder, bool enableFeedback = true, bool excludeFromSemantics = false, FocusNode focusNode, bool canRequestFocus = true, ValueChanged<bool> onFocusChange, bool autofocus = false, }) : super( key: key, child: child, onTap: onTap, onDoubleTap: onDoubleTap, onLongPress: onLongPress, onTapDown: onTapDown, onTapCancel: onTapCancel, onHighlightChanged: onHighlightChanged, onHover: onHover, containedInkWell: true, highlightShape: BoxShape.rectangle, focusColor: focusColor, hoverColor: hoverColor, highlightColor: highlightColor, splashColor: splashColor, splashFactory: splashFactory, radius: radius, borderRadius: borderRadius, customBorder: customBorder, enableFeedback: enableFeedback ?? true, excludeFromSemantics: excludeFromSemantics ?? false, focusNode: focusNode, canRequestFocus: canRequestFocus ?? true, onFocusChange: onFocusChange, autofocus: autofocus ?? false, ); }