Add support for a new kind of InkSplash: the "ripple" (#13986)

This commit is contained in:
Hans Muller 2018-01-09 14:43:08 -08:00 committed by GitHub
parent c5c63dfd47
commit 03e8ab1f53
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 614 additions and 60 deletions

View file

@ -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';

View file

@ -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<int> _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;

View file

@ -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<double>(
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<double> _radius;
AnimationController _radiusController;
Animation<int> _fadeIn;
AnimationController _fadeInController;
Animation<int> _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();
}
}
}

View file

@ -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<int> _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);

View file

@ -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<T extends InkResponse> extends State<T> with AutomaticKeepAliveClientMixin {
Set<InkSplash> _splashes;
InkSplash _currentSplash;
Set<InteractiveInkFeature> _splashes;
InteractiveInkFeature _currentSplash;
InkHighlight _lastHighlight;
@override
@ -295,20 +397,16 @@ class _InkResponseState<T extends InkResponse> extends State<T> 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),
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: () {
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);
@ -317,8 +415,25 @@ class _InkResponseState<T extends InkResponse> extends State<T> with AutomaticKe
updateKeepAlive();
} // else we're probably in deactivate()
}
splash = (widget.splashFactory ?? Theme.of(context).splashFactory).create(
controller: inkController,
referenceBox: referenceBox,
position: position,
color: color,
containedInkWell: widget.containedInkWell,
rectCallback: rectCallback,
radius: widget.radius,
borderRadius: borderRadius,
onRemoved: onRemoved,
);
_splashes ??= new HashSet<InkSplash>();
return splash;
}
void _handleTapDown(TapDownDetails details) {
final InteractiveInkFeature splash = _createInkFeature(details);
_splashes ??= new HashSet<InteractiveInkFeature>();
_splashes.add(splash);
_currentSplash = splash;
updateKeepAlive();
@ -362,9 +477,9 @@ class _InkResponseState<T extends InkResponse> extends State<T> with AutomaticKe
@override
void deactivate() {
if (_splashes != null) {
final Set<InkSplash> splashes = _splashes;
final Set<InteractiveInkFeature> 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<bool> 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,

View file

@ -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,

View file

@ -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<dynamic> 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<dynamic> 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<dynamic> 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<dynamic> 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<dynamic> 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}''';
}));
});
}