ink_splash.dart 7.49 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5 6 7 8 9
// 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/rendering.dart';
import 'package:flutter/widgets.dart';

10
import 'ink_well.dart';
11 12
import 'material.dart';

13 14
const Duration _kUnconfirmedSplashDuration = Duration(seconds: 1);
const Duration _kSplashFadeDuration = Duration(milliseconds: 200);
15 16 17 18

const double _kSplashInitialSize = 0.0; // logical pixels
const double _kSplashConfirmedVelocity = 1.0; // logical pixels per millisecond

19
RectCallback? _getClipCallback(RenderBox referenceBox, bool containedInkWell, RectCallback? rectCallback) {
20 21 22 23 24
  if (rectCallback != null) {
    assert(containedInkWell);
    return rectCallback;
  }
  if (containedInkWell)
25
    return () => Offset.zero & referenceBox.size;
26 27 28
  return null;
}

29
double _getTargetRadius(RenderBox referenceBox, bool containedInkWell, RectCallback? rectCallback, Offset position) {
30 31
  if (containedInkWell) {
    final Size size = rectCallback != null ? rectCallback().size : referenceBox.size;
32
    return _getSplashRadiusForPositionInSize(size, position);
33 34 35 36
  }
  return Material.defaultSplashRadius;
}

37
double _getSplashRadiusForPositionInSize(Size bounds, Offset position) {
38 39 40 41
  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;
42 43 44
  return math.max(math.max(d1, d2), math.max(d3, d4)).ceilToDouble();
}

45 46 47 48 49
class _InkSplashFactory extends InteractiveInkFeatureFactory {
  const _InkSplashFactory();

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

78 79
/// A visual reaction on a piece of [Material] to user input.
///
80 81 82
/// A circular ink feature whose origin starts at the input touch point
/// and whose radius expands from zero.
///
83 84 85 86 87 88
/// 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:
///
89 90
///  * [InkRipple], which is an ink splash feature that expands more
///    aggressively than this class does.
91 92 93 94 95 96 97
///  * [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].
98 99
///  * [Ink], a convenience widget for drawing images and other decorations on
///    Material widgets.
100
class InkSplash extends InteractiveInkFeature {
101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116
  /// Begin a splash, centered at position relative to [referenceBox].
  ///
  /// The [controller] argument is typically obtained via
  /// `Material.of(context)`.
  ///
  /// If `containedInkWell` is true, then the splash 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 splash is clipped only to the edges of the [Material].
  /// This is the default.
  ///
  /// When the splash is removed, `onRemoved` will be called.
  InkSplash({
117 118 119 120 121
    required MaterialInkController controller,
    required RenderBox referenceBox,
    required TextDirection textDirection,
    Offset? position,
    required Color color,
122
    bool containedInkWell = false,
123 124 125 126 127
    RectCallback? rectCallback,
    BorderRadius? borderRadius,
    ShapeBorder? customBorder,
    double? radius,
    VoidCallback? onRemoved,
128 129
  }) : assert(textDirection != null),
       _position = position,
Ian Hickson's avatar
Ian Hickson committed
130
       _borderRadius = borderRadius ?? BorderRadius.zero,
131
       _customBorder = customBorder,
132
       _targetRadius = radius ?? _getTargetRadius(referenceBox, containedInkWell, rectCallback, position!),
133 134
       _clipCallback = _getClipCallback(referenceBox, containedInkWell, rectCallback),
       _repositionToReferenceBox = !containedInkWell,
135
       _textDirection = textDirection,
136
       super(controller: controller, referenceBox: referenceBox, color: color, onRemoved: onRemoved) {
137
    assert(_borderRadius != null);
138
    _radiusController = AnimationController(duration: _kUnconfirmedSplashDuration, vsync: controller.vsync)
139 140
      ..addListener(controller.markNeedsPaint)
      ..forward();
141
    _radius = _radiusController.drive(Tween<double>(
142
      begin: _kSplashInitialSize,
143 144
      end: _targetRadius,
    ));
145
    _alphaController = AnimationController(duration: _kSplashFadeDuration, vsync: controller.vsync)
146 147
      ..addListener(controller.markNeedsPaint)
      ..addStatusListener(_handleAlphaStatusChanged);
148
    _alpha = _alphaController!.drive(IntTween(
149
      begin: color.alpha,
150 151
      end: 0,
    ));
152 153 154 155

    controller.addInkFeature(this);
  }

156
  final Offset? _position;
157
  final BorderRadius _borderRadius;
158
  final ShapeBorder? _customBorder;
159
  final double _targetRadius;
160
  final RectCallback? _clipCallback;
161
  final bool _repositionToReferenceBox;
162
  final TextDirection _textDirection;
163

164 165
  late Animation<double> _radius;
  late AnimationController _radiusController;
166

167 168
  late Animation<int> _alpha;
  AnimationController? _alphaController;
169

170 171 172 173
  /// Used to specify this type of ink splash for an [InkWell], [InkResponse]
  /// or material [Theme].
  static const InteractiveInkFeatureFactory splashFactory = _InkSplashFactory();

174
  @override
175 176 177
  void confirm() {
    final int duration = (_targetRadius / _kSplashConfirmedVelocity).floor();
    _radiusController
178
      ..duration = Duration(milliseconds: duration)
179
      ..forward();
180
    _alphaController!.forward();
181 182
  }

183
  @override
184
  void cancel() {
185
    _alphaController?.forward();
186 187 188 189 190 191 192 193 194 195
  }

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

  @override
  void dispose() {
    _radiusController.dispose();
196
    _alphaController!.dispose();
197
    _alphaController = null;
198 199 200 201 202
    super.dispose();
  }

  @override
  void paintFeature(Canvas canvas, Matrix4 transform) {
203
    final Paint paint = Paint()..color = color.withAlpha(_alpha.value);
204
    Offset? center = _position;
205
    if (_repositionToReferenceBox)
206
      center = Offset.lerp(center, referenceBox.size.center(Offset.zero), _radiusController.value);
207 208 209 210
    paintInkCircle(
      canvas: canvas,
      transform: transform,
      paint: paint,
211
      center: center!,
212 213 214 215 216 217
      textDirection: _textDirection,
      radius: _radius.value,
      customBorder: _customBorder,
      borderRadius: _borderRadius,
      clipCallback: _clipCallback,
    );
218 219
  }
}