// 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. 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 'ink_splash.dart'; import 'material.dart'; import 'theme.dart'; /// 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/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/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/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)); /// ``` /// The parameter [enableFeedback] must not be null. /// /// 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. const InkResponse({ Key key, this.child, this.onTap, this.onDoubleTap, this.onLongPress, this.onHighlightChanged, this.containedInkWell: false, this.highlightShape: BoxShape.circle, this.radius, this.borderRadius: BorderRadius.zero, this.highlightColor, this.splashColor, this.enableFeedback: true, }) : assert(enableFeedback != null), super(key: key); /// The widget below this widget in the tree. final Widget child; /// Called when the user taps this part of the material. final GestureTapCallback onTap; /// 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. final ValueChanged onHighlightChanged; /// 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], 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. final bool containedInkWell; /// 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. 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. final double radius; /// The clipping radius of the containing rect. final BorderRadius borderRadius; /// The highlight color of the ink response. If this property is null then the /// highlight color of the theme, [ThemeData.highlightColor], will be used. /// /// See also: /// /// * [highlightShape], the shape of the highlight. /// * [splashColor], the color 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: /// /// * [radius], the (maximum) size of the ink splash. /// * [highlightColor], the color of the highlight. final Color splashColor; /// 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; /// 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)); return true; } @override _InkResponseState createState() => new _InkResponseState(); @override void debugFillProperties(DiagnosticPropertiesBuilder description) { super.debugFillProperties(description); final List gestures = []; if (onTap != null) gestures.add('tap'); if (onDoubleTap != null) gestures.add('double tap'); if (onLongPress != null) gestures.add('long press'); description.add(new IterableProperty('gestures', gestures, ifEmpty: '')); description.add(new DiagnosticsProperty('containedInkWell', containedInkWell, level: DiagnosticLevel.fine)); description.add(new DiagnosticsProperty( 'highlightShape', highlightShape, description: '${containedInkWell ? "clipped to " : ""}$highlightShape', showName: false, )); } } class _InkResponseState extends State with AutomaticKeepAliveClientMixin { Set _splashes; InkSplash _currentSplash; InkHighlight _lastHighlight; @override bool get wantKeepAlive => _lastHighlight != null || (_splashes != null && _splashes.isNotEmpty); void updateHighlight(bool value) { if (value == (_lastHighlight != null && _lastHighlight.active)) return; if (value) { if (_lastHighlight == null) { final RenderBox referenceBox = context.findRenderObject(); _lastHighlight = new InkHighlight( controller: Material.of(context), referenceBox: referenceBox, color: widget.highlightColor ?? Theme.of(context).highlightColor, shape: widget.highlightShape, borderRadius: widget.borderRadius, rectCallback: widget.getRectCallback(referenceBox), onRemoved: _handleInkHighlightRemoval, ); updateKeepAlive(); } else { _lastHighlight.activate(); } } else { _lastHighlight.deactivate(); } assert(value == (_lastHighlight != null && _lastHighlight.active)); if (widget.onHighlightChanged != null) widget.onHighlightChanged(value); } void _handleInkHighlightRemoval() { assert(_lastHighlight != null); _lastHighlight = null; updateKeepAlive(); } void _handleTapDown(TapDownDetails details) { final RenderBox referenceBox = context.findRenderObject(); final RectCallback rectCallback = widget.getRectCallback(referenceBox); InkSplash splash; splash = new InkSplash( controller: Material.of(context), referenceBox: referenceBox, position: referenceBox.globalToLocal(details.globalPosition), color: widget.splashColor ?? Theme.of(context).splashColor, containedInkWell: widget.containedInkWell, rectCallback: widget.containedInkWell ? rectCallback : null, radius: widget.radius, borderRadius: widget.borderRadius ?? BorderRadius.zero, onRemoved: () { if (_splashes != null) { assert(_splashes.contains(splash)); _splashes.remove(splash); if (_currentSplash == splash) _currentSplash = null; updateKeepAlive(); } // else we're probably in deactivate() } ); _splashes ??= new HashSet(); _splashes.add(splash); _currentSplash = splash; updateKeepAlive(); updateHighlight(true); } void _handleTap(BuildContext context) { _currentSplash?.confirm(); _currentSplash = null; updateHighlight(false); if (widget.onTap != null) { if (widget.enableFeedback) Feedback.forTap(context); widget.onTap(); } } void _handleTapCancel() { _currentSplash?.cancel(); _currentSplash = null; updateHighlight(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 splashes = _splashes; _splashes = null; for (InkSplash splash in splashes) splash.dispose(); _currentSplash = null; } assert(_currentSplash == null); _lastHighlight?.dispose(); _lastHighlight = null; super.deactivate(); } @override Widget build(BuildContext context) { assert(widget.debugCheckContext(context)); super.build(context); // See AutomaticKeepAliveClientMixin. final ThemeData themeData = Theme.of(context); _lastHighlight?.color = widget.highlightColor ?? themeData.highlightColor; _currentSplash?.color = widget.splashColor ?? themeData.splashColor; final bool enabled = widget.onTap != null || widget.onDoubleTap != null || widget.onLongPress != null; return new 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, 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/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)); /// ``` /// /// 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. const InkWell({ Key key, Widget child, GestureTapCallback onTap, GestureTapCallback onDoubleTap, GestureLongPressCallback onLongPress, ValueChanged onHighlightChanged, Color highlightColor, Color splashColor, BorderRadius borderRadius, bool enableFeedback: true, }) : super( key: key, child: child, onTap: onTap, onDoubleTap: onDoubleTap, onLongPress: onLongPress, onHighlightChanged: onHighlightChanged, containedInkWell: true, highlightShape: BoxShape.rectangle, highlightColor: highlightColor, splashColor: splashColor, borderRadius: borderRadius, enableFeedback: enableFeedback, ); }