| // Copyright 2017 The Chromium 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/foundation.dart'; |
| import 'package:flutter/rendering.dart'; |
| import 'package:flutter/widgets.dart'; |
| |
| import 'material.dart'; |
| |
| const Duration _kUnconfirmedSplashDuration = const Duration(seconds: 1); |
| const Duration _kSplashFadeDuration = const 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(); |
| } |
| |
| /// A visual reaction on a piece of [Material] to user input. |
| /// |
| /// 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: |
| /// |
| /// * [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 InkSplash extends InkFeature { |
| /// 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 RenderBox referenceBox, |
| Offset position, |
| Color color, |
| bool containedInkWell: false, |
| RectCallback rectCallback, |
| BorderRadius borderRadius = BorderRadius.zero, |
| double radius, |
| VoidCallback onRemoved, |
| }) : _position = position, |
| _color = color, |
| _borderRadius = borderRadius, |
| _targetRadius = radius ?? _getTargetRadius(referenceBox, containedInkWell, rectCallback, position), |
| _clipCallback = _getClipCallback(referenceBox, containedInkWell, rectCallback), |
| _repositionToReferenceBox = !containedInkWell, |
| super(controller: controller, referenceBox: referenceBox, onRemoved: onRemoved) { |
| assert(_borderRadius != null); |
| _radiusController = new AnimationController(duration: _kUnconfirmedSplashDuration, vsync: controller.vsync) |
| ..addListener(controller.markNeedsPaint) |
| ..forward(); |
| _radius = new Tween<double>( |
| begin: _kSplashInitialSize, |
| end: _targetRadius |
| ).animate(_radiusController); |
| _alphaController = new AnimationController(duration: _kSplashFadeDuration, vsync: controller.vsync) |
| ..addListener(controller.markNeedsPaint) |
| ..addStatusListener(_handleAlphaStatusChanged); |
| _alpha = new IntTween( |
| begin: color.alpha, |
| end: 0 |
| ).animate(_alphaController); |
| |
| controller.addInkFeature(this); |
| } |
| |
| final Offset _position; |
| final BorderRadius _borderRadius; |
| final double _targetRadius; |
| final RectCallback _clipCallback; |
| final bool _repositionToReferenceBox; |
| |
| Animation<double> _radius; |
| AnimationController _radiusController; |
| |
| Animation<int> _alpha; |
| AnimationController _alphaController; |
| |
| /// The color of the splash. |
| Color get color => _color; |
| Color _color; |
| set color(Color value) { |
| if (value == _color) |
| return; |
| _color = value; |
| controller.markNeedsPaint(); |
| } |
| |
| |
| /// The user input is confirmed. |
| /// |
| /// Causes the reaction to propagate faster across the material. |
| void confirm() { |
| final int duration = (_targetRadius / _kSplashConfirmedVelocity).floor(); |
| _radiusController |
| ..duration = new Duration(milliseconds: duration) |
| ..forward(); |
| _alphaController.forward(); |
| } |
| |
| /// The user input was canceled. |
| /// |
| /// Causes the reaction to gradually disappear. |
| void cancel() { |
| _alphaController.forward(); |
| } |
| |
| void _handleAlphaStatusChanged(AnimationStatus status) { |
| if (status == AnimationStatus.completed) |
| dispose(); |
| } |
| |
| @override |
| void dispose() { |
| _radiusController.dispose(); |
| _alphaController.dispose(); |
| super.dispose(); |
| } |
| |
| RRect _clipRRectFromRect(Rect rect) { |
| return new RRect.fromRectAndCorners( |
| rect, |
| topLeft: _borderRadius.topLeft, topRight: _borderRadius.topRight, |
| bottomLeft: _borderRadius.bottomLeft, bottomRight: _borderRadius.bottomRight, |
| ); |
| } |
| |
| void _clipCanvasWithRect(Canvas canvas, Rect rect, {Offset offset}) { |
| Rect clipRect = rect; |
| if (offset != null) { |
| clipRect = clipRect.shift(offset); |
| } |
| if (_borderRadius != BorderRadius.zero) { |
| canvas.clipRRect(_clipRRectFromRect(clipRect)); |
| } else { |
| canvas.clipRect(clipRect); |
| } |
| } |
| |
| @override |
| void paintFeature(Canvas canvas, Matrix4 transform) { |
| final Paint paint = new Paint()..color = _color.withAlpha(_alpha.value); |
| Offset center = _position; |
| if (_repositionToReferenceBox) |
| center = Offset.lerp(center, referenceBox.size.center(Offset.zero), _radiusController.value); |
| final Offset originOffset = MatrixUtils.getAsTranslation(transform); |
| if (originOffset == null) { |
| canvas.save(); |
| canvas.transform(transform.storage); |
| if (_clipCallback != null) { |
| _clipCanvasWithRect(canvas, _clipCallback()); |
| } |
| canvas.drawCircle(center, _radius.value, paint); |
| canvas.restore(); |
| } else { |
| if (_clipCallback != null) { |
| canvas.save(); |
| _clipCanvasWithRect(canvas, _clipCallback(), offset: originOffset); |
| } |
| canvas.drawCircle(center + originOffset, _radius.value, paint); |
| if (_clipCallback != null) |
| canvas.restore(); |
| } |
| } |
| } |