// Copyright 2014 The Flutter 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/widgets.dart'; import 'ink_well.dart'; import 'material.dart'; const Duration _kUnconfirmedSplashDuration = Duration(seconds: 1); const Duration _kSplashFadeDuration = Duration(milliseconds: 200); const double _kSplashInitialSize = 0.0; // logical pixels const double _kSplashConfirmedVelocity = 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 _getSplashRadiusForPositionInSize(size, position); } return Material.defaultSplashRadius; } double _getSplashRadiusForPositionInSize(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 _InkSplashFactory extends InteractiveInkFeatureFactory { const _InkSplashFactory(); @override InteractiveInkFeature create({ required MaterialInkController controller, required RenderBox referenceBox, required Offset position, required Color color, required TextDirection textDirection, bool containedInkWell = false, RectCallback? rectCallback, BorderRadius? borderRadius, ShapeBorder? customBorder, double? radius, VoidCallback? onRemoved, }) { return InkSplash( controller: controller, referenceBox: referenceBox, position: position, color: color, containedInkWell: containedInkWell, rectCallback: rectCallback, borderRadius: borderRadius, customBorder: customBorder, radius: radius, onRemoved: onRemoved, textDirection: textDirection, ); } } /// 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 /// 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]. /// * [Ink], a convenience widget for drawing images and other decorations on /// Material widgets. class InkSplash extends InteractiveInkFeature { /// 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({ required MaterialInkController controller, required super.referenceBox, required TextDirection textDirection, Offset? position, required Color color, bool containedInkWell = false, RectCallback? rectCallback, BorderRadius? borderRadius, ShapeBorder? customBorder, double? radius, super.onRemoved, }) : _position = position, _borderRadius = borderRadius ?? BorderRadius.zero, _customBorder = customBorder, _targetRadius = radius ?? _getTargetRadius(referenceBox, containedInkWell, rectCallback, position!), _clipCallback = _getClipCallback(referenceBox, containedInkWell, rectCallback), _repositionToReferenceBox = !containedInkWell, _textDirection = textDirection, super(controller: controller, color: color) { _radiusController = AnimationController(duration: _kUnconfirmedSplashDuration, vsync: controller.vsync) ..addListener(controller.markNeedsPaint) ..forward(); _radius = _radiusController.drive(Tween<double>( begin: _kSplashInitialSize, end: _targetRadius, )); _alphaController = AnimationController(duration: _kSplashFadeDuration, vsync: controller.vsync) ..addListener(controller.markNeedsPaint) ..addStatusListener(_handleAlphaStatusChanged); _alpha = _alphaController!.drive(IntTween( begin: color.alpha, end: 0, )); controller.addInkFeature(this); } final Offset? _position; final BorderRadius _borderRadius; final ShapeBorder? _customBorder; final double _targetRadius; final RectCallback? _clipCallback; final bool _repositionToReferenceBox; final TextDirection _textDirection; late Animation<double> _radius; late AnimationController _radiusController; late Animation<int> _alpha; AnimationController? _alphaController; /// Used to specify this type of ink splash for an [InkWell], [InkResponse], /// material [Theme], or [ButtonStyle]. static const InteractiveInkFeatureFactory splashFactory = _InkSplashFactory(); @override void confirm() { final int duration = (_targetRadius / _kSplashConfirmedVelocity).floor(); _radiusController ..duration = Duration(milliseconds: duration) ..forward(); _alphaController!.forward(); } @override void cancel() { _alphaController?.forward(); } void _handleAlphaStatusChanged(AnimationStatus status) { if (status == AnimationStatus.completed) { dispose(); } } @override void dispose() { _radiusController.dispose(); _alphaController!.dispose(); _alphaController = null; super.dispose(); } @override void paintFeature(Canvas canvas, Matrix4 transform) { final Paint paint = Paint()..color = color.withAlpha(_alpha.value); Offset? center = _position; if (_repositionToReferenceBox) { center = Offset.lerp(center, referenceBox.size.center(Offset.zero), _radiusController.value); } paintInkCircle( canvas: canvas, transform: transform, paint: paint, center: center!, textDirection: _textDirection, radius: _radius.value, customBorder: _customBorder, borderRadius: _borderRadius, clipCallback: _clipCallback, ); } }