diff --git a/packages/flutter/lib/material.dart b/packages/flutter/lib/material.dart index 7bad025140..b5e7d45b40 100644 --- a/packages/flutter/lib/material.dart +++ b/packages/flutter/lib/material.dart @@ -53,6 +53,7 @@ export 'src/material/grid_tile_bar.dart'; export 'src/material/icon_button.dart'; export 'src/material/icons.dart'; export 'src/material/ink_highlight.dart'; +export 'src/material/ink_ripple.dart'; export 'src/material/ink_splash.dart'; export 'src/material/ink_well.dart'; export 'src/material/input_border.dart'; diff --git a/packages/flutter/lib/src/material/ink_highlight.dart b/packages/flutter/lib/src/material/ink_highlight.dart index 3130b3d8a9..196808b1e8 100644 --- a/packages/flutter/lib/src/material/ink_highlight.dart +++ b/packages/flutter/lib/src/material/ink_highlight.dart @@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; +import 'ink_well.dart' show InteractiveInkFeature; import 'material.dart'; const Duration _kHighlightFadeDuration = const Duration(milliseconds: 200); @@ -25,7 +26,7 @@ const Duration _kHighlightFadeDuration = const Duration(milliseconds: 200); /// * [Material], which is the widget on which the ink highlight is painted. /// * [InkSplash], which is an ink feature that shows a reaction to user input /// on a [Material]. -class InkHighlight extends InkFeature { +class InkHighlight extends InteractiveInkFeature { /// Begin a highlight animation. /// /// The [controller] argument is typically obtained via @@ -45,11 +46,10 @@ class InkHighlight extends InkFeature { VoidCallback onRemoved, }) : assert(color != null), assert(shape != null), - _color = color, _shape = shape, _borderRadius = borderRadius ?? BorderRadius.zero, _rectCallback = rectCallback, - super(controller: controller, referenceBox: referenceBox, onRemoved: onRemoved) { + super(controller: controller, referenceBox: referenceBox, color: color, onRemoved: onRemoved) { _alphaController = new AnimationController(duration: _kHighlightFadeDuration, vsync: controller.vsync) ..addListener(controller.markNeedsPaint) ..addStatusListener(_handleAlphaStatusChanged) @@ -69,16 +69,6 @@ class InkHighlight extends InkFeature { Animation _alpha; AnimationController _alphaController; - /// The color of the ink used to emphasize part of the material. - Color get color => _color; - Color _color; - set color(Color value) { - if (value == _color) - return; - _color = value; - controller.markNeedsPaint(); - } - /// Whether this part of the material is being visually emphasized. bool get active => _active; bool _active = true; diff --git a/packages/flutter/lib/src/material/ink_ripple.dart b/packages/flutter/lib/src/material/ink_ripple.dart new file mode 100644 index 0000000000..abad5985d6 --- /dev/null +++ b/packages/flutter/lib/src/material/ink_ripple.dart @@ -0,0 +1,275 @@ +// Copyright 2017 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:math' as math; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/widgets.dart'; + +import 'ink_well.dart'; +import 'material.dart'; + +const Duration _kUnconfirmedRippleDuration = const Duration(seconds: 1); +const Duration _kFadeInDuration = const Duration(milliseconds: 75); +const Duration _kRadiusDuration = const Duration(milliseconds: 225); +const Duration _kFadeOutDuration = const Duration(milliseconds: 450); +const Duration _kCancelDuration = const Duration(milliseconds: 75); + +// The fade out begins 300ms after the _fadeOutController starts. See confirm(). +const double _kFadeOutIntervalStart = 300.0 / 450.0; + +const double _kRippleConfirmedVelocity = 1.0; // logical pixels per millisecond + +RectCallback _getClipCallback(RenderBox referenceBox, bool containedInkWell, RectCallback rectCallback) { + if (rectCallback != null) { + assert(containedInkWell); + return rectCallback; + } + if (containedInkWell) + return () => Offset.zero & referenceBox.size; + return null; +} + +double _getTargetRadius(RenderBox referenceBox, bool containedInkWell, RectCallback rectCallback, Offset position) { + if (containedInkWell) { + final Size size = rectCallback != null ? rectCallback().size : referenceBox.size; + return _getRippleRadiusForPositionInSize(size, position); + } + return Material.defaultSplashRadius; +} + +double _getRippleRadiusForPositionInSize(Size bounds, Offset position) { + final double d1 = (position - bounds.topLeft(Offset.zero)).distance; + final double d2 = (position - bounds.topRight(Offset.zero)).distance; + final double d3 = (position - bounds.bottomLeft(Offset.zero)).distance; + final double d4 = (position - bounds.bottomRight(Offset.zero)).distance; + return math.max(math.max(d1, d2), math.max(d3, d4)).ceilToDouble(); +} + +class _InkRippleFactory extends InteractiveInkFeatureFactory { + const _InkRippleFactory(); + + @override + InteractiveInkFeature create({ + @required MaterialInkController controller, + @required RenderBox referenceBox, + @required Offset position, + @required Color color, + bool containedInkWell: false, + RectCallback rectCallback, + BorderRadius borderRadius, + double radius, + VoidCallback onRemoved, + }) { + return new InkRipple( + controller: controller, + referenceBox: referenceBox, + position: position, + color: color, + containedInkWell: containedInkWell, + rectCallback: rectCallback, + borderRadius: borderRadius, + radius: radius, + onRemoved: onRemoved, + ); + } +} + +/// A visual reaction on a piece of [Material] to user input. +/// +/// A circular ink feature whose origin starts at the input touch point and +/// whose radius expands from 60% of the final radius. The splash origin +/// animates to the center of its [referenceBox]. +/// +/// This object is rarely created directly. Instead of creating an ink ripple, +/// consider using an [InkResponse] or [InkWell] widget, which uses +/// gestures (such as tap and long-press) to trigger ink splashes. This class +/// is used when the [Theme]'s [ThemeData.splashType] is [InkSplashType.ripple]. +/// +/// See also: +/// +/// * [InkSplash], which is an ink splash feature that expands less +/// aggressively than the ripple. +/// * [InkResponse], which uses gestures to trigger ink highlights and ink +/// splashes in the parent [Material]. +/// * [InkWell], which is a rectangular [InkResponse] (the most common type of +/// ink response). +/// * [Material], which is the widget on which the ink splash is painted. +/// * [InkHighlight], which is an ink feature that emphasizes a part of a +/// [Material]. +class InkRipple extends InteractiveInkFeature { + /// Used to specify this type of ink splash for an [InkWell], [InkResponse] + /// or material [Theme]. + static const InteractiveInkFeatureFactory splashFactory = const _InkRippleFactory(); + + /// Begin a ripple, centered at [position] relative to [referenceBox]. + /// + /// The [controller] argument is typically obtained via + /// `Material.of(context)`. + /// + /// If [containedInkWell] is true, then the ripple will be sized to fit + /// the well rectangle, then clipped to it when drawn. The well + /// rectangle is the box returned by [rectCallback], if provided, or + /// otherwise is the bounds of the [referenceBox]. + /// + /// If [containedInkWell] is false, then [rectCallback] should be null. + /// The ink ripple is clipped only to the edges of the [Material]. + /// This is the default. + /// + /// When the ripple is removed, [onRemoved] will be called. + InkRipple({ + @required MaterialInkController controller, + @required RenderBox referenceBox, + @required Offset position, + @required Color color, + bool containedInkWell: false, + RectCallback rectCallback, + BorderRadius borderRadius, + double radius, + VoidCallback onRemoved, + }) : assert(color != null), + assert(position != null), + _position = position, + _borderRadius = borderRadius ?? BorderRadius.zero, + _targetRadius = radius ?? _getTargetRadius(referenceBox, containedInkWell, rectCallback, position), + _clipCallback = _getClipCallback(referenceBox, containedInkWell, rectCallback), + super(controller: controller, referenceBox: referenceBox, color: color, onRemoved: onRemoved) + { + assert(_borderRadius != null); + + // Immediately begin fading-in the initial splash. + _fadeInController = new AnimationController(duration: _kFadeInDuration, vsync: controller.vsync) + ..addListener(controller.markNeedsPaint) + ..forward(); + _fadeIn = new IntTween( + begin: 0, + end: color.alpha, + ).animate(_fadeInController); + + // Controls the splash radius and its center. Starts upon confirm. + _radiusController = new AnimationController(duration: _kUnconfirmedRippleDuration, vsync: controller.vsync) + ..addListener(controller.markNeedsPaint) + ..forward(); + // Initial splash diamater is 60% of the target diameter, final + // diameter is 10dps larger than the target diameter. + _radius = new Tween( + begin: _targetRadius * 0.30, + end: _targetRadius + 5.0, + ).animate( + new CurvedAnimation( + parent: _radiusController, + curve: Curves.ease, + ) + ); + + // Controls the splash radius and its center. Starts upon confirm however its + // Interval delays changes until the radius expansion has completed. + _fadeOutController = new AnimationController(duration: _kFadeOutDuration, vsync: controller.vsync) + ..addListener(controller.markNeedsPaint) + ..addStatusListener(_handleAlphaStatusChanged); + _fadeOut = new IntTween( + begin: color.alpha, + end: 0, + ).animate( + new CurvedAnimation( + parent: _fadeOutController, + curve: const Interval(_kFadeOutIntervalStart, 1.0) + ), + ); + + controller.addInkFeature(this); + } + + final Offset _position; + final BorderRadius _borderRadius; + final double _targetRadius; + final RectCallback _clipCallback; + + Animation _radius; + AnimationController _radiusController; + + Animation _fadeIn; + AnimationController _fadeInController; + + Animation _fadeOut; + AnimationController _fadeOutController; + + @override + void confirm() { + _radiusController + ..duration = _kRadiusDuration + ..forward(); + _fadeOutController.forward(); + } + + @override + void cancel() { + _fadeInController.stop(); + _fadeOutController.animateTo(1.0, duration: _kCancelDuration); + } + + void _handleAlphaStatusChanged(AnimationStatus status) { + if (status == AnimationStatus.completed) + dispose(); + } + + @override + void dispose() { + _radiusController.dispose(); + _fadeInController.dispose(); + _fadeOutController.dispose(); + super.dispose(); + } + + RRect _clipRRectFromRect(Rect rect) { + return new RRect.fromRectAndCorners( + rect, + topLeft: _borderRadius.topLeft, topRight: _borderRadius.topRight, + bottomLeft: _borderRadius.bottomLeft, bottomRight: _borderRadius.bottomRight, + ); + } + + void _clipCanvasWithRect(Canvas canvas, Rect rect, {Offset offset}) { + Rect clipRect = rect; + if (offset != null) { + clipRect = clipRect.shift(offset); + } + if (_borderRadius != BorderRadius.zero) { + canvas.clipRRect(_clipRRectFromRect(clipRect)); + } else { + canvas.clipRect(clipRect); + } + } + + @override + void paintFeature(Canvas canvas, Matrix4 transform) { + final int alpha = _fadeInController.isAnimating ? _fadeIn.value : _fadeOut.value; + final Paint paint = new Paint()..color = color.withAlpha(alpha); + // Splash moves to the center of the reference box. + final Offset center = Offset.lerp( + _position, + referenceBox.size.center(Offset.zero), + Curves.ease.transform(_radiusController.value), + ); + final Offset originOffset = MatrixUtils.getAsTranslation(transform); + if (originOffset == null) { + canvas.save(); + canvas.transform(transform.storage); + if (_clipCallback != null) { + _clipCanvasWithRect(canvas, _clipCallback()); + } + canvas.drawCircle(center, _radius.value, paint); + canvas.restore(); + } else { + if (_clipCallback != null) { + canvas.save(); + _clipCanvasWithRect(canvas, _clipCallback(), offset: originOffset); + } + canvas.drawCircle(center + originOffset, _radius.value, paint); + if (_clipCallback != null) + canvas.restore(); + } + } +} diff --git a/packages/flutter/lib/src/material/ink_splash.dart b/packages/flutter/lib/src/material/ink_splash.dart index fcf4887d0d..200ab5e798 100644 --- a/packages/flutter/lib/src/material/ink_splash.dart +++ b/packages/flutter/lib/src/material/ink_splash.dart @@ -8,6 +8,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; +import 'ink_well.dart'; import 'material.dart'; const Duration _kUnconfirmedSplashDuration = const Duration(seconds: 1); @@ -42,14 +43,48 @@ double _getSplashRadiusForPositionInSize(Size bounds, Offset position) { return math.max(math.max(d1, d2), math.max(d3, d4)).ceilToDouble(); } +class _InkSplashFactory extends InteractiveInkFeatureFactory { + const _InkSplashFactory(); + + @override + InteractiveInkFeature create({ + @required MaterialInkController controller, + @required RenderBox referenceBox, + @required Offset position, + @required Color color, + bool containedInkWell: false, + RectCallback rectCallback, + BorderRadius borderRadius, + double radius, + VoidCallback onRemoved, + }) { + return new InkSplash( + controller: controller, + referenceBox: referenceBox, + position: position, + color: color, + containedInkWell: containedInkWell, + rectCallback: rectCallback, + borderRadius: borderRadius, + radius: radius, + onRemoved: onRemoved, + ); + } +} + /// A visual reaction on a piece of [Material] to user input. /// +/// A circular ink feature whose origin starts at the input touch point +/// and whose radius expands from zero. +/// /// This object is rarely created directly. Instead of creating an ink splash /// directly, consider using an [InkResponse] or [InkWell] widget, which uses /// gestures (such as tap and long-press) to trigger ink splashes. /// /// See also: /// +/// * [InkRipple], which is an ink splash feature that expands more +/// aggressively than this class does. /// * [InkResponse], which uses gestures to trigger ink highlights and ink /// splashes in the parent [Material]. /// * [InkWell], which is a rectangular [InkResponse] (the most common type of @@ -57,7 +92,11 @@ double _getSplashRadiusForPositionInSize(Size bounds, Offset position) { /// * [Material], which is the widget on which the ink splash is painted. /// * [InkHighlight], which is an ink feature that emphasizes a part of a /// [Material]. -class InkSplash extends InkFeature { +class InkSplash extends InteractiveInkFeature { + /// Used to specify this type of ink splash for an [InkWell], [InkResponse] + /// or material [Theme]. + static const InteractiveInkFeatureFactory splashFactory = const _InkSplashFactory(); + /// Begin a splash, centered at position relative to [referenceBox]. /// /// The [controller] argument is typically obtained via @@ -84,12 +123,11 @@ class InkSplash extends InkFeature { double radius, VoidCallback onRemoved, }) : _position = position, - _color = color, _borderRadius = borderRadius, _targetRadius = radius ?? _getTargetRadius(referenceBox, containedInkWell, rectCallback, position), _clipCallback = _getClipCallback(referenceBox, containedInkWell, rectCallback), _repositionToReferenceBox = !containedInkWell, - super(controller: controller, referenceBox: referenceBox, onRemoved: onRemoved) { + super(controller: controller, referenceBox: referenceBox, color: color, onRemoved: onRemoved) { assert(_borderRadius != null); _radiusController = new AnimationController(duration: _kUnconfirmedSplashDuration, vsync: controller.vsync) ..addListener(controller.markNeedsPaint) @@ -121,20 +159,7 @@ class InkSplash extends InkFeature { Animation _alpha; AnimationController _alphaController; - /// The color of the splash. - Color get color => _color; - Color _color; - set color(Color value) { - if (value == _color) - return; - _color = value; - controller.markNeedsPaint(); - } - - - /// The user input is confirmed. - /// - /// Causes the reaction to propagate faster across the material. + @override void confirm() { final int duration = (_targetRadius / _kSplashConfirmedVelocity).floor(); _radiusController @@ -143,9 +168,7 @@ class InkSplash extends InkFeature { _alphaController.forward(); } - /// The user input was canceled. - /// - /// Causes the reaction to gradually disappear. + @override void cancel() { _alphaController.forward(); } @@ -184,7 +207,7 @@ class InkSplash extends InkFeature { @override void paintFeature(Canvas canvas, Matrix4 transform) { - final Paint paint = new Paint()..color = _color.withAlpha(_alpha.value); + final Paint paint = new Paint()..color = color.withAlpha(_alpha.value); Offset center = _position; if (_repositionToReferenceBox) center = Offset.lerp(center, referenceBox.size.center(Offset.zero), _radiusController.value); diff --git a/packages/flutter/lib/src/material/ink_well.dart b/packages/flutter/lib/src/material/ink_well.dart index 1186870281..adb4413077 100644 --- a/packages/flutter/lib/src/material/ink_well.dart +++ b/packages/flutter/lib/src/material/ink_well.dart @@ -12,10 +12,94 @@ 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 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. + 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(); + } +} + +/// 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, + bool containedInkWell: false, + RectCallback rectCallback, + BorderRadius borderRadius, + 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. /// @@ -95,6 +179,7 @@ class InkResponse extends StatefulWidget { this.borderRadius: BorderRadius.zero, this.highlightColor, this.splashColor, + this.splashFactory, this.enableFeedback: true, this.excludeFromSemantics: false, }) : assert(enableFeedback != null), super(key: key); @@ -162,6 +247,7 @@ class InkResponse extends StatefulWidget { /// 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. @@ -174,6 +260,7 @@ class InkResponse extends StatefulWidget { /// /// * [highlightShape], the shape of the highlight. /// * [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 @@ -181,10 +268,25 @@ class InkResponse extends StatefulWidget { /// /// 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 aggresively 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 @@ -255,8 +357,8 @@ class InkResponse extends StatefulWidget { } class _InkResponseState extends State with AutomaticKeepAliveClientMixin { - Set _splashes; - InkSplash _currentSplash; + Set _splashes; + InteractiveInkFeature _currentSplash; InkHighlight _lastHighlight; @override @@ -295,30 +397,43 @@ class _InkResponseState extends State with AutomaticKe updateKeepAlive(); } - void _handleTapDown(TapDownDetails details) { + InteractiveInkFeature _createInkFeature(TapDownDetails details) { + final MaterialInkController inkController = Material.of(context); final RenderBox referenceBox = context.findRenderObject(); - final RectCallback rectCallback = widget.getRectCallback(referenceBox); - InkSplash splash; - splash = new InkSplash( - controller: Material.of(context), + 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; + final BorderRadius borderRadius = widget.borderRadius ?? BorderRadius.zero; + + 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: referenceBox.globalToLocal(details.globalPosition), - color: widget.splashColor ?? Theme.of(context).splashColor, + position: position, + color: color, containedInkWell: widget.containedInkWell, - rectCallback: widget.containedInkWell ? rectCallback : null, + rectCallback: rectCallback, 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() - } + borderRadius: borderRadius, + onRemoved: onRemoved, ); - _splashes ??= new HashSet(); + + return splash; + } + + void _handleTapDown(TapDownDetails details) { + final InteractiveInkFeature splash = _createInkFeature(details); + _splashes ??= new HashSet(); _splashes.add(splash); _currentSplash = splash; updateKeepAlive(); @@ -362,9 +477,9 @@ class _InkResponseState extends State with AutomaticKe @override void deactivate() { if (_splashes != null) { - final Set splashes = _splashes; + final Set splashes = _splashes; _splashes = null; - for (InkSplash splash in splashes) + for (InteractiveInkFeature splash in splashes) splash.dispose(); _currentSplash = null; } @@ -436,6 +551,8 @@ class InkWell extends InkResponse { ValueChanged onHighlightChanged, Color highlightColor, Color splashColor, + InteractiveInkFeatureFactory splashFactory, + double radius, BorderRadius borderRadius, bool enableFeedback: true, bool excludeFromSemantics: false, @@ -450,6 +567,8 @@ class InkWell extends InkResponse { highlightShape: BoxShape.rectangle, highlightColor: highlightColor, splashColor: splashColor, + splashFactory: splashFactory, + radius: radius, borderRadius: borderRadius, enableFeedback: enableFeedback, excludeFromSemantics: excludeFromSemantics, diff --git a/packages/flutter/lib/src/material/theme_data.dart b/packages/flutter/lib/src/material/theme_data.dart index 3821623005..9498b70086 100644 --- a/packages/flutter/lib/src/material/theme_data.dart +++ b/packages/flutter/lib/src/material/theme_data.dart @@ -8,6 +8,8 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'colors.dart'; +import 'ink_splash.dart'; +import 'ink_well.dart' show InteractiveInkFeatureFactory; import 'typography.dart'; /// Describes the contrast needs of a color. @@ -82,6 +84,7 @@ class ThemeData { Color dividerColor, Color highlightColor, Color splashColor, + InteractiveInkFeatureFactory splashFactory, Color selectedRowColor, Color unselectedWidgetColor, Color disabledColor, @@ -118,6 +121,7 @@ class ThemeData { dividerColor ??= isDark ? const Color(0x1FFFFFFF) : const Color(0x1F000000); highlightColor ??= isDark ? _kDarkThemeHighlightColor : _kLightThemeHighlightColor; splashColor ??= isDark ? _kDarkThemeSplashColor : _kLightThemeSplashColor; + splashFactory ??= InkSplash.splashFactory; selectedRowColor ??= Colors.grey[100]; unselectedWidgetColor ??= isDark ? Colors.white70 : Colors.black54; disabledColor ??= isDark ? Colors.white30 : Colors.black26; @@ -156,6 +160,7 @@ class ThemeData { dividerColor: dividerColor, highlightColor: highlightColor, splashColor: splashColor, + splashFactory: splashFactory, selectedRowColor: selectedRowColor, unselectedWidgetColor: unselectedWidgetColor, disabledColor: disabledColor, @@ -196,6 +201,7 @@ class ThemeData { @required this.dividerColor, @required this.highlightColor, @required this.splashColor, + @required this.splashFactory, @required this.selectedRowColor, @required this.unselectedWidgetColor, @required this.disabledColor, @@ -226,6 +232,7 @@ class ThemeData { assert(dividerColor != null), assert(highlightColor != null), assert(splashColor != null), + assert(splashFactory != null), assert(selectedRowColor != null), assert(unselectedWidgetColor != null), assert(disabledColor != null), @@ -317,6 +324,16 @@ class ThemeData { /// The color of ink splashes. See [InkWell]. final Color splashColor; + /// Defines the appearance of ink splashes produces by [InkWell] + /// and [InkResponse]. + /// + /// See also: + /// + /// * [InkSplash.splashFactory], which defines the default splash. + /// * [InkRipple.splashFactory], which defines a splash that spreads out + /// more aggresively than the default. + final InteractiveInkFeatureFactory splashFactory; + /// The color used to highlight selected rows. final Color selectedRowColor; @@ -398,6 +415,7 @@ class ThemeData { Color dividerColor, Color highlightColor, Color splashColor, + InteractiveInkFeatureFactory splashFactory, Color selectedRowColor, Color unselectedWidgetColor, Color disabledColor, @@ -430,6 +448,7 @@ class ThemeData { dividerColor: dividerColor ?? this.dividerColor, highlightColor: highlightColor ?? this.highlightColor, splashColor: splashColor ?? this.splashColor, + splashFactory: splashFactory ?? this.splashFactory, selectedRowColor: selectedRowColor ?? this.selectedRowColor, unselectedWidgetColor: unselectedWidgetColor ?? this.unselectedWidgetColor, disabledColor: disabledColor ?? this.disabledColor, @@ -545,6 +564,7 @@ class ThemeData { dividerColor: Color.lerp(a.dividerColor, b.dividerColor, t), highlightColor: Color.lerp(a.highlightColor, b.highlightColor, t), splashColor: Color.lerp(a.splashColor, b.splashColor, t), + splashFactory: t < 0.5 ? a.splashFactory : b.splashFactory, selectedRowColor: Color.lerp(a.selectedRowColor, b.selectedRowColor, t), unselectedWidgetColor: Color.lerp(a.unselectedWidgetColor, b.unselectedWidgetColor, t), disabledColor: Color.lerp(a.disabledColor, b.disabledColor, t), @@ -583,6 +603,7 @@ class ThemeData { (otherData.dividerColor == dividerColor) && (otherData.highlightColor == highlightColor) && (otherData.splashColor == splashColor) && + (otherData.splashFactory == splashFactory) && (otherData.selectedRowColor == selectedRowColor) && (otherData.unselectedWidgetColor == unselectedWidgetColor) && (otherData.disabledColor == disabledColor) && @@ -618,6 +639,7 @@ class ThemeData { dividerColor, highlightColor, splashColor, + splashFactory, selectedRowColor, unselectedWidgetColor, disabledColor, @@ -627,8 +649,8 @@ class ThemeData { textSelectionHandleColor, backgroundColor, accentColor, - accentColorBrightness, hashValues( // Too many values. + accentColorBrightness, indicatorColor, dialogBackgroundColor, hintColor, diff --git a/packages/flutter/test/material/ink_paint_test.dart b/packages/flutter/test/material/ink_paint_test.dart index 776908b434..50dfe219ad 100644 --- a/packages/flutter/test/material/ink_paint_test.dart +++ b/packages/flutter/test/material/ink_paint_test.dart @@ -9,7 +9,7 @@ import 'package:flutter_test/flutter_test.dart'; import '../rendering/mock_canvas.dart'; void main() { - testWidgets('Does the ink widget render a border radius', (WidgetTester tester) async { + testWidgets('The inkwell widget renders an ink splash', (WidgetTester tester) async { final Color highlightColor = const Color(0xAAFF0000); final Color splashColor = const Color(0xAA0000FF); final BorderRadius borderRadius = new BorderRadius.circular(6.0); @@ -50,4 +50,128 @@ void main() { await gesture.up(); }); + + testWidgets('The inkwell widget renders an ink ripple', (WidgetTester tester) async { + final Color highlightColor = const Color(0xAAFF0000); + final Color splashColor = const Color(0xB40000FF); + final BorderRadius borderRadius = new BorderRadius.circular(6.0); + + await tester.pumpWidget( + new Material( + child: new Center( + child: new Container( + width: 100.0, + height: 100.0, + child: new InkWell( + borderRadius: borderRadius, + highlightColor: highlightColor, + splashColor: splashColor, + onTap: () { }, + radius: 100.0, + splashFactory: InkRipple.splashFactory, + ), + ), + ), + ), + ); + + final Offset tapDownOffset = tester.getTopLeft(find.byType(InkWell)); + final Offset inkWellCenter = tester.getCenter(find.byType(InkWell)); + //final TestGesture gesture = await tester.startGesture(tapDownOffset); + await tester.tapAt(tapDownOffset); + await tester.pump(); // start gesture + + final RenderBox box = Material.of(tester.element(find.byType(InkWell))) as dynamic; + + bool offsetsAreClose(Offset a, Offset b) => (a - b).distance < 1.0; + bool radiiAreClose(double a, double b) => (a - b).abs() < 1.0; + + // Initially the ripple's center is where the tap occurred, + expect(box, paints..something((Symbol method, List arguments) { + if (method != #drawCircle) + return false; + final Offset center = arguments[0]; + final double radius = arguments[1]; + final Paint paint = arguments[2]; + if (offsetsAreClose(center, tapDownOffset) && radius == 30.0 && paint.color.alpha == 0) + return true; + throw ''' + Expected: center == $tapDownOffset, radius == 30.0, alpha == 0 + Found: center == $center radius == $radius alpha == ${paint.color.alpha}'''; + })); + + // The ripple fades in for 75ms. During that time its alpha is eased from + // 0 to the splashColor's alpha value and its center moves towards the + // center of the ink well. + await tester.pump(const Duration(milliseconds: 50)); + expect(box, paints..something((Symbol method, List arguments) { + if (method != #drawCircle) + return false; + final Offset center = arguments[0]; + final double radius = arguments[1]; + final Paint paint = arguments[2]; + final Offset expectedCenter = tapDownOffset + const Offset(17.0, 17.0); + final double expectedRadius = 56.0; + if (offsetsAreClose(center, expectedCenter) && radiiAreClose(radius, expectedRadius) && paint.color.alpha == 120) + return true; + throw ''' + Expected: center == $expectedCenter, radius == $expectedRadius, alpha == 120 + Found: center == $center radius == $radius alpha == ${paint.color.alpha}'''; + })); + + // At 75ms the ripple has fade in: it's alpha matches the splashColor's + // alpha and its center has moved closer to the ink well's center. + await tester.pump(const Duration(milliseconds: 25)); + expect(box, paints..something((Symbol method, List arguments) { + if (method != #drawCircle) + return false; + final Offset center = arguments[0]; + final double radius = arguments[1]; + final Paint paint = arguments[2]; + final Offset expectedCenter = tapDownOffset + const Offset(29.0, 29.0); + final double expectedRadius = 73.0; + if (offsetsAreClose(center, expectedCenter) && radiiAreClose(radius, expectedRadius) && paint.color.alpha == 180) + return true; + throw ''' + Expected: center == $expectedCenter, radius == $expectedRadius, alpha == 180 + Found: center == $center radius == $radius alpha == ${paint.color.alpha}'''; + })); + + // At this point the splash radius has expanded to its limit: 5 past the + // ink well's radius parameter. The splash center has moved to its final + // location at the inkwell's center and the fade-out is about to start. + await tester.pump(const Duration(milliseconds: 225)); + expect(box, paints..something((Symbol method, List arguments) { + if (method != #drawCircle) + return false; + final Offset center = arguments[0]; + final double radius = arguments[1]; + final Paint paint = arguments[2]; + final Offset expectedCenter = inkWellCenter; + final double expectedRadius = 105.0; + if (offsetsAreClose(center, expectedCenter) && radiiAreClose(radius, expectedRadius) && paint.color.alpha == 180) + return true; + throw ''' + Expected: center == $expectedCenter, radius == $expectedRadius, alpha == 180 + Found: center == $center radius == $radius alpha == ${paint.color.alpha}'''; + })); + + // After another 150ms the fade-out is complete. + await tester.pump(const Duration(milliseconds: 150)); + expect(box, paints..something((Symbol method, List arguments) { + if (method != #drawCircle) + return false; + final Offset center = arguments[0]; + final double radius = arguments[1]; + final Paint paint = arguments[2]; + final Offset expectedCenter = inkWellCenter; + final double expectedRadius = 105.0; + if (offsetsAreClose(center, expectedCenter) && radiiAreClose(radius, expectedRadius) && paint.color.alpha == 0) + return true; + throw ''' + Expected: center == $expectedCenter, radius == $expectedRadius, alpha == 0 + Found: center == $center radius == $radius alpha == ${paint.color.alpha}'''; + })); + + }); }