1// Copyright 2015 The Chromium 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 5import 'dart:collection'; 6 7import 'package:flutter/foundation.dart'; 8import 'package:flutter/gestures.dart'; 9import 'package:flutter/rendering.dart'; 10import 'package:flutter/widgets.dart'; 11 12import 'debug.dart'; 13import 'feedback.dart'; 14import 'ink_highlight.dart'; 15import 'material.dart'; 16import 'theme.dart'; 17 18/// An ink feature that displays a [color] "splash" in response to a user 19/// gesture that can be confirmed or canceled. 20/// 21/// Subclasses call [confirm] when an input gesture is recognized. For 22/// example a press event might trigger an ink feature that's confirmed 23/// when the corresponding up event is seen. 24/// 25/// Subclasses call [cancel] when an input gesture is aborted before it 26/// is recognized. For example a press event might trigger an ink feature 27/// that's canceled when the pointer is dragged out of the reference 28/// box. 29/// 30/// The [InkWell] and [InkResponse] widgets generate instances of this 31/// class. 32abstract class InteractiveInkFeature extends InkFeature { 33 /// Creates an InteractiveInkFeature. 34 /// 35 /// The [controller] and [referenceBox] arguments must not be null. 36 InteractiveInkFeature({ 37 @required MaterialInkController controller, 38 @required RenderBox referenceBox, 39 Color color, 40 VoidCallback onRemoved, 41 }) : assert(controller != null), 42 assert(referenceBox != null), 43 _color = color, 44 super(controller: controller, referenceBox: referenceBox, onRemoved: onRemoved); 45 46 /// Called when the user input that triggered this feature's appearance was confirmed. 47 /// 48 /// Typically causes the ink to propagate faster across the material. By default this 49 /// method does nothing. 50 void confirm() { } 51 52 /// Called when the user input that triggered this feature's appearance was canceled. 53 /// 54 /// Typically causes the ink to gradually disappear. By default this method does 55 /// nothing. 56 void cancel() { } 57 58 /// The ink's color. 59 Color get color => _color; 60 Color _color; 61 set color(Color value) { 62 if (value == _color) 63 return; 64 _color = value; 65 controller.markNeedsPaint(); 66 } 67} 68 69/// An encapsulation of an [InteractiveInkFeature] constructor used by 70/// [InkWell], [InkResponse], and [ThemeData]. 71/// 72/// Interactive ink feature implementations should provide a static const 73/// `splashFactory` value that's an instance of this class. The `splashFactory` 74/// can be used to configure an [InkWell], [InkResponse] or [ThemeData]. 75/// 76/// See also: 77/// 78/// * [InkSplash.splashFactory] 79/// * [InkRipple.splashFactory] 80abstract class InteractiveInkFeatureFactory { 81 /// Subclasses should provide a const constructor. 82 const InteractiveInkFeatureFactory(); 83 84 /// The factory method. 85 /// 86 /// Subclasses should override this method to return a new instance of an 87 /// [InteractiveInkFeature]. 88 InteractiveInkFeature create({ 89 @required MaterialInkController controller, 90 @required RenderBox referenceBox, 91 @required Offset position, 92 @required Color color, 93 @required TextDirection textDirection, 94 bool containedInkWell = false, 95 RectCallback rectCallback, 96 BorderRadius borderRadius, 97 ShapeBorder customBorder, 98 double radius, 99 VoidCallback onRemoved, 100 }); 101} 102 103/// An area of a [Material] that responds to touch. Has a configurable shape and 104/// can be configured to clip splashes that extend outside its bounds or not. 105/// 106/// For a variant of this widget that is specialized for rectangular areas that 107/// always clip splashes, see [InkWell]. 108/// 109/// An [InkResponse] widget does two things when responding to a tap: 110/// 111/// * It starts to animate a _highlight_. The shape of the highlight is 112/// determined by [highlightShape]. If it is a [BoxShape.circle], the 113/// default, then the highlight is a circle of fixed size centered in the 114/// [InkResponse]. If it is [BoxShape.rectangle], then the highlight is a box 115/// the size of the [InkResponse] itself, unless [getRectCallback] is 116/// provided, in which case that callback defines the rectangle. The color of 117/// the highlight is set by [highlightColor]. 118/// 119/// * Simultaneously, it starts to animate a _splash_. This is a growing circle 120/// initially centered on the tap location. If this is a [containedInkWell], 121/// the splash grows to the [radius] while remaining centered at the tap 122/// location. Otherwise, the splash migrates to the center of the box as it 123/// grows. 124/// 125/// The following two diagrams show how [InkResponse] looks when tapped if the 126/// [highlightShape] is [BoxShape.circle] (the default) and [containedInkWell] 127/// is false (also the default). 128/// 129/// The first diagram shows how it looks if the [InkResponse] is relatively 130/// large: 131/// 132///  133/// 134/// The second diagram shows how it looks if the [InkResponse] is small: 135/// 136///  137/// 138/// The main thing to notice from these diagrams is that the splashes happily 139/// exceed the bounds of the widget (because [containedInkWell] is false). 140/// 141/// The following diagram shows the effect when the [InkResponse] has a 142/// [highlightShape] of [BoxShape.rectangle] with [containedInkWell] set to 143/// true. These are the values used by [InkWell]. 144/// 145///  146/// 147/// The [InkResponse] widget must have a [Material] widget as an ancestor. The 148/// [Material] widget is where the ink reactions are actually painted. This 149/// matches the material design premise wherein the [Material] is what is 150/// 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/// ```dart 156/// assert(debugCheckHasMaterial(context)); 157/// ``` 158/// 159/// ## Troubleshooting 160/// 161/// ### The ink splashes aren't visible! 162/// 163/// If there is an opaque graphic, e.g. painted using a [Container], [Image], or 164/// [DecoratedBox], between the [Material] widget and the [InkResponse] widget, 165/// then the splash won't be visible because it will be under the opaque graphic. 166/// This is because ink splashes draw on the underlying [Material] itself, as 167/// if the ink was spreading inside the material. 168/// 169/// The [Ink] widget can be used as a replacement for [Image], [Container], or 170/// [DecoratedBox] to ensure that the image or decoration also paints in the 171/// [Material] itself, below the ink. 172/// 173/// If this is not possible for some reason, e.g. because you are using an 174/// opaque [CustomPaint] widget, alternatively consider using a second 175/// [Material] above the opaque widget but below the [InkResponse] (as an 176/// ancestor to the ink response). The [MaterialType.transparency] material 177/// kind can be used for this purpose. 178/// 179/// See also: 180/// 181/// * [GestureDetector], for listening for gestures without ink splashes. 182/// * [RaisedButton] and [FlatButton], two kinds of buttons in material design. 183/// * [IconButton], which combines [InkResponse] with an [Icon]. 184class InkResponse extends StatefulWidget { 185 /// Creates an area of a [Material] that responds to touch. 186 /// 187 /// Must have an ancestor [Material] widget in which to cause ink reactions. 188 /// 189 /// The [containedInkWell], [highlightShape], [enableFeedback], and 190 /// [excludeFromSemantics] arguments must not be null. 191 const InkResponse({ 192 Key key, 193 this.child, 194 this.onTap, 195 this.onTapDown, 196 this.onTapCancel, 197 this.onDoubleTap, 198 this.onLongPress, 199 this.onHighlightChanged, 200 this.onHover, 201 this.containedInkWell = false, 202 this.highlightShape = BoxShape.circle, 203 this.radius, 204 this.borderRadius, 205 this.customBorder, 206 this.focusColor, 207 this.hoverColor, 208 this.highlightColor, 209 this.splashColor, 210 this.splashFactory, 211 this.enableFeedback = true, 212 this.excludeFromSemantics = false, 213 }) : assert(containedInkWell != null), 214 assert(highlightShape != null), 215 assert(enableFeedback != null), 216 assert(excludeFromSemantics != null), 217 super(key: key); 218 219 /// The widget below this widget in the tree. 220 /// 221 /// {@macro flutter.widgets.child} 222 final Widget child; 223 224 /// Called when the user taps this part of the material. 225 final GestureTapCallback onTap; 226 227 /// Called when the user taps down this part of the material. 228 final GestureTapDownCallback onTapDown; 229 230 /// Called when the user cancels a tap that was started on this part of the 231 /// material. 232 final GestureTapCallback onTapCancel; 233 234 /// Called when the user double taps this part of the material. 235 final GestureTapCallback onDoubleTap; 236 237 /// Called when the user long-presses on this part of the material. 238 final GestureLongPressCallback onLongPress; 239 240 /// Called when this part of the material either becomes highlighted or stops 241 /// being highlighted. 242 /// 243 /// The value passed to the callback is true if this part of the material has 244 /// become highlighted and false if this part of the material has stopped 245 /// being highlighted. 246 /// 247 /// If all of [onTap], [onDoubleTap], and [onLongPress] become null while a 248 /// gesture is ongoing, then [onTapCancel] will be fired and 249 /// [onHighlightChanged] will be fired with the value false _during the 250 /// build_. This means, for instance, that in that scenario [State.setState] 251 /// cannot be called. 252 final ValueChanged<bool> onHighlightChanged; 253 254 /// Called when a pointer enters or exits the ink response area. 255 /// 256 /// The value passed to the callback is true if a pointer has entered this 257 /// part of the material and false if a pointer has exited this part of the 258 /// material. 259 final ValueChanged<bool> onHover; 260 261 /// Whether this ink response should be clipped its bounds. 262 /// 263 /// This flag also controls whether the splash migrates to the center of the 264 /// [InkResponse] or not. If [containedInkWell] is true, the splash remains 265 /// centered around the tap location. If it is false, the splash migrates to 266 /// the center of the [InkResponse] as it grows. 267 /// 268 /// See also: 269 /// 270 /// * [highlightShape], the shape of the focus, hover, and pressed 271 /// highlights. 272 /// * [borderRadius], which controls the corners when the box is a rectangle. 273 /// * [getRectCallback], which controls the size and position of the box when 274 /// it is a rectangle. 275 final bool containedInkWell; 276 277 /// The shape (e.g., circle, rectangle) to use for the highlight drawn around 278 /// this part of the material when pressed, hovered over, or focused. 279 /// 280 /// The same shape is used for the pressed highlight (see [highlightColor]), 281 /// the focus highlight (see [focusColor]), and the hover highlight (see 282 /// [hoverColor]). 283 /// 284 /// If the shape is [BoxShape.circle], then the highlight is centered on the 285 /// [InkResponse]. If the shape is [BoxShape.rectangle], then the highlight 286 /// fills the [InkResponse], or the rectangle provided by [getRectCallback] if 287 /// the callback is specified. 288 /// 289 /// See also: 290 /// 291 /// * [containedInkWell], which controls clipping behavior. 292 /// * [borderRadius], which controls the corners when the box is a rectangle. 293 /// * [highlightColor], the color of the highlight. 294 /// * [getRectCallback], which controls the size and position of the box when 295 /// it is a rectangle. 296 final BoxShape highlightShape; 297 298 /// The radius of the ink splash. 299 /// 300 /// Splashes grow up to this size. By default, this size is determined from 301 /// the size of the rectangle provided by [getRectCallback], or the size of 302 /// the [InkResponse] itself. 303 /// 304 /// See also: 305 /// 306 /// * [splashColor], the color of the splash. 307 /// * [splashFactory], which defines the appearance of the splash. 308 final double radius; 309 310 /// The clipping radius of the containing rect. This is effective only if 311 /// [customBorder] is null. 312 /// 313 /// If this is null, it is interpreted as [BorderRadius.zero]. 314 final BorderRadius borderRadius; 315 316 /// The custom clip border which overrides [borderRadius]. 317 final ShapeBorder customBorder; 318 319 /// The color of the ink response when the parent widget is focused. If this 320 /// property is null then the focus color of the theme, 321 /// [ThemeData.focusColor], will be used. 322 /// 323 /// See also: 324 /// 325 /// * [highlightShape], the shape of the focus, hover, and pressed 326 /// highlights. 327 /// * [hoverColor], the color of the hover highlight. 328 /// * [splashColor], the color of the splash. 329 /// * [splashFactory], which defines the appearance of the splash. 330 final Color focusColor; 331 332 /// The color of the ink response when a pointer is hovering over it. If this 333 /// property is null then the hover color of the theme, 334 /// [ThemeData.hoverColor], will be used. 335 /// 336 /// See also: 337 /// 338 /// * [highlightShape], the shape of the focus, hover, and pressed 339 /// highlights. 340 /// * [highlightColor], the color of the pressed highlight. 341 /// * [focusColor], the color of the focus highlight. 342 /// * [splashColor], the color of the splash. 343 /// * [splashFactory], which defines the appearance of the splash. 344 final Color hoverColor; 345 346 /// The highlight color of the ink response when pressed. If this property is 347 /// null then the highlight color of the theme, [ThemeData.highlightColor], 348 /// will be used. 349 /// 350 /// See also: 351 /// 352 /// * [hoverColor], the color of the hover highlight. 353 /// * [focusColor], the color of the focus highlight. 354 /// * [highlightShape], the shape of the focus, hover, and pressed 355 /// highlights. 356 /// * [splashColor], the color of the splash. 357 /// * [splashFactory], which defines the appearance of the splash. 358 final Color highlightColor; 359 360 /// The splash color of the ink response. If this property is null then the 361 /// splash color of the theme, [ThemeData.splashColor], will be used. 362 /// 363 /// See also: 364 /// 365 /// * [splashFactory], which defines the appearance of the splash. 366 /// * [radius], the (maximum) size of the ink splash. 367 /// * [highlightColor], the color of the highlight. 368 final Color splashColor; 369 370 /// Defines the appearance of the splash. 371 /// 372 /// Defaults to the value of the theme's splash factory: [ThemeData.splashFactory]. 373 /// 374 /// See also: 375 /// 376 /// * [radius], the (maximum) size of the ink splash. 377 /// * [splashColor], the color of the splash. 378 /// * [highlightColor], the color of the highlight. 379 /// * [InkSplash.splashFactory], which defines the default splash. 380 /// * [InkRipple.splashFactory], which defines a splash that spreads out 381 /// more aggressively than the default. 382 final InteractiveInkFeatureFactory splashFactory; 383 384 /// Whether detected gestures should provide acoustic and/or haptic feedback. 385 /// 386 /// For example, on Android a tap will produce a clicking sound and a 387 /// long-press will produce a short vibration, when feedback is enabled. 388 /// 389 /// See also: 390 /// 391 /// * [Feedback] for providing platform-specific feedback to certain actions. 392 final bool enableFeedback; 393 394 /// Whether to exclude the gestures introduced by this widget from the 395 /// semantics tree. 396 /// 397 /// For example, a long-press gesture for showing a tooltip is usually 398 /// excluded because the tooltip itself is included in the semantics 399 /// tree directly and so having a gesture to show it would result in 400 /// duplication of information. 401 final bool excludeFromSemantics; 402 403 /// The rectangle to use for the highlight effect and for clipping 404 /// the splash effects if [containedInkWell] is true. 405 /// 406 /// This method is intended to be overridden by descendants that 407 /// specialize [InkResponse] for unusual cases. For example, 408 /// [TableRowInkWell] implements this method to return the rectangle 409 /// corresponding to the row that the widget is in. 410 /// 411 /// The default behavior returns null, which is equivalent to 412 /// returning the referenceBox argument's bounding box (though 413 /// slightly more efficient). 414 RectCallback getRectCallback(RenderBox referenceBox) => null; 415 416 /// Asserts that the given context satisfies the prerequisites for 417 /// this class. 418 /// 419 /// This method is intended to be overridden by descendants that 420 /// specialize [InkResponse] for unusual cases. For example, 421 /// [TableRowInkWell] implements this method to verify that the widget is 422 /// in a table. 423 @mustCallSuper 424 bool debugCheckContext(BuildContext context) { 425 assert(debugCheckHasMaterial(context)); 426 assert(debugCheckHasDirectionality(context)); 427 return true; 428 } 429 430 @override 431 _InkResponseState<InkResponse> createState() => _InkResponseState<InkResponse>(); 432 433 @override 434 void debugFillProperties(DiagnosticPropertiesBuilder properties) { 435 super.debugFillProperties(properties); 436 final List<String> gestures = <String>[]; 437 if (onTap != null) 438 gestures.add('tap'); 439 if (onDoubleTap != null) 440 gestures.add('double tap'); 441 if (onLongPress != null) 442 gestures.add('long press'); 443 if (onTapDown != null) 444 gestures.add('tap down'); 445 if (onTapCancel != null) 446 gestures.add('tap cancel'); 447 properties.add(IterableProperty<String>('gestures', gestures, ifEmpty: '<none>')); 448 properties.add(DiagnosticsProperty<bool>('containedInkWell', containedInkWell, level: DiagnosticLevel.fine)); 449 properties.add(DiagnosticsProperty<BoxShape>( 450 'highlightShape', 451 highlightShape, 452 description: '${containedInkWell ? "clipped to " : ""}$highlightShape', 453 showName: false, 454 )); 455 } 456} 457 458/// Used to index the allocated highlights for the different types of highlights 459/// in [_InkResponseState]. 460enum _HighlightType { 461 pressed, 462 hover, 463 focus, 464} 465 466class _InkResponseState<T extends InkResponse> extends State<T> with AutomaticKeepAliveClientMixin<T> { 467 Set<InteractiveInkFeature> _splashes; 468 InteractiveInkFeature _currentSplash; 469 FocusNode _focusNode; 470 bool _hovering = false; 471 final Map<_HighlightType, InkHighlight> _highlights = <_HighlightType, InkHighlight>{}; 472 473 bool get highlightsExist => _highlights.values.where((InkHighlight highlight) => highlight != null).isNotEmpty; 474 475 @override 476 void didChangeDependencies() { 477 super.didChangeDependencies(); 478 _focusNode?.removeListener(_handleFocusUpdate); 479 _focusNode = Focus.of(context, nullOk: true); 480 _focusNode?.addListener(_handleFocusUpdate); 481 WidgetsBinding.instance.focusManager.addHighlightModeListener(_handleFocusHighlightModeChange); 482 } 483 484 @override 485 void didUpdateWidget(InkResponse oldWidget) { 486 super.didUpdateWidget(oldWidget); 487 if (_isWidgetEnabled(widget) != _isWidgetEnabled(oldWidget)) { 488 _handleHoverChange(_hovering); 489 _handleFocusUpdate(); 490 } 491 } 492 493 @override 494 void dispose() { 495 WidgetsBinding.instance.focusManager.removeHighlightModeListener(_handleFocusHighlightModeChange); 496 _focusNode?.removeListener(_handleFocusUpdate); 497 super.dispose(); 498 } 499 500 @override 501 bool get wantKeepAlive => highlightsExist || (_splashes != null && _splashes.isNotEmpty); 502 503 Color getHighlightColorForType(_HighlightType type) { 504 switch (type) { 505 case _HighlightType.pressed: 506 return widget.highlightColor ?? Theme.of(context).highlightColor; 507 case _HighlightType.focus: 508 return widget.focusColor ?? Theme.of(context).focusColor; 509 case _HighlightType.hover: 510 return widget.hoverColor ?? Theme.of(context).hoverColor; 511 } 512 assert(false, 'Unhandled $_HighlightType $type'); 513 return null; 514 } 515 516 Duration getFadeDurationForType(_HighlightType type) { 517 switch (type) { 518 case _HighlightType.pressed: 519 return const Duration(milliseconds: 200); 520 case _HighlightType.hover: 521 case _HighlightType.focus: 522 return const Duration(milliseconds: 50); 523 } 524 assert(false, 'Unhandled $_HighlightType $type'); 525 return null; 526 } 527 528 void updateHighlight(_HighlightType type, {@required bool value}) { 529 final InkHighlight highlight = _highlights[type]; 530 void handleInkRemoval() { 531 assert(_highlights[type] != null); 532 _highlights[type] = null; 533 updateKeepAlive(); 534 } 535 536 if (value == (highlight != null && highlight.active)) 537 return; 538 if (value) { 539 if (highlight == null) { 540 final RenderBox referenceBox = context.findRenderObject(); 541 _highlights[type] = InkHighlight( 542 controller: Material.of(context), 543 referenceBox: referenceBox, 544 color: getHighlightColorForType(type), 545 shape: widget.highlightShape, 546 borderRadius: widget.borderRadius, 547 customBorder: widget.customBorder, 548 rectCallback: widget.getRectCallback(referenceBox), 549 onRemoved: handleInkRemoval, 550 textDirection: Directionality.of(context), 551 fadeDuration: getFadeDurationForType(type), 552 ); 553 updateKeepAlive(); 554 } else { 555 highlight.activate(); 556 } 557 } else { 558 highlight.deactivate(); 559 } 560 assert(value == (_highlights[type] != null && _highlights[type].active)); 561 562 switch(type) { 563 case _HighlightType.pressed: 564 if (widget.onHighlightChanged != null) 565 widget.onHighlightChanged(value); 566 break; 567 case _HighlightType.hover: 568 if (widget.onHover != null) 569 widget.onHover(value); 570 break; 571 case _HighlightType.focus: 572 break; 573 } 574 } 575 576 InteractiveInkFeature _createInkFeature(TapDownDetails details) { 577 final MaterialInkController inkController = Material.of(context); 578 final RenderBox referenceBox = context.findRenderObject(); 579 final Offset position = referenceBox.globalToLocal(details.globalPosition); 580 final Color color = widget.splashColor ?? Theme.of(context).splashColor; 581 final RectCallback rectCallback = widget.containedInkWell ? widget.getRectCallback(referenceBox) : null; 582 final BorderRadius borderRadius = widget.borderRadius; 583 final ShapeBorder customBorder = widget.customBorder; 584 585 InteractiveInkFeature splash; 586 void onRemoved() { 587 if (_splashes != null) { 588 assert(_splashes.contains(splash)); 589 _splashes.remove(splash); 590 if (_currentSplash == splash) 591 _currentSplash = null; 592 updateKeepAlive(); 593 } // else we're probably in deactivate() 594 } 595 596 splash = (widget.splashFactory ?? Theme.of(context).splashFactory).create( 597 controller: inkController, 598 referenceBox: referenceBox, 599 position: position, 600 color: color, 601 containedInkWell: widget.containedInkWell, 602 rectCallback: rectCallback, 603 radius: widget.radius, 604 borderRadius: borderRadius, 605 customBorder: customBorder, 606 onRemoved: onRemoved, 607 textDirection: Directionality.of(context), 608 ); 609 610 return splash; 611 } 612 613 void _handleFocusHighlightModeChange(FocusHighlightMode mode) { 614 if (!mounted) { 615 return; 616 } 617 setState(() { 618 _handleFocusUpdate(); 619 }); 620 } 621 622 void _handleFocusUpdate() { 623 bool showFocus; 624 switch (WidgetsBinding.instance.focusManager.highlightMode) { 625 case FocusHighlightMode.touch: 626 showFocus = false; 627 break; 628 case FocusHighlightMode.traditional: 629 showFocus = enabled && (Focus.of(context, nullOk: true)?.hasPrimaryFocus ?? false); 630 break; 631 } 632 updateHighlight(_HighlightType.focus, value: showFocus); 633 } 634 635 void _handleTapDown(TapDownDetails details) { 636 final InteractiveInkFeature splash = _createInkFeature(details); 637 _splashes ??= HashSet<InteractiveInkFeature>(); 638 _splashes.add(splash); 639 _currentSplash = splash; 640 if (widget.onTapDown != null) { 641 widget.onTapDown(details); 642 } 643 updateKeepAlive(); 644 updateHighlight(_HighlightType.pressed, value: true); 645 } 646 647 void _handleTap(BuildContext context) { 648 _currentSplash?.confirm(); 649 _currentSplash = null; 650 updateHighlight(_HighlightType.pressed, value: false); 651 if (widget.onTap != null) { 652 if (widget.enableFeedback) 653 Feedback.forTap(context); 654 widget.onTap(); 655 } 656 } 657 658 void _handleTapCancel() { 659 _currentSplash?.cancel(); 660 _currentSplash = null; 661 if (widget.onTapCancel != null) { 662 widget.onTapCancel(); 663 } 664 updateHighlight(_HighlightType.pressed, value: false); 665 } 666 667 void _handleDoubleTap() { 668 _currentSplash?.confirm(); 669 _currentSplash = null; 670 if (widget.onDoubleTap != null) 671 widget.onDoubleTap(); 672 } 673 674 void _handleLongPress(BuildContext context) { 675 _currentSplash?.confirm(); 676 _currentSplash = null; 677 if (widget.onLongPress != null) { 678 if (widget.enableFeedback) 679 Feedback.forLongPress(context); 680 widget.onLongPress(); 681 } 682 } 683 684 @override 685 void deactivate() { 686 if (_splashes != null) { 687 final Set<InteractiveInkFeature> splashes = _splashes; 688 _splashes = null; 689 for (InteractiveInkFeature splash in splashes) 690 splash.dispose(); 691 _currentSplash = null; 692 } 693 assert(_currentSplash == null); 694 for (_HighlightType highlight in _highlights.keys) { 695 _highlights[highlight]?.dispose(); 696 _highlights[highlight] = null; 697 } 698 super.deactivate(); 699 } 700 701 bool _isWidgetEnabled(InkResponse widget) { 702 return widget.onTap != null || widget.onDoubleTap != null || widget.onLongPress != null; 703 } 704 705 bool get enabled => _isWidgetEnabled(widget); 706 707 void _handleMouseEnter(PointerEnterEvent event) => _handleHoverChange(true); 708 void _handleMouseExit(PointerExitEvent event) => _handleHoverChange(false); 709 void _handleHoverChange(bool hovering) { 710 if (_hovering != hovering) { 711 _hovering = hovering; 712 updateHighlight(_HighlightType.hover, value: enabled && _hovering); 713 } 714 } 715 716 @override 717 Widget build(BuildContext context) { 718 assert(widget.debugCheckContext(context)); 719 super.build(context); // See AutomaticKeepAliveClientMixin. 720 for (_HighlightType type in _highlights.keys) { 721 _highlights[type]?.color = getHighlightColorForType(type); 722 } 723 _currentSplash?.color = widget.splashColor ?? Theme.of(context).splashColor; 724 return MouseRegion( 725 onEnter: enabled ? _handleMouseEnter : null, 726 onExit: enabled ? _handleMouseExit : null, 727 child: GestureDetector( 728 onTapDown: enabled ? _handleTapDown : null, 729 onTap: enabled ? () => _handleTap(context) : null, 730 onTapCancel: enabled ? _handleTapCancel : null, 731 onDoubleTap: widget.onDoubleTap != null ? _handleDoubleTap : null, 732 onLongPress: widget.onLongPress != null ? () => _handleLongPress(context) : null, 733 behavior: HitTestBehavior.opaque, 734 child: widget.child, 735 excludeFromSemantics: widget.excludeFromSemantics, 736 ), 737 ); 738 } 739} 740 741/// A rectangular area of a [Material] that responds to touch. 742/// 743/// For a variant of this widget that does not clip splashes, see [InkResponse]. 744/// 745/// The following diagram shows how an [InkWell] looks when tapped, when using 746/// default values. 747/// 748///  749/// 750/// The [InkWell] widget must have a [Material] widget as an ancestor. The 751/// [Material] widget is where the ink reactions are actually painted. This 752/// matches the material design premise wherein the [Material] is what is 753/// actually reacting to touches by spreading ink. 754/// 755/// If a Widget uses this class directly, it should include the following line 756/// at the top of its build function to call [debugCheckHasMaterial]: 757/// 758/// ```dart 759/// assert(debugCheckHasMaterial(context)); 760/// ``` 761/// 762/// ## Troubleshooting 763/// 764/// ### The ink splashes aren't visible! 765/// 766/// If there is an opaque graphic, e.g. painted using a [Container], [Image], or 767/// [DecoratedBox], between the [Material] widget and the [InkWell] widget, then 768/// the splash won't be visible because it will be under the opaque graphic. 769/// This is because ink splashes draw on the underlying [Material] itself, as 770/// if the ink was spreading inside the material. 771/// 772/// The [Ink] widget can be used as a replacement for [Image], [Container], or 773/// [DecoratedBox] to ensure that the image or decoration also paints in the 774/// [Material] itself, below the ink. 775/// 776/// If this is not possible for some reason, e.g. because you are using an 777/// opaque [CustomPaint] widget, alternatively consider using a second 778/// [Material] above the opaque widget but below the [InkWell] (as an 779/// ancestor to the ink well). The [MaterialType.transparency] material 780/// kind can be used for this purpose. 781/// 782/// ### The ink splashes don't track the size of an animated container 783/// If the size of an InkWell's [Material] ancestor changes while the InkWell's 784/// splashes are expanding, you may notice that the splashes aren't clipped 785/// correctly. This can't be avoided. 786/// 787/// An example of this situation is as follows: 788/// 789/// {@tool snippet --template=stateful_widget_scaffold} 790/// 791/// Tap the container to cause it to grow. Then, tap it again and hold before 792/// the widget reaches its maximum size to observe the clipped ink splash. 793/// 794/// ```dart 795/// double sideLength = 50; 796/// 797/// Widget build(BuildContext context) { 798/// return Center( 799/// child: AnimatedContainer( 800/// height: sideLength, 801/// width: sideLength, 802/// duration: Duration(seconds: 2), 803/// curve: Curves.easeIn, 804/// child: Material( 805/// color: Colors.yellow, 806/// child: InkWell( 807/// onTap: () { 808/// setState(() { 809/// sideLength == 50 ? sideLength = 100 : sideLength = 50; 810/// }); 811/// }, 812/// ), 813/// ), 814/// ), 815/// ); 816/// } 817/// ``` 818/// {@end-tool} 819/// 820/// An InkWell's splashes will not properly update to conform to changes if the 821/// size of its underlying [Material], where the splashes are rendered, changes 822/// during animation. You should avoid using InkWells within [Material] widgets 823/// that are changing size. 824/// 825/// See also: 826/// 827/// * [GestureDetector], for listening for gestures without ink splashes. 828/// * [RaisedButton] and [FlatButton], two kinds of buttons in material design. 829/// * [InkResponse], a variant of [InkWell] that doesn't force a rectangular 830/// shape on the ink reaction. 831class InkWell extends InkResponse { 832 /// Creates an ink well. 833 /// 834 /// Must have an ancestor [Material] widget in which to cause ink reactions. 835 /// 836 /// The [enableFeedback] and [excludeFromSemantics] arguments must not be 837 /// null. 838 const InkWell({ 839 Key key, 840 Widget child, 841 GestureTapCallback onTap, 842 GestureTapCallback onDoubleTap, 843 GestureLongPressCallback onLongPress, 844 GestureTapDownCallback onTapDown, 845 GestureTapCancelCallback onTapCancel, 846 ValueChanged<bool> onHighlightChanged, 847 ValueChanged<bool> onHover, 848 Color focusColor, 849 Color hoverColor, 850 Color highlightColor, 851 Color splashColor, 852 InteractiveInkFeatureFactory splashFactory, 853 double radius, 854 BorderRadius borderRadius, 855 ShapeBorder customBorder, 856 bool enableFeedback = true, 857 bool excludeFromSemantics = false, 858 }) : super( 859 key: key, 860 child: child, 861 onTap: onTap, 862 onDoubleTap: onDoubleTap, 863 onLongPress: onLongPress, 864 onTapDown: onTapDown, 865 onTapCancel: onTapCancel, 866 onHighlightChanged: onHighlightChanged, 867 onHover: onHover, 868 containedInkWell: true, 869 highlightShape: BoxShape.rectangle, 870 focusColor: focusColor, 871 hoverColor: hoverColor, 872 highlightColor: highlightColor, 873 splashColor: splashColor, 874 splashFactory: splashFactory, 875 radius: radius, 876 borderRadius: borderRadius, 877 customBorder: customBorder, 878 enableFeedback: enableFeedback ?? true, 879 excludeFromSemantics: excludeFromSemantics ?? false, 880 ); 881} 882