1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5/// @docImport 'button_style.dart';
6/// @docImport 'elevated_button.dart';
7/// @docImport 'outlined_button.dart';
8/// @docImport 'text_button.dart';
9/// @docImport 'theme.dart';
10/// @docImport 'theme_data.dart';
11library;
12
13import 'dart:math' as math;
14import 'dart:ui' as ui;
15
16import 'package:flutter/widgets.dart';
17import 'package:vector_math/vector_math_64.dart';
18
19import 'ink_well.dart';
20import 'material.dart';
21
22/// Begin a Material 3 ink sparkle ripple, centered at the tap or click position
23/// relative to the [referenceBox].
24///
25/// To use this effect, pass an instance of [splashFactory] to the
26/// `splashFactory` parameter of either the Material [ThemeData] or any
27/// component that has a `splashFactory` parameter, such as buttons:
28/// - [ElevatedButton]
29/// - [TextButton]
30/// - [OutlinedButton]
31///
32/// The [controller] argument is typically obtained via
33/// `Material.of(context)`.
34///
35/// If `containedInkWell` is true, then the effect will be sized to fit
36/// the well rectangle, and clipped to it when drawn. The well
37/// rectangle is the box returned by `rectCallback`, if provided, or
38/// otherwise is the bounds of the [referenceBox].
39///
40/// If `containedInkWell` is false, then `rectCallback` should be null.
41/// The ink ripple is clipped only to the edges of the [Material].
42/// This is the default.
43///
44/// When the ripple is removed, [onRemoved] will be called.
45///
46/// {@tool snippet}
47///
48/// For typical use, pass the [InkSparkle.splashFactory] to the `splashFactory`
49/// parameter of a button style or [ThemeData].
50///
51/// ```dart
52/// ElevatedButton(
53/// style: ElevatedButton.styleFrom(splashFactory: InkSparkle.splashFactory),
54/// child: const Text('Sparkle!'),
55/// onPressed: () { },
56/// )
57/// ```
58/// {@end-tool}
59class InkSparkle extends InteractiveInkFeature {
60 /// Begin a sparkly ripple effect, centered at [position] relative to
61 /// [referenceBox].
62 ///
63 /// The [color] defines the color of the splash itself. The sparkles are
64 /// always white.
65 ///
66 /// The [controller] argument is typically obtained via
67 /// `Material.of(context)`.
68 ///
69 /// [textDirection] is used by [customBorder] if it is non-null. This allows
70 /// the [customBorder]'s path to be properly defined if it was the path was
71 /// expressed in terms of "start" and "end" instead of
72 /// "left" and "right".
73 ///
74 /// If [containedInkWell] is true, then the ripple will be sized to fit
75 /// the well rectangle, then clipped to it when drawn. The well
76 /// rectangle is the box returned by [rectCallback], if provided, or
77 /// otherwise is the bounds of the [referenceBox].
78 ///
79 /// If [containedInkWell] is false, then [rectCallback] should be null.
80 /// The ink ripple is clipped only to the edges of the [Material].
81 /// This is the default.
82 ///
83 /// Clipping can happen in 3 different ways:
84 /// 1. If [customBorder] is provided, it is used to determine the path for
85 /// clipping.
86 /// 2. If [customBorder] is null, and [borderRadius] is provided, then the
87 /// canvas is clipped by an [RRect] created from [borderRadius].
88 /// 3. If [borderRadius] is the default [BorderRadius.zero], then the canvas
89 /// is clipped with [rectCallback].
90 /// When the ripple is removed, [onRemoved] will be called.
91 ///
92 /// [turbulenceSeed] can be passed if a non random seed should be used for
93 /// the turbulence and sparkles. By default, the seed is a random number
94 /// between 0.0 and 1000.0.
95 ///
96 /// Turbulence is an input to the shader and helps to provides a more natural,
97 /// non-circular, "splash" effect.
98 ///
99 /// Sparkle randomization is also driven by the [turbulenceSeed]. Sparkles are
100 /// identified in the shader as "noise", and the sparkles are derived from
101 /// pseudorandom triangular noise.
102 InkSparkle({
103 required super.controller,
104 required super.referenceBox,
105 required super.color,
106 required Offset position,
107 required TextDirection textDirection,
108 bool containedInkWell = true,
109 RectCallback? rectCallback,
110 BorderRadius? borderRadius,
111 super.customBorder,
112 double? radius,
113 super.onRemoved,
114 double? turbulenceSeed,
115 }) : assert(containedInkWell || rectCallback == null),
116 _color = color,
117 _position = position,
118 _borderRadius = borderRadius ?? BorderRadius.zero,
119 _textDirection = textDirection,
120 _targetRadius =
121 (radius ?? _getTargetRadius(referenceBox, containedInkWell, rectCallback, position)) *
122 _targetRadiusMultiplier,
123 _clipCallback = _getClipCallback(referenceBox, containedInkWell, rectCallback) {
124 // InkSparkle will not be painted until the async compilation completes.
125 _InkSparkleFactory.initializeShader();
126 controller.addInkFeature(this);
127
128 // Immediately begin animating the ink.
129 _animationController =
130 AnimationController(duration: _animationDuration, vsync: controller.vsync)
131 ..addListener(controller.markNeedsPaint)
132 ..addStatusListener(_handleStatusChanged)
133 ..forward();
134
135 _radiusScale = TweenSequence<double>(<TweenSequenceItem<double>>[
136 TweenSequenceItem<double>(tween: CurveTween(curve: Curves.fastOutSlowIn), weight: 75),
137 TweenSequenceItem<double>(tween: ConstantTween<double>(1.0), weight: 25),
138 ]).animate(_animationController);
139
140 // Functionally equivalent to Android 12's SkSL:
141 //`return mix(u_touch, u_resolution, saturate(in_radius_scale * 2.0))`
142 final Tween<Vector2> centerTween = Tween<Vector2>(
143 begin: Vector2.array(<double>[_position.dx, _position.dy]),
144 end: Vector2.array(<double>[referenceBox.size.width / 2, referenceBox.size.height / 2]),
145 );
146 final Animation<double> centerProgress = TweenSequence<double>(<TweenSequenceItem<double>>[
147 TweenSequenceItem<double>(tween: Tween<double>(begin: 0.0, end: 1.0), weight: 50),
148 TweenSequenceItem<double>(tween: ConstantTween<double>(1.0), weight: 50),
149 ]).animate(_radiusScale);
150 _center = centerTween.animate(centerProgress);
151
152 _alpha = TweenSequence<double>(<TweenSequenceItem<double>>[
153 TweenSequenceItem<double>(tween: Tween<double>(begin: 0.0, end: 1.0), weight: 13),
154 TweenSequenceItem<double>(tween: ConstantTween<double>(1.0), weight: 27),
155 TweenSequenceItem<double>(tween: Tween<double>(begin: 1.0, end: 0.0), weight: 60),
156 ]).animate(_animationController);
157
158 _sparkleAlpha = TweenSequence<double>(<TweenSequenceItem<double>>[
159 TweenSequenceItem<double>(tween: Tween<double>(begin: 0.0, end: 1.0), weight: 13),
160 TweenSequenceItem<double>(tween: ConstantTween<double>(1.0), weight: 27),
161 TweenSequenceItem<double>(tween: Tween<double>(begin: 1.0, end: 0.0), weight: 50),
162 ]).animate(_animationController);
163
164 // Creates an element of randomness so that ink emanating from the same
165 // pixel have slightly different rings and sparkles.
166 assert(() {
167 // In tests, randomness can cause flakes. So if a seed has not
168 // already been specified (i.e. for the purpose of the test), set it to
169 // the constant turbulence seed.
170 turbulenceSeed ??= _InkSparkleFactory.constantSeed;
171 return true;
172 }());
173 _turbulenceSeed = turbulenceSeed ?? math.Random().nextDouble() * 1000.0;
174 }
175
176 void _handleStatusChanged(AnimationStatus status) {
177 if (status.isCompleted) {
178 dispose();
179 }
180 }
181
182 static const Duration _animationDuration = Duration(milliseconds: 617);
183 static const double _targetRadiusMultiplier = 2.3;
184 static const double _rotateRight = math.pi * 0.0078125;
185 static const double _rotateLeft = -_rotateRight;
186 static const double _noiseDensity = 2.1;
187
188 late AnimationController _animationController;
189
190 // The Android 12 version has these values calculated in the GLSL. They are
191 // constant for every pixel in the animation, so the Flutter implementation
192 // computes these animation values in software in order to simplify the shader
193 // implementation and provide better performance on most devices.
194 late Animation<Vector2> _center;
195 late Animation<double> _radiusScale;
196 late Animation<double> _alpha;
197 late Animation<double> _sparkleAlpha;
198
199 late double _turbulenceSeed;
200
201 final Color _color;
202 final Offset _position;
203 final BorderRadius _borderRadius;
204 final double _targetRadius;
205 final RectCallback? _clipCallback;
206 final TextDirection _textDirection;
207
208 late final ui.FragmentShader _fragmentShader;
209 bool _fragmentShaderInitialized = false;
210
211 /// Used to specify this type of ink splash for an [InkWell], [InkResponse],
212 /// material [Theme], or [ButtonStyle].
213 ///
214 /// Since no `turbulenceSeed` is passed, the effect will be random for
215 /// subsequent presses in the same position.
216 static const InteractiveInkFeatureFactory splashFactory = _InkSparkleFactory();
217
218 /// Used to specify this type of ink splash for an [InkWell], [InkResponse],
219 /// material [Theme], or [ButtonStyle].
220 ///
221 /// Since a `turbulenceSeed` is passed, the effect will not be random for
222 /// subsequent presses in the same position. This can be used for testing.
223 static const InteractiveInkFeatureFactory constantTurbulenceSeedSplashFactory =
224 _InkSparkleFactory.constantTurbulenceSeed();
225
226 @override
227 void dispose() {
228 _animationController.stop();
229 _animationController.dispose();
230 if (_fragmentShaderInitialized) {
231 _fragmentShader.dispose();
232 }
233 super.dispose();
234 }
235
236 @override
237 void paintFeature(Canvas canvas, Matrix4 transform) {
238 assert(_animationController.isAnimating);
239
240 // InkSparkle can only paint if its shader has been compiled.
241 if (_InkSparkleFactory._program == null) {
242 // Skipping paintFeature because the shader it relies on is not ready to
243 // be used. InkSparkleFactory.initializeShader must complete
244 // before InkSparkle can paint.
245 return;
246 }
247
248 if (!_fragmentShaderInitialized) {
249 _fragmentShader = _InkSparkleFactory._program!.fragmentShader();
250 _fragmentShaderInitialized = true;
251 }
252
253 canvas.save();
254 _transformCanvas(canvas: canvas, transform: transform);
255 if (_clipCallback != null) {
256 _clipCanvas(
257 canvas: canvas,
258 clipCallback: _clipCallback,
259 textDirection: _textDirection,
260 customBorder: customBorder,
261 borderRadius: _borderRadius,
262 );
263 }
264
265 _updateFragmentShader();
266
267 final Paint paint = Paint()..shader = _fragmentShader;
268 if (_clipCallback != null) {
269 canvas.drawRect(_clipCallback(), paint);
270 } else {
271 canvas.drawPaint(paint);
272 }
273 canvas.restore();
274 }
275
276 double get _width => referenceBox.size.width;
277 double get _height => referenceBox.size.height;
278
279 /// All double values for uniforms come from the Android 12 ripple
280 /// implementation from the following files:
281 /// - https://cs.android.com/android/platform/superproject/+/main:frameworks/base/graphics/java/android/graphics/drawable/RippleShader.java
282 /// - https://cs.android.com/android/platform/superproject/+/main:frameworks/base/graphics/java/android/graphics/drawable/RippleDrawable.java
283 /// - https://cs.android.com/android/platform/superproject/+/main:frameworks/base/graphics/java/android/graphics/drawable/RippleAnimationSession.java
284 void _updateFragmentShader() {
285 const double turbulenceScale = 1.5;
286 final double turbulencePhase = _turbulenceSeed + _radiusScale.value;
287 final double noisePhase = turbulencePhase;
288 final double rotation1 = turbulencePhase * _rotateRight + 1.7 * math.pi;
289 final double rotation2 = turbulencePhase * _rotateLeft + 2.0 * math.pi;
290 final double rotation3 = turbulencePhase * _rotateRight + 2.75 * math.pi;
291
292 _fragmentShader
293 // uColor
294 ..setFloat(0, _color.red / 255.0)
295 ..setFloat(1, _color.green / 255.0)
296 ..setFloat(2, _color.blue / 255.0)
297 ..setFloat(3, _color.alpha / 255.0)
298 // Composite 1 (u_alpha, u_sparkle_alpha, u_blur, u_radius_scale)
299 ..setFloat(4, _alpha.value)
300 ..setFloat(5, _sparkleAlpha.value)
301 ..setFloat(6, 1.0)
302 ..setFloat(7, _radiusScale.value)
303 // uCenter
304 ..setFloat(8, _center.value.x)
305 ..setFloat(9, _center.value.y)
306 // uMaxRadius
307 ..setFloat(10, _targetRadius)
308 // uResolutionScale
309 ..setFloat(11, 1.0 / _width)
310 ..setFloat(12, 1.0 / _height)
311 // uNoiseScale
312 ..setFloat(13, _noiseDensity / _width)
313 ..setFloat(14, _noiseDensity / _height)
314 // uNoisePhase
315 ..setFloat(15, noisePhase / 1000.0)
316 // uCircle1
317 ..setFloat(
318 16,
319 turbulenceScale * 0.5 + (turbulencePhase * 0.01 * math.cos(turbulenceScale * 0.55)),
320 )
321 ..setFloat(
322 17,
323 turbulenceScale * 0.5 + (turbulencePhase * 0.01 * math.sin(turbulenceScale * 0.55)),
324 )
325 // uCircle2
326 ..setFloat(
327 18,
328 turbulenceScale * 0.2 + (turbulencePhase * -0.0066 * math.cos(turbulenceScale * 0.45)),
329 )
330 ..setFloat(
331 19,
332 turbulenceScale * 0.2 + (turbulencePhase * -0.0066 * math.sin(turbulenceScale * 0.45)),
333 )
334 // uCircle3
335 ..setFloat(
336 20,
337 turbulenceScale + (turbulencePhase * -0.0066 * math.cos(turbulenceScale * 0.35)),
338 )
339 ..setFloat(
340 21,
341 turbulenceScale + (turbulencePhase * -0.0066 * math.sin(turbulenceScale * 0.35)),
342 )
343 // uRotation1
344 ..setFloat(22, math.cos(rotation1))
345 ..setFloat(23, math.sin(rotation1))
346 // uRotation2
347 ..setFloat(24, math.cos(rotation2))
348 ..setFloat(25, math.sin(rotation2))
349 // uRotation3
350 ..setFloat(26, math.cos(rotation3))
351 ..setFloat(27, math.sin(rotation3));
352 }
353
354 /// Transforms the canvas for an ink feature to be painted on the [canvas].
355 ///
356 /// This should be called before painting ink features that do not use
357 /// [paintInkCircle].
358 ///
359 /// The [transform] argument is the [Matrix4] transform that typically
360 /// shifts the coordinate space of the canvas to the space in which
361 /// the ink feature is to be painted.
362 ///
363 /// For examples on how the function is used, see [InkSparkle] and [paintInkCircle].
364 void _transformCanvas({required Canvas canvas, required Matrix4 transform}) {
365 final Offset? originOffset = MatrixUtils.getAsTranslation(transform);
366 if (originOffset == null) {
367 canvas.transform(transform.storage);
368 } else {
369 canvas.translate(originOffset.dx, originOffset.dy);
370 }
371 }
372
373 /// Clips the canvas for an ink feature to be painted on the [canvas].
374 ///
375 /// This should be called before painting ink features with [paintFeature]
376 /// that do not use [paintInkCircle].
377 ///
378 /// The [clipCallback] is the callback used to obtain the [Rect] used for clipping
379 /// the ink effect.
380 ///
381 /// If [clipCallback] is null, no clipping is performed on the ink circle.
382 ///
383 /// The [textDirection] is used by [customBorder] if it is non-null. This
384 /// allows the [customBorder]'s path to be properly defined if the path was
385 /// expressed in terms of "start" and "end" instead of "left" and "right".
386 ///
387 /// For examples on how the function is used, see [InkSparkle].
388 void _clipCanvas({
389 required Canvas canvas,
390 required RectCallback clipCallback,
391 TextDirection? textDirection,
392 ShapeBorder? customBorder,
393 BorderRadius borderRadius = BorderRadius.zero,
394 }) {
395 final Rect rect = clipCallback();
396 if (customBorder != null) {
397 canvas.clipPath(customBorder.getOuterPath(rect, textDirection: textDirection));
398 } else if (borderRadius != BorderRadius.zero) {
399 canvas.clipRRect(
400 RRect.fromRectAndCorners(
401 rect,
402 topLeft: borderRadius.topLeft,
403 topRight: borderRadius.topRight,
404 bottomLeft: borderRadius.bottomLeft,
405 bottomRight: borderRadius.bottomRight,
406 ),
407 );
408 } else {
409 canvas.clipRect(rect);
410 }
411 }
412}
413
414class _InkSparkleFactory extends InteractiveInkFeatureFactory {
415 const _InkSparkleFactory() : turbulenceSeed = null;
416
417 const _InkSparkleFactory.constantTurbulenceSeed()
418 : turbulenceSeed = _InkSparkleFactory.constantSeed;
419
420 static const double constantSeed = 1337.0;
421
422 static void initializeShader() {
423 if (!_initCalled) {
424 ui.FragmentProgram.fromAsset('shaders/ink_sparkle.frag').then((ui.FragmentProgram program) {
425 _program = program;
426 });
427 _initCalled = true;
428 }
429 }
430
431 static bool _initCalled = false;
432 static ui.FragmentProgram? _program;
433
434 final double? turbulenceSeed;
435
436 @override
437 InteractiveInkFeature create({
438 required MaterialInkController controller,
439 required RenderBox referenceBox,
440 required ui.Offset position,
441 required ui.Color color,
442 required ui.TextDirection textDirection,
443 bool containedInkWell = false,
444 RectCallback? rectCallback,
445 BorderRadius? borderRadius,
446 ShapeBorder? customBorder,
447 double? radius,
448 ui.VoidCallback? onRemoved,
449 }) {
450 return InkSparkle(
451 controller: controller,
452 referenceBox: referenceBox,
453 position: position,
454 color: color,
455 textDirection: textDirection,
456 containedInkWell: containedInkWell,
457 rectCallback: rectCallback,
458 borderRadius: borderRadius,
459 customBorder: customBorder,
460 radius: radius,
461 onRemoved: onRemoved,
462 turbulenceSeed: turbulenceSeed,
463 );
464 }
465}
466
467RectCallback? _getClipCallback(
468 RenderBox referenceBox,
469 bool containedInkWell,
470 RectCallback? rectCallback,
471) {
472 if (rectCallback != null) {
473 assert(containedInkWell);
474 return rectCallback;
475 }
476 if (containedInkWell) {
477 return () => Offset.zero & referenceBox.size;
478 }
479 return null;
480}
481
482double _getTargetRadius(
483 RenderBox referenceBox,
484 bool containedInkWell,
485 RectCallback? rectCallback,
486 Offset position,
487) {
488 final Size size = rectCallback != null ? rectCallback().size : referenceBox.size;
489 final double d1 = size.bottomRight(Offset.zero).distance;
490 final double d2 = (size.topRight(Offset.zero) - size.bottomLeft(Offset.zero)).distance;
491 return math.max(d1, d2) / 2.0;
492}
493

Provided by KDAB

Privacy Policy
Learn more about Flutter for embedded and desktop on industrialflutter.com