InkSparkle constructor Null safety

InkSparkle(
  1. {required MaterialInkController controller,
  2. required RenderBox referenceBox,
  3. required Color color,
  4. required Offset position,
  5. required TextDirection textDirection,
  6. bool containedInkWell = true,
  7. RectCallback? rectCallback,
  8. BorderRadius? borderRadius,
  9. ShapeBorder? customBorder,
  10. double? radius,
  11. VoidCallback? onRemoved,
  12. double? turbulenceSeed}
)

Begin a sparkly ripple effect, centered at position relative to referenceBox.

The color defines the color of the splash itself. The sparkles are always white.

The controller argument is typically obtained via Material.of(context).

textDirection is used by customBorder if it is non-null. This allows the customBorder's path to be properly defined if it was the path was expressed in terms of "start" and "end" instead of "left" and "right".

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.

Clipping can happen in 3 different ways:

  1. If customBorder is provided, it is used to determine the path for clipping.
  2. If customBorder is null, and borderRadius is provided, then the canvas is clipped by an RRect created from borderRadius.
  3. If borderRadius is the default BorderRadius.zero, then the canvas is clipped with rectCallback. When the ripple is removed, onRemoved will be called.

turbulenceSeed can be passed if a non random seed should be used for the turbulence and sparkles. By default, the seed is a random number between 0.0 and 1000.0.

Turbulence is an input to the shader and helps to provides a more natural, non-circular, "splash" effect.

Sparkle randomization is also driven by the turbulenceSeed. Sparkles are identified in the shader as "noise", and the sparkles are derived from pseudorandom triangular noise.

Implementation

InkSparkle({
  required MaterialInkController controller,
  required RenderBox referenceBox,
  required super.color,
  required Offset position,
  required TextDirection textDirection,
  bool containedInkWell = true,
  RectCallback? rectCallback,
  BorderRadius? borderRadius,
  ShapeBorder? customBorder,
  double? radius,
  super.onRemoved,
  double? turbulenceSeed,
}) : assert(containedInkWell || rectCallback == null),
     _color = color,
     _position = position,
     _borderRadius = borderRadius ?? BorderRadius.zero,
     _customBorder = customBorder,
     _textDirection = textDirection,
     _targetRadius = (radius ?? _getTargetRadius(referenceBox, containedInkWell, rectCallback, position)) * _targetRadiusMultiplier,
     _clipCallback = _getClipCallback(referenceBox, containedInkWell, rectCallback),
     super(controller: controller, referenceBox: referenceBox) {
  // InkSparkle will not be painted until the async compilation completes.
  _InkSparkleFactory.compileShaderIfNeccessary();
  controller.addInkFeature(this);

  // All animation values are derived from Android 12 source code. See:
  // - https://cs.android.com/android/platform/superproject/+/master:frameworks/base/graphics/java/android/graphics/drawable/RippleShader.java
  // - https://cs.android.com/android/platform/superproject/+/master:frameworks/base/graphics/java/android/graphics/drawable/RippleDrawable.java
  // - https://cs.android.com/android/platform/superproject/+/master:frameworks/base/graphics/java/android/graphics/drawable/RippleAnimationSession.java

  // Immediately begin animating the ink.
  _animationController = AnimationController(
    duration: _animationDuration,
    vsync: controller.vsync,
  )..addListener(controller.markNeedsPaint)
   ..addStatusListener(_handleStatusChanged)
   ..forward();

  _radiusScale = TweenSequence<double>(
    <TweenSequenceItem<double>>[
      TweenSequenceItem<double>(
        tween: CurveTween(curve: Curves.fastOutSlowIn),
        weight: 75,
      ),
      TweenSequenceItem<double>(
        tween: ConstantTween<double>(1.0),
        weight: 25,
      ),
    ],
  ).animate(_animationController);

  // Functionally equivalent to Android 12's SkSL:
  //`return mix(u_touch, u_resolution, saturate(in_radius_scale * 2.0))`
  final Tween<Vector2> centerTween = Tween<Vector2>(
    begin: Vector2.array(<double>[_position.dx, _position.dy]),
    end: Vector2.array(<double>[referenceBox.size.width / 2, referenceBox.size.height / 2]),
  );
  final Animation<double> centerProgress = TweenSequence<double>(
    <TweenSequenceItem<double>>[
      TweenSequenceItem<double>(
        tween: Tween<double>(begin: 0.0, end: 1.0),
        weight: 50,
      ),
      TweenSequenceItem<double>(
        tween: ConstantTween<double>(1.0),
        weight: 50,
      ),
    ],
  ).animate(_radiusScale);
  _center = centerTween.animate(centerProgress);

  _alpha = TweenSequence<double>(
    <TweenSequenceItem<double>>[
      TweenSequenceItem<double>(
        tween: Tween<double>(begin: 0.0, end: 1.0),
        weight: 13,
      ),
      TweenSequenceItem<double>(
        tween: ConstantTween<double>(1.0),
        weight: 27,
      ),
      TweenSequenceItem<double>(
        tween: Tween<double>(begin: 1.0, end: 0.0),
        weight: 60,
      ),
    ],
  ).animate(_animationController);

  _sparkleAlpha = TweenSequence<double>(
    <TweenSequenceItem<double>>[
      TweenSequenceItem<double>(
        tween: Tween<double>(begin: 0.0, end: 1.0),
        weight: 13,
      ),
      TweenSequenceItem<double>(
        tween: ConstantTween<double>(1.0),
        weight: 27,
      ),
      TweenSequenceItem<double>(
        tween: Tween<double>(begin: 1.0, end: 0.0),
        weight: 50,
      ),
    ],
  ).animate(_animationController);

  // Creates an element of randomness so that ink eminating from the same
  // pixel have slightly different rings and sparkles.
  _turbulenceSeed = turbulenceSeed ?? math.Random().nextDouble() * 1000.0;
}