mirror of
https://github.com/flutter/flutter
synced 2024-10-13 19:52:53 +00:00
Add support for a new kind of InkSplash: the "ripple" (#13986)
This commit is contained in:
parent
c5c63dfd47
commit
03e8ab1f53
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
|
|
275
packages/flutter/lib/src/material/ink_ripple.dart
Normal file
275
packages/flutter/lib/src/material/ink_ripple.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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,30 +397,43 @@ 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),
|
||||
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<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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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}''';
|
||||
}));
|
||||
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue