@@ -34,6 +34,7 @@ @interface RNGestureHandlerButtonComponentView () <RCTRNGestureHandlerButtonView
3434
3535@implementation RNGestureHandlerButtonComponentView {
3636 RNGestureHandlerButton *_buttonView;
37+ BOOL _needsAnimationStateReset;
3738}
3839
3940#if TARGET_OS_OSX
@@ -67,6 +68,20 @@ - (instancetype)initWithFrame:(CGRect)frame
6768 return self;
6869}
6970
71+ - (void )prepareForRecycle
72+ {
73+ [self .layer removeAnimationForKey: @" transform" ];
74+ self.layer .transform = CATransform3DIdentity;
75+
76+ [_buttonView prepareForRecycle ];
77+
78+ // Force the next updateProps: to re-run applyStartAnimationState even if
79+ // the new mount's defaults match the previous mount's.
80+ _needsAnimationStateReset = YES ;
81+
82+ [super prepareForRecycle ];
83+ }
84+
7085- (void )mountChildComponentView : (RNGHUIView<RCTComponentViewProtocol> *)childComponentView index : (NSInteger )index
7186{
7287 [_buttonView mountChildComponentView: childComponentView index: index];
@@ -143,8 +158,33 @@ - (void)updateLayoutMetrics:(const facebook::react::LayoutMetrics &)layoutMetric
143158
144159- (void )finalizeUpdates : (RNComponentViewUpdateMask)updateMask
145160{
161+ // super's invalidateLayer (called via finalizeUpdates) unconditionally sets
162+ // self.layer.opacity to the React style.opacity, overwriting our
163+ // applyStartAnimationState alpha and interrupting in-flight press
164+ // animations. Save/restore around super, but only touch what super actually
165+ // disturbed — re-adding an unchanged animation resets its progress.
166+ float savedOpacity = self.layer .opacity ;
167+ CAAnimation *savedOpacityAnimation = [self .layer animationForKey: @" opacity" ];
168+
146169 [super finalizeUpdates: updateMask];
147170
171+ BOOL opacityChanged = savedOpacity != self.layer .opacity ;
172+ BOOL animationChanged = savedOpacityAnimation != [self .layer animationForKey: @" opacity" ];
173+ if (opacityChanged || animationChanged) {
174+ [CATransaction begin ];
175+ [CATransaction setDisableActions: YES ];
176+ if (animationChanged) {
177+ [self .layer removeAnimationForKey: @" opacity" ];
178+ if (savedOpacityAnimation) {
179+ [self .layer addAnimation: savedOpacityAnimation forKey: @" opacity" ];
180+ }
181+ }
182+ if (opacityChanged) {
183+ self.layer .opacity = savedOpacity;
184+ }
185+ [CATransaction commit ];
186+ }
187+
148188 // Resolve per-corner border radii from props and forward to the button
149189 // so its underlay CALayer gets the matching shape.
150190 const auto borderMetrics = _props->resolveBorderMetrics (_layoutMetrics);
@@ -267,11 +307,15 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &
267307{
268308 const auto &newProps = *std::static_pointer_cast<const RNGestureHandlerButtonProps>(props);
269309
270- // Re-apply the idle visual state only on first mount or when one of the default
271- // values actually changed. Doing it on every commit interrupts in-flight press
272- // animations whenever React re-renders the children mid-press.
273- BOOL shouldApplyStartAnimationState = !oldProps;
274- if (oldProps) {
310+ // After recycling, treat diffing branches as a fresh mount so values that
311+ // survived recycling on _buttonView (e.g. pointerEvents) get re-applied.
312+ BOOL treatAsFirstMount = !oldProps || _needsAnimationStateReset;
313+ _needsAnimationStateReset = NO ;
314+
315+ // Avoid re-running applyStartAnimationState on every commit — it would
316+ // interrupt in-flight press animations during mid-press re-renders.
317+ BOOL shouldApplyStartAnimationState = treatAsFirstMount;
318+ if (!treatAsFirstMount) {
275319 const auto &oldButtonProps = *std::static_pointer_cast<const RNGestureHandlerButtonProps>(oldProps);
276320 shouldApplyStartAnimationState = oldButtonProps.defaultOpacity != newProps.defaultOpacity ||
277321 oldButtonProps.defaultScale != newProps.defaultScale ||
@@ -303,7 +347,7 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &
303347 // This is necessary because pointerEvents is redefined in the spec,
304348 // which shadows the base property with a different, incompatible type.
305349 const auto &newViewProps = static_cast <const ViewProps &>(newProps);
306- if (!oldProps ) {
350+ if (treatAsFirstMount ) {
307351 _buttonView.pointerEvents = RCTPointerEventsToEnum (newViewProps.pointerEvents );
308352 } else {
309353 const auto &oldButtonProps = *std::static_pointer_cast<const RNGestureHandlerButtonProps>(oldProps);
0 commit comments