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