ink_well.dart 25.9 KB
Newer Older
1 2 3 4
// Copyright 2015 The Chromium Authors. All rights reserved.
// 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 16
import 'material.dart';
import 'theme.dart';
17

18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
/// 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 cancelled 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.
50
  void confirm() { }
51 52 53 54 55

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

  /// The ink's color.
  Color get color => _color;
  Color _color;
  set color(Color value) {
    if (value == _color)
      return;
    _color = value;
    controller.markNeedsPaint();
  }
}

69 70
/// An encapsulation of an [InteractiveInkFeature] constructor used by
/// [InkWell], [InkResponse], and [ThemeData].
71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92
///
/// 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,
93
    @required TextDirection textDirection,
94
    bool containedInkWell = false,
95 96
    RectCallback rectCallback,
    BorderRadius borderRadius,
97
    ShapeBorder customBorder,
98 99 100 101 102
    double radius,
    VoidCallback onRemoved,
  });
}

103
/// An area of a [Material] that responds to touch. Has a configurable shape and
104 105
/// can be configured to clip splashes that extend outside its bounds or not.
///
106
/// For a variant of this widget that is specialized for rectangular areas that
107 108
/// always clip splashes, see [InkWell].
///
109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124
/// 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.
///
125 126
/// The following two diagrams show how [InkResponse] looks when tapped if the
/// [highlightShape] is [BoxShape.circle] (the default) and [containedInkWell]
127 128 129 130
/// is false (also the default).
///
/// The first diagram shows how it looks if the [InkResponse] is relatively
/// large:
131
///
132
/// ![The highlight is a disc centered in the box, smaller than the child widget.](https://flutter.github.io/assets-for-api-docs/assets/material/ink_response_large.png)
133 134 135
///
/// The second diagram shows how it looks if the [InkResponse] is small:
///
136
/// ![The highlight is a disc overflowing the box, centered on the child.](https://flutter.github.io/assets-for-api-docs/assets/material/ink_response_small.png)
137
///
138 139 140
/// The main thing to notice from these diagrams is that the splashes happily
/// exceed the bounds of the widget (because [containedInkWell] is false).
///
141 142 143 144
/// 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].
///
145
/// ![The highlight is a rectangle the size of the box.](https://flutter.github.io/assets-for-api-docs/assets/material/ink_well.png)
146
///
147
/// The [InkResponse] widget must have a [Material] widget as an ancestor. The
148 149 150
/// [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.
151 152
///
/// If a Widget uses this class directly, it should include the following line
153
/// at the top of its build function to call [debugCheckHasMaterial]:
154
///
155 156 157
/// ```dart
/// assert(debugCheckHasMaterial(context));
/// ```
Ian Hickson's avatar
Ian Hickson committed
158 159 160 161 162 163 164
///
/// ## 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,
165 166 167 168 169 170 171 172 173 174 175 176 177
/// 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.
178 179 180 181 182 183
///
/// 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].
184
class InkResponse extends StatefulWidget {
185 186 187
  /// 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
188 189 190
  ///
  /// The [containedInkWell], [highlightShape], [enableFeedback], and
  /// [excludeFromSemantics] arguments must not be null.
191
  const InkResponse({
192 193 194
    Key key,
    this.child,
    this.onTap,
195 196
    this.onTapDown,
    this.onTapCancel,
197
    this.onDoubleTap,
198
    this.onLongPress,
199
    this.onHighlightChanged,
200 201
    this.containedInkWell = false,
    this.highlightShape = BoxShape.circle,
202
    this.radius,
Ian Hickson's avatar
Ian Hickson committed
203
    this.borderRadius,
204
    this.customBorder,
205 206
    this.highlightColor,
    this.splashColor,
207
    this.splashFactory,
208 209
    this.enableFeedback = true,
    this.excludeFromSemantics = false,
Ian Hickson's avatar
Ian Hickson committed
210 211 212 213 214
  }) : assert(containedInkWell != null),
       assert(highlightShape != null),
       assert(enableFeedback != null),
       assert(excludeFromSemantics != null),
       super(key: key);
215

216
  /// The widget below this widget in the tree.
217 218
  ///
  /// {@macro flutter.widgets.child}
219
  final Widget child;
220

221
  /// Called when the user taps this part of the material.
222
  final GestureTapCallback onTap;
223

224 225 226 227 228 229 230
  /// 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;

231
  /// Called when the user double taps this part of the material.
232
  final GestureTapCallback onDoubleTap;
233

234
  /// Called when the user long-presses on this part of the material.
235
  final GestureLongPressCallback onLongPress;
236

237 238
  /// Called when this part of the material either becomes highlighted or stops
  /// being highlighted.
239 240 241 242
  ///
  /// 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.
243 244 245 246 247 248
  ///
  /// 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.
249
  final ValueChanged<bool> onHighlightChanged;
250

251
  /// Whether this ink response should be clipped its bounds.
252 253 254 255 256 257 258 259 260 261 262 263
  ///
  /// 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], which determines the shape of the highlight.
  ///  * [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.
264
  final bool containedInkWell;
265

266 267 268 269 270 271 272 273 274 275 276 277 278 279 280
  /// The shape (e.g., circle, rectangle) to use for the highlight drawn around
  /// this part of the material.
  ///
  /// 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.
281
  final BoxShape highlightShape;
282

283
  /// The radius of the ink splash.
284 285 286 287 288 289 290 291
  ///
  /// 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.
292
  ///  * [splashFactory], which defines the appearance of the splash.
293 294
  final double radius;

295 296
  /// The clipping radius of the containing rect. This is effective only if
  /// [customBorder] is null.
Ian Hickson's avatar
Ian Hickson committed
297 298
  ///
  /// If this is null, it is interpreted as [BorderRadius.zero].
299 300
  final BorderRadius borderRadius;

301 302 303
  /// The custom clip border which overrides [borderRadius].
  final ShapeBorder customBorder;

304
  /// The highlight color of the ink response. If this property is null then the
305
  /// highlight color of the theme, [ThemeData.highlightColor], will be used.
306 307 308 309 310
  ///
  /// See also:
  ///
  ///  * [highlightShape], the shape of the highlight.
  ///  * [splashColor], the color of the splash.
311
  ///  * [splashFactory], which defines the appearance of the splash.
312 313 314
  final Color highlightColor;

  /// The splash color of the ink response. If this property is null then the
315
  /// splash color of the theme, [ThemeData.splashColor], will be used.
316 317 318
  ///
  /// See also:
  ///
319
  ///  * [splashFactory], which defines the appearance of the splash.
320 321
  ///  * [radius], the (maximum) size of the ink splash.
  ///  * [highlightColor], the color of the highlight.
322 323
  final Color splashColor;

324 325 326 327 328 329 330 331 332 333 334
  /// 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
335
  ///    more aggressively than the default.
336 337
  final InteractiveInkFeatureFactory splashFactory;

338 339 340 341 342 343 344 345 346 347
  /// 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;

348 349 350 351 352 353 354 355 356
  /// 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;

357 358 359 360 361
  /// 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,
362
  /// [TableRowInkWell] implements this method to return the rectangle
363 364 365 366 367 368 369 370 371 372 373 374
  /// 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,
375
  /// [TableRowInkWell] implements this method to verify that the widget is
376
  /// in a table.
377
  @mustCallSuper
378 379
  bool debugCheckContext(BuildContext context) {
    assert(debugCheckHasMaterial(context));
380
    assert(debugCheckHasDirectionality(context));
381 382 383
    return true;
  }

384
  @override
385
  _InkResponseState<InkResponse> createState() => _InkResponseState<InkResponse>();
Ian Hickson's avatar
Ian Hickson committed
386 387

  @override
388 389
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
Ian Hickson's avatar
Ian Hickson committed
390 391 392 393 394 395 396
    final List<String> gestures = <String>[];
    if (onTap != null)
      gestures.add('tap');
    if (onDoubleTap != null)
      gestures.add('double tap');
    if (onLongPress != null)
      gestures.add('long press');
397 398 399 400
    if (onTapDown != null)
      gestures.add('tap down');
    if (onTapCancel != null)
      gestures.add('tap cancel');
401 402 403
    properties.add(IterableProperty<String>('gestures', gestures, ifEmpty: '<none>'));
    properties.add(DiagnosticsProperty<bool>('containedInkWell', containedInkWell, level: DiagnosticLevel.fine));
    properties.add(DiagnosticsProperty<BoxShape>(
404 405 406 407 408
      'highlightShape',
      highlightShape,
      description: '${containedInkWell ? "clipped to " : ""}$highlightShape',
      showName: false,
    ));
Ian Hickson's avatar
Ian Hickson committed
409
  }
410 411
}

412
class _InkResponseState<T extends InkResponse> extends State<T> with AutomaticKeepAliveClientMixin<T> {
413 414
  Set<InteractiveInkFeature> _splashes;
  InteractiveInkFeature _currentSplash;
415 416
  InkHighlight _lastHighlight;

417 418 419
  @override
  bool get wantKeepAlive => _lastHighlight != null || (_splashes != null && _splashes.isNotEmpty);

420 421 422 423 424
  void updateHighlight(bool value) {
    if (value == (_lastHighlight != null && _lastHighlight.active))
      return;
    if (value) {
      if (_lastHighlight == null) {
425
        final RenderBox referenceBox = context.findRenderObject();
426
        _lastHighlight = InkHighlight(
427
          controller: Material.of(context),
428
          referenceBox: referenceBox,
429 430 431
          color: widget.highlightColor ?? Theme.of(context).highlightColor,
          shape: widget.highlightShape,
          borderRadius: widget.borderRadius,
432
          customBorder: widget.customBorder,
433
          rectCallback: widget.getRectCallback(referenceBox),
434
          onRemoved: _handleInkHighlightRemoval,
435
          textDirection: Directionality.of(context),
436
        );
437
        updateKeepAlive();
438 439 440 441 442 443
      } else {
        _lastHighlight.activate();
      }
    } else {
      _lastHighlight.deactivate();
    }
444
    assert(value == (_lastHighlight != null && _lastHighlight.active));
445 446
    if (widget.onHighlightChanged != null)
      widget.onHighlightChanged(value);
447 448
  }

449 450 451 452 453 454
  void _handleInkHighlightRemoval() {
    assert(_lastHighlight != null);
    _lastHighlight = null;
    updateKeepAlive();
  }

455 456
  InteractiveInkFeature _createInkFeature(TapDownDetails details) {
    final MaterialInkController inkController = Material.of(context);
457
    final RenderBox referenceBox = context.findRenderObject();
458 459 460
    final Offset position = referenceBox.globalToLocal(details.globalPosition);
    final Color color = widget.splashColor ?? Theme.of(context).splashColor;
    final RectCallback rectCallback = widget.containedInkWell ? widget.getRectCallback(referenceBox) : null;
Ian Hickson's avatar
Ian Hickson committed
461
    final BorderRadius borderRadius = widget.borderRadius;
462
    final ShapeBorder customBorder = widget.customBorder;
463 464 465 466 467 468 469 470 471 472 473 474 475 476

    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,
477
      referenceBox: referenceBox,
478 479
      position: position,
      color: color,
480
      containedInkWell: widget.containedInkWell,
481
      rectCallback: rectCallback,
482
      radius: widget.radius,
483
      borderRadius: borderRadius,
484
      customBorder: customBorder,
485
      onRemoved: onRemoved,
486
      textDirection: Directionality.of(context),
487
    );
488 489 490 491 492 493

    return splash;
  }

  void _handleTapDown(TapDownDetails details) {
    final InteractiveInkFeature splash = _createInkFeature(details);
494
    _splashes ??= HashSet<InteractiveInkFeature>();
495 496
    _splashes.add(splash);
    _currentSplash = splash;
497 498 499
    if (widget.onTapDown != null) {
      widget.onTapDown(details);
    }
500
    updateKeepAlive();
501
    updateHighlight(true);
502 503
  }

504
  void _handleTap(BuildContext context) {
505 506
    _currentSplash?.confirm();
    _currentSplash = null;
507
    updateHighlight(false);
508 509 510
    if (widget.onTap != null) {
      if (widget.enableFeedback)
        Feedback.forTap(context);
511
      widget.onTap();
512
    }
513 514
  }

515 516 517
  void _handleTapCancel() {
    _currentSplash?.cancel();
    _currentSplash = null;
518 519 520
    if (widget.onTapCancel != null) {
      widget.onTapCancel();
    }
521
    updateHighlight(false);
522 523
  }

524 525 526
  void _handleDoubleTap() {
    _currentSplash?.confirm();
    _currentSplash = null;
527 528
    if (widget.onDoubleTap != null)
      widget.onDoubleTap();
529 530
  }

531
  void _handleLongPress(BuildContext context) {
532 533
    _currentSplash?.confirm();
    _currentSplash = null;
534 535 536
    if (widget.onLongPress != null) {
      if (widget.enableFeedback)
        Feedback.forLongPress(context);
537
      widget.onLongPress();
538
    }
539 540
  }

541
  @override
542 543
  void deactivate() {
    if (_splashes != null) {
544
      final Set<InteractiveInkFeature> splashes = _splashes;
545
      _splashes = null;
546
      for (InteractiveInkFeature splash in splashes)
547 548 549 550
        splash.dispose();
      _currentSplash = null;
    }
    assert(_currentSplash == null);
551 552
    _lastHighlight?.dispose();
    _lastHighlight = null;
553
    super.deactivate();
554 555
  }

556
  @override
557
  Widget build(BuildContext context) {
558
    assert(widget.debugCheckContext(context));
559
    super.build(context); // See AutomaticKeepAliveClientMixin.
560
    final ThemeData themeData = Theme.of(context);
561 562 563
    _lastHighlight?.color = widget.highlightColor ?? themeData.highlightColor;
    _currentSplash?.color = widget.splashColor ?? themeData.splashColor;
    final bool enabled = widget.onTap != null || widget.onDoubleTap != null || widget.onLongPress != null;
564
    return GestureDetector(
565
      onTapDown: enabled ? _handleTapDown : null,
566
      onTap: enabled ? () => _handleTap(context) : null,
567
      onTapCancel: enabled ? _handleTapCancel : null,
568
      onDoubleTap: widget.onDoubleTap != null ? _handleDoubleTap : null,
569
      onLongPress: widget.onLongPress != null ? () => _handleLongPress(context) : null,
570
      behavior: HitTestBehavior.opaque,
571 572
      child: widget.child,
      excludeFromSemantics: widget.excludeFromSemantics,
573
    );
574 575
  }

576
}
577

578
/// A rectangular area of a [Material] that responds to touch.
579
///
580 581 582 583 584
/// 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.
///
585
/// ![The highlight is a rectangle the size of the box.](https://flutter.github.io/assets-for-api-docs/assets/material/ink_well.png)
586
///
587
/// The [InkWell] widget must have a [Material] widget as an ancestor. The
588 589 590 591
/// [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.
///
592
/// If a Widget uses this class directly, it should include the following line
593
/// at the top of its build function to call [debugCheckHasMaterial]:
594
///
595 596 597 598
/// ```dart
/// assert(debugCheckHasMaterial(context));
/// ```
///
Ian Hickson's avatar
Ian Hickson committed
599 600 601 602 603 604
/// ## 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
605 606 607 608 609 610 611 612 613 614 615 616 617
/// 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
618
///
619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661
/// ### 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 snippet --template=stateful_widget_scaffold}
///
/// 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 Center(
///     child: 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.
///
662 663 664 665 666 667
/// 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.
668
class InkWell extends InkResponse {
669 670 671
  /// Creates an ink well.
  ///
  /// Must have an ancestor [Material] widget in which to cause ink reactions.
Ian Hickson's avatar
Ian Hickson committed
672 673 674
  ///
  /// The [enableFeedback] and [excludeFromSemantics] arguments must not be
  /// null.
675
  const InkWell({
676 677
    Key key,
    Widget child,
678
    GestureTapCallback onTap,
679
    GestureTapCallback onDoubleTap,
680
    GestureLongPressCallback onLongPress,
681 682
    GestureTapDownCallback onTapDown,
    GestureTapCancelCallback onTapCancel,
683 684 685
    ValueChanged<bool> onHighlightChanged,
    Color highlightColor,
    Color splashColor,
686 687
    InteractiveInkFeatureFactory splashFactory,
    double radius,
688
    BorderRadius borderRadius,
689
    ShapeBorder customBorder,
690 691
    bool enableFeedback = true,
    bool excludeFromSemantics = false,
692 693 694 695 696
  }) : super(
    key: key,
    child: child,
    onTap: onTap,
    onDoubleTap: onDoubleTap,
697
    onLongPress: onLongPress,
698 699
    onTapDown: onTapDown,
    onTapCancel: onTapCancel,
700
    onHighlightChanged: onHighlightChanged,
701
    containedInkWell: true,
702 703 704
    highlightShape: BoxShape.rectangle,
    highlightColor: highlightColor,
    splashColor: splashColor,
705 706
    splashFactory: splashFactory,
    radius: radius,
707
    borderRadius: borderRadius,
708
    customBorder: customBorder,
709
    enableFeedback: enableFeedback,
710
    excludeFromSemantics: excludeFromSemantics,
711
  );
712
}