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