There are two separate systems trying to manage touch events on the `MDCButton`. First, the button itself uses its standard `UIControl` event infrastructure to trigger ink ripples. Second, an external `MDCInkTouchController` attaches its own gesture recognizers to do the same thing. This creates a conflict where both systems respond to the same touch, causing two ink animations to be created and displayed simultaneously, which results in incorrect visual feedback.
PiperOrigin-RevId: 770704081
diff --git a/components/Ink/src/MDCInkView.m b/components/Ink/src/MDCInkView.m
index d9476ee..172eb4d 100644
--- a/components/Ink/src/MDCInkView.m
+++ b/components/Ink/src/MDCInkView.m
@@ -43,7 +43,9 @@
@end
-@implementation MDCInkView
+@implementation MDCInkView {
+ BOOL _isActiveInkLayerAnimationRunning;
+}
+ (Class)layerClass {
return [MDCLegacyInkLayer class];
@@ -179,6 +181,13 @@
if (self.usesLegacyInkRipple) {
[self.inkLayer spreadFromPoint:point completion:completionBlock];
} else {
+ @synchronized(self) {
+ if (animated && _isActiveInkLayerAnimationRunning) {
+ // Only one ink layer animation can be running at a time.
+ return;
+ }
+ _isActiveInkLayerAnimationRunning = YES;
+ }
self.startInkRippleCompletionBlock = completionBlock;
MDCInkLayer *inkLayer = [MDCInkLayer layer];
inkLayer.inkColor = self.inkColor;
@@ -277,6 +286,14 @@
}
}
+- (void)inkLayerStartAnimationDidFinish:(MDCInkLayer *)inkLayer {
+ @synchronized(self) {
+ if (self.activeInkLayer == inkLayer && _isActiveInkLayerAnimationRunning) {
+ _isActiveInkLayerAnimationRunning = NO;
+ }
+ }
+}
+
- (void)inkLayerAnimationDidEnd:(MDCInkLayer *)inkLayer {
if (self.activeInkLayer == inkLayer && self.endInkRippleCompletionBlock) {
self.endInkRippleCompletionBlock();
diff --git a/components/Ink/src/private/MDCInkLayer.m b/components/Ink/src/private/MDCInkLayer.m
index c1ba06a..4ab3b5e 100644
--- a/components/Ink/src/private/MDCInkLayer.m
+++ b/components/Ink/src/private/MDCInkLayer.m
@@ -165,6 +165,9 @@
animGroup.removedOnCompletion = NO;
[CATransaction setCompletionBlock:^{
self->_startAnimationActive = NO;
+ if ([self.animationDelegate respondsToSelector:@selector(inkLayerStartAnimationDidFinish:)]) {
+ [self.animationDelegate inkLayerStartAnimationDidFinish:self];
+ }
}];
[self addAnimation:animGroup forKey:nil];
[CATransaction commit];
diff --git a/components/Ink/src/private/MDCInkLayerDelegate.h b/components/Ink/src/private/MDCInkLayerDelegate.h
index a8cfff3..5f2e2ae 100644
--- a/components/Ink/src/private/MDCInkLayerDelegate.h
+++ b/components/Ink/src/private/MDCInkLayerDelegate.h
@@ -39,6 +39,12 @@
- (void)inkLayerAnimationDidStart:(nonnull MDCInkLayer *)inkLayer;
/**
+ Called when the ink ripple appearing animation: scale up, reposition and fade in animation,
+ finishes.
+ */
+- (void)inkLayerStartAnimationDidFinish:(nonnull MDCInkLayer *)inkLayer;
+
+/**
Called when the ink ripple animation ends.
@param inkLayer The MDCInkLayer that ends animating.
diff --git a/components/Ink/tests/unit/MDCInkViewTests.m b/components/Ink/tests/unit/MDCInkViewTests.m
index 42a6e4a..b840fca 100644
--- a/components/Ink/tests/unit/MDCInkViewTests.m
+++ b/components/Ink/tests/unit/MDCInkViewTests.m
@@ -164,4 +164,23 @@
XCTAssertFalse(masksToBoundsWhenUnbounded);
}
+- (void)testStartTouchBeganAtPointAddsInkLayerWithAnimationIsIdempotent {
+ // Given
+ MDCInkView *testInkView = [[MDCInkView alloc] init];
+ testInkView.usesLegacyInkRipple = NO;
+
+ // Verifies that only one ink animation layer is added, even with concurrent triggers.
+ // The initial sublayer count is 1 due to the button's default shape-drawing layer.
+ // After adding the ink layer via startTouchBeganAtPoint:, the count must be 2.
+ // This assertion ensures subsequent calls do not erroneously add more layers.
+ [testInkView startTouchBeganAtPoint:CGPointZero animated:YES withCompletion:nil];
+ [testInkView startTouchBeganAtPoint:CGPointZero animated:YES withCompletion:nil];
+
+ XCTAssertEqual(testInkView.layer.sublayers.count, 2);
+
+ // Verifies that the ink layer is removed when cancelAllAnimationsAnimated:NO is called.
+ [testInkView cancelAllAnimationsAnimated:NO];
+ XCTAssertEqual(testInkView.layer.sublayers.count, 1);
+}
+
@end