diff --git a/packages/flutter/lib/src/material/ink_highlight.dart b/packages/flutter/lib/src/material/ink_highlight.dart index 6b92f4e951a..e294b305759 100644 --- a/packages/flutter/lib/src/material/ink_highlight.dart +++ b/packages/flutter/lib/src/material/ink_highlight.dart @@ -44,14 +44,14 @@ class InkHighlight extends InteractiveInkFeature { BoxShape shape = BoxShape.rectangle, double? radius, BorderRadius? borderRadius, - ShapeBorder? customBorder, + super.customBorder, RectCallback? rectCallback, super.onRemoved, Duration fadeDuration = _kDefaultHighlightFadeDuration, }) : _shape = shape, _radius = radius, _borderRadius = borderRadius ?? BorderRadius.zero, - _customBorder = customBorder, + _textDirection = textDirection, _rectCallback = rectCallback { _alphaController = AnimationController(duration: fadeDuration, vsync: controller.vsync) @@ -69,7 +69,6 @@ class InkHighlight extends InteractiveInkFeature { final BoxShape _shape; final double? _radius; final BorderRadius _borderRadius; - final ShapeBorder? _customBorder; final RectCallback? _rectCallback; final TextDirection _textDirection; @@ -106,8 +105,8 @@ class InkHighlight extends InteractiveInkFeature { void _paintHighlight(Canvas canvas, Rect rect, Paint paint) { canvas.save(); - if (_customBorder != null) { - canvas.clipPath(_customBorder!.getOuterPath(rect, textDirection: _textDirection)); + if (customBorder != null) { + canvas.clipPath(customBorder!.getOuterPath(rect, textDirection: _textDirection)); } switch (_shape) { case BoxShape.circle: diff --git a/packages/flutter/lib/src/material/ink_ripple.dart b/packages/flutter/lib/src/material/ink_ripple.dart index 3df140365fb..7a574153db6 100644 --- a/packages/flutter/lib/src/material/ink_ripple.dart +++ b/packages/flutter/lib/src/material/ink_ripple.dart @@ -116,12 +116,11 @@ class InkRipple extends InteractiveInkFeature { bool containedInkWell = false, RectCallback? rectCallback, BorderRadius? borderRadius, - ShapeBorder? customBorder, + super.customBorder, double? radius, super.onRemoved, }) : _position = position, _borderRadius = borderRadius ?? BorderRadius.zero, - _customBorder = customBorder, _textDirection = textDirection, _targetRadius = radius ?? _getTargetRadius(referenceBox, containedInkWell, rectCallback, position), _clipCallback = _getClipCallback(referenceBox, containedInkWell, rectCallback), @@ -166,7 +165,6 @@ class InkRipple extends InteractiveInkFeature { final Offset _position; final BorderRadius _borderRadius; - final ShapeBorder? _customBorder; final double _targetRadius; final RectCallback? _clipCallback; final TextDirection _textDirection; @@ -245,7 +243,7 @@ class InkRipple extends InteractiveInkFeature { center: center, textDirection: _textDirection, radius: _radius.value, - customBorder: _customBorder, + customBorder: customBorder, borderRadius: _borderRadius, clipCallback: _clipCallback, ); diff --git a/packages/flutter/lib/src/material/ink_sparkle.dart b/packages/flutter/lib/src/material/ink_sparkle.dart index 1a7a97caa55..44678581b54 100644 --- a/packages/flutter/lib/src/material/ink_sparkle.dart +++ b/packages/flutter/lib/src/material/ink_sparkle.dart @@ -106,7 +106,7 @@ class InkSparkle extends InteractiveInkFeature { bool containedInkWell = true, RectCallback? rectCallback, BorderRadius? borderRadius, - ShapeBorder? customBorder, + super.customBorder, double? radius, super.onRemoved, double? turbulenceSeed, @@ -114,7 +114,6 @@ class InkSparkle extends InteractiveInkFeature { _color = color, _position = position, _borderRadius = borderRadius ?? BorderRadius.zero, - _customBorder = customBorder, _textDirection = textDirection, _targetRadius = (radius ?? _getTargetRadius( referenceBox, @@ -236,7 +235,6 @@ class InkSparkle extends InteractiveInkFeature { final Color _color; final Offset _position; final BorderRadius _borderRadius; - final ShapeBorder? _customBorder; final double _targetRadius; final RectCallback? _clipCallback; final TextDirection _textDirection; @@ -292,7 +290,7 @@ class InkSparkle extends InteractiveInkFeature { canvas: canvas, clipCallback: _clipCallback!, textDirection: _textDirection, - customBorder: _customBorder, + customBorder: customBorder, borderRadius: _borderRadius, ); } diff --git a/packages/flutter/lib/src/material/ink_splash.dart b/packages/flutter/lib/src/material/ink_splash.dart index 50474ba444f..1a34ae2b7a4 100644 --- a/packages/flutter/lib/src/material/ink_splash.dart +++ b/packages/flutter/lib/src/material/ink_splash.dart @@ -122,12 +122,11 @@ class InkSplash extends InteractiveInkFeature { bool containedInkWell = false, RectCallback? rectCallback, BorderRadius? borderRadius, - ShapeBorder? customBorder, + super.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, @@ -153,7 +152,6 @@ class InkSplash extends InteractiveInkFeature { final Offset? _position; final BorderRadius _borderRadius; - final ShapeBorder? _customBorder; final double _targetRadius; final RectCallback? _clipCallback; final bool _repositionToReferenceBox; @@ -211,7 +209,7 @@ class InkSplash extends InteractiveInkFeature { center: center!, textDirection: _textDirection, radius: _radius.value, - customBorder: _customBorder, + customBorder: customBorder, borderRadius: _borderRadius, clipCallback: _clipCallback, ); diff --git a/packages/flutter/lib/src/material/ink_well.dart b/packages/flutter/lib/src/material/ink_well.dart index 4ec6c82098d..c5147df6d81 100644 --- a/packages/flutter/lib/src/material/ink_well.dart +++ b/packages/flutter/lib/src/material/ink_well.dart @@ -41,8 +41,10 @@ abstract class InteractiveInkFeature extends InkFeature { required super.controller, required super.referenceBox, required Color color, + ShapeBorder? customBorder, super.onRemoved, - }) : _color = color; + }) : _color = color, + _customBorder = customBorder; /// Called when the user input that triggered this feature's appearance was confirmed. /// @@ -67,6 +69,17 @@ abstract class InteractiveInkFeature extends InkFeature { controller.markNeedsPaint(); } + /// The ink's optional custom border. + ShapeBorder? get customBorder => _customBorder; + ShapeBorder? _customBorder; + set customBorder(ShapeBorder? value) { + if (value == _customBorder) { + return; + } + _customBorder = value; + controller.markNeedsPaint(); + } + /// Draws an ink splash or ink ripple on the passed in [Canvas]. /// /// The [transform] argument is the [Matrix4] transform that typically @@ -854,10 +867,9 @@ class _InkResponseState extends State<_InkResponseStateWidget> } initStatesController(); } - if (widget.customBorder != oldWidget.customBorder || - widget.radius != oldWidget.radius || - widget.borderRadius != oldWidget.borderRadius || - widget.highlightShape != oldWidget.highlightShape) { + if (widget.radius != oldWidget.radius || + widget.highlightShape != oldWidget.highlightShape || + widget.borderRadius != oldWidget.borderRadius) { final InkHighlight? hoverHighlight = _highlights[_HighlightType.hover]; if (hoverHighlight != null) { hoverHighlight.dispose(); @@ -869,6 +881,9 @@ class _InkResponseState extends State<_InkResponseStateWidget> // Do not call updateFocusHighlights() here because it is called below } } + if (widget.customBorder != oldWidget.customBorder) { + _updateHighlightsAndSplashes(); + } if (enabled != isWidgetEnabled(oldWidget)) { statesController.update(MaterialState.disabled, !enabled); if (!enabled) { @@ -986,7 +1001,23 @@ class _InkResponseState extends State<_InkResponseStateWidget> } } - InteractiveInkFeature _createInkFeature(Offset globalPosition) { + void _updateHighlightsAndSplashes() { + for (final InkHighlight? highlight in _highlights.values) { + if (highlight != null) { + highlight.customBorder = widget.customBorder; + } + } + if (_currentSplash != null) { + _currentSplash!.customBorder = widget.customBorder; + } + if (_splashes != null && _splashes!.isNotEmpty) { + for (final InteractiveInkFeature inkFeature in _splashes!) { + inkFeature.customBorder = widget.customBorder; + } + } + } + + InteractiveInkFeature _createSplash(Offset globalPosition) { final MaterialInkController inkController = Material.of(context); final RenderBox referenceBox = context.findRenderObject()! as RenderBox; final Offset position = referenceBox.globalToLocal(globalPosition); @@ -1103,7 +1134,7 @@ class _InkResponseState extends State<_InkResponseStateWidget> globalPosition = details!.globalPosition; } statesController.update(MaterialState.pressed, true); // ... before creating the splash - final InteractiveInkFeature splash = _createInkFeature(globalPosition); + final InteractiveInkFeature splash = _createSplash(globalPosition); _splashes ??= HashSet(); _splashes!.add(splash); _currentSplash?.cancel(); diff --git a/packages/flutter/test/material/ink_well_test.dart b/packages/flutter/test/material/ink_well_test.dart index e309185eec9..7b3992182a9 100644 --- a/packages/flutter/test/material/ink_well_test.dart +++ b/packages/flutter/test/material/ink_well_test.dart @@ -816,6 +816,91 @@ testWidgets('InkResponse radius can be updated', (WidgetTester tester) async { ); }); + testWidgets('InkWell splash customBorder can be updated', (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/121626. + final FocusNode focusNode = FocusNode(debugLabel: 'Ink Focus'); + Widget boilerplate(BorderRadius borderRadius) { + return Material( + child: Directionality( + textDirection: TextDirection.ltr, + child: Align( + alignment: Alignment.topLeft, + child: SizedBox( + width: 100, + height: 100, + child: MouseRegion( + child: InkWell( + focusNode: focusNode, + customBorder: RoundedRectangleBorder(borderRadius: borderRadius), + onTap: () { }, + ), + ), + ), + ), + ), + ); + } + + await tester.pumpWidget(boilerplate(BorderRadius.circular(20))); + await tester.pumpAndSettle(); + + final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures'); + expect(inkFeatures, paintsExactlyCountTimes(#clipPath, 0)); + + final TestGesture gesture = await tester.startGesture(tester.getRect(find.byType(InkWell)).center); + await tester.pump(const Duration(milliseconds: 200)); // Unconfirmed splash is well underway. + expect(inkFeatures, paintsExactlyCountTimes(#clipPath, 2)); // Splash and highlight. + + const Rect expectedClipRect = Rect.fromLTRB(0, 0, 100, 100); + Path expectedClipPath = Path() + ..addRRect(RRect.fromRectAndRadius( + expectedClipRect, + const Radius.circular(20), + )); + + // Check that the splash and the highlight are correctly clipped. + expect( + inkFeatures, + paints + ..clipPath(pathMatcher: coversSameAreaAs( + expectedClipPath, + areaToCompare: expectedClipRect.inflate(20.0), + sampleSize: 100, + )) + ..clipPath(pathMatcher: coversSameAreaAs( + expectedClipPath, + areaToCompare: expectedClipRect.inflate(20.0), + sampleSize: 100, + )), + ); + + await tester.pumpWidget(boilerplate(BorderRadius.circular(40))); + await tester.pumpAndSettle(); + expectedClipPath = Path() + ..addRRect(RRect.fromRectAndRadius( + expectedClipRect, + const Radius.circular(40), + )); + + // Check that the splash and the highlight are correctly clipped. + expect( + inkFeatures, + paints + ..clipPath(pathMatcher: coversSameAreaAs( + expectedClipPath, + areaToCompare: expectedClipRect.inflate(20.0), + sampleSize: 100, + )) + ..clipPath(pathMatcher: coversSameAreaAs( + expectedClipPath, + areaToCompare: expectedClipRect.inflate(20.0), + sampleSize: 100, + )), + ); + + await gesture.up(); + }); + testWidgets("ink response doesn't change color on focus when on touch device", (WidgetTester tester) async { FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTouch; final FocusNode focusNode = FocusNode(debugLabel: 'Ink Focus');