ink_ripple.dart 8.86 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5 6
// @dart = 2.8

7 8 9 10 11 12 13 14
import 'dart:math' as math;

import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';

import 'ink_well.dart';
import 'material.dart';

15 16 17 18 19
const Duration _kUnconfirmedRippleDuration = Duration(seconds: 1);
const Duration _kFadeInDuration = Duration(milliseconds: 75);
const Duration _kRadiusDuration = Duration(milliseconds: 225);
const Duration _kFadeOutDuration = Duration(milliseconds: 375);
const Duration _kCancelDuration = Duration(milliseconds: 75);
20

21 22
// The fade out begins 225ms after the _fadeOutController starts. See confirm().
const double _kFadeOutIntervalStart = 225.0 / 375.0;
23 24 25 26 27 28 29 30 31 32 33 34

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) {
35 36 37 38
  final Size size = rectCallback != null ? rectCallback().size : referenceBox.size;
  final double d1 = size.bottomRight(Offset.zero).distance;
  final double d2 = (size.topRight(Offset.zero) - size.bottomLeft(Offset.zero)).distance;
  return math.max(d1, d2) / 2.0;
39 40 41 42 43 44 45 46 47 48 49
}

class _InkRippleFactory extends InteractiveInkFeatureFactory {
  const _InkRippleFactory();

  @override
  InteractiveInkFeature create({
    @required MaterialInkController controller,
    @required RenderBox referenceBox,
    @required Offset position,
    @required Color color,
50
    @required TextDirection textDirection,
51
    bool containedInkWell = false,
52 53
    RectCallback rectCallback,
    BorderRadius borderRadius,
54
    ShapeBorder customBorder,
55 56 57
    double radius,
    VoidCallback onRemoved,
  }) {
58
    return InkRipple(
59 60 61 62 63 64 65
      controller: controller,
      referenceBox: referenceBox,
      position: position,
      color: color,
      containedInkWell: containedInkWell,
      rectCallback: rectCallback,
      borderRadius: borderRadius,
66
      customBorder: customBorder,
67 68
      radius: radius,
      onRemoved: onRemoved,
69
      textDirection: textDirection,
70 71 72 73 74 75 76 77 78 79 80 81 82
    );
  }
}

/// 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
83
/// is used when the [Theme]'s [ThemeData.splashFactory] is [InkRipple.splashFactory].
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116
///
/// 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 {
  /// 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,
117
    @required TextDirection textDirection,
118
    bool containedInkWell = false,
119 120
    RectCallback rectCallback,
    BorderRadius borderRadius,
121
    ShapeBorder customBorder,
122 123 124 125
    double radius,
    VoidCallback onRemoved,
  }) : assert(color != null),
       assert(position != null),
126
       assert(textDirection != null),
127 128
       _position = position,
       _borderRadius = borderRadius ?? BorderRadius.zero,
129
       _customBorder = customBorder,
130
       _textDirection = textDirection,
131 132
       _targetRadius = radius ?? _getTargetRadius(referenceBox, containedInkWell, rectCallback, position),
       _clipCallback = _getClipCallback(referenceBox, containedInkWell, rectCallback),
133
       super(controller: controller, referenceBox: referenceBox, color: color, onRemoved: onRemoved) {
134 135 136
    assert(_borderRadius != null);

    // Immediately begin fading-in the initial splash.
137
    _fadeInController = AnimationController(duration: _kFadeInDuration, vsync: controller.vsync)
138 139
      ..addListener(controller.markNeedsPaint)
      ..forward();
140
    _fadeIn = _fadeInController.drive(IntTween(
141 142
      begin: 0,
      end: color.alpha,
143
    ));
144 145

    // Controls the splash radius and its center. Starts upon confirm.
146
    _radiusController = AnimationController(duration: _kUnconfirmedRippleDuration, vsync: controller.vsync)
147 148
      ..addListener(controller.markNeedsPaint)
      ..forward();
Josh Soref's avatar
Josh Soref committed
149
     // Initial splash diameter is 60% of the target diameter, final
150
     // diameter is 10dps larger than the target diameter.
151 152 153 154 155
    _radius = _radiusController.drive(
      Tween<double>(
        begin: _targetRadius * 0.30,
        end: _targetRadius + 5.0,
      ).chain(_easeCurveTween),
156 157 158 159
    );

    // Controls the splash radius and its center. Starts upon confirm however its
    // Interval delays changes until the radius expansion has completed.
160
    _fadeOutController = AnimationController(duration: _kFadeOutDuration, vsync: controller.vsync)
161 162
      ..addListener(controller.markNeedsPaint)
      ..addStatusListener(_handleAlphaStatusChanged);
163 164 165 166 167
    _fadeOut = _fadeOutController.drive(
      IntTween(
        begin: color.alpha,
        end: 0,
      ).chain(_fadeOutIntervalTween),
168 169 170 171 172 173 174
    );

    controller.addInkFeature(this);
  }

  final Offset _position;
  final BorderRadius _borderRadius;
175
  final ShapeBorder _customBorder;
176 177
  final double _targetRadius;
  final RectCallback _clipCallback;
178
  final TextDirection _textDirection;
179 180 181 182 183 184 185 186 187 188

  Animation<double> _radius;
  AnimationController _radiusController;

  Animation<int> _fadeIn;
  AnimationController _fadeInController;

  Animation<int> _fadeOut;
  AnimationController _fadeOutController;

189 190 191 192 193 194 195
  /// Used to specify this type of ink splash for an [InkWell], [InkResponse]
  /// or material [Theme].
  static const InteractiveInkFeatureFactory splashFactory = _InkRippleFactory();

  static final Animatable<double> _easeCurveTween = CurveTween(curve: Curves.ease);
  static final Animatable<double> _fadeOutIntervalTween = CurveTween(curve: const Interval(_kFadeOutIntervalStart, 1.0));

196 197 198 199 200
  @override
  void confirm() {
    _radiusController
      ..duration = _kRadiusDuration
      ..forward();
Josh Soref's avatar
Josh Soref committed
201
    // This confirm may have been preceded by a cancel.
202
    _fadeInController.forward();
203
    _fadeOutController.animateTo(1.0, duration: _kFadeOutDuration);
204 205 206 207 208
  }

  @override
  void cancel() {
    _fadeInController.stop();
209 210
    // Watch out: setting _fadeOutController's value to 1.0 will
    // trigger a call to _handleAlphaStatusChanged() which will
211
    // dispose _fadeOutController.
212 213 214 215
    final double fadeOutValue = 1.0 - _fadeInController.value;
    _fadeOutController.value = fadeOutValue;
    if (fadeOutValue < 1.0)
      _fadeOutController.animateTo(1.0, duration: _kCancelDuration);
216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233
  }

  void _handleAlphaStatusChanged(AnimationStatus status) {
    if (status == AnimationStatus.completed)
      dispose();
  }

  @override
  void dispose() {
    _radiusController.dispose();
    _fadeInController.dispose();
    _fadeOutController.dispose();
    super.dispose();
  }

  @override
  void paintFeature(Canvas canvas, Matrix4 transform) {
    final int alpha = _fadeInController.isAnimating ? _fadeIn.value : _fadeOut.value;
234
    final Paint paint = Paint()..color = color.withAlpha(alpha);
235 236 237 238 239 240
    // 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),
    );
241 242 243 244 245 246 247 248 249 250 251
    paintInkCircle(
      canvas: canvas,
      transform: transform,
      paint: paint,
      center: center,
      textDirection: _textDirection,
      radius: _radius.value,
      customBorder: _customBorder,
      borderRadius: _borderRadius,
      clipCallback: _clipCallback,
    );
252 253
  }
}