@@ -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,25 @@ - (instancetype)initWithFrame:(CGRect)frame
6768 return self;
6869}
6970
71+ - (void )prepareForRecycle
72+ {
73+ // Reset the wrapper layer's transform (and any in-flight transform animation)
74+ // before super resets _props, so the recycled view starts in a clean visual
75+ // state regardless of whether the next updateProps: re-runs applyStartAnimationState.
76+ [self .layer removeAnimationForKey: @" transform" ];
77+ self.layer .transform = CATransform3DIdentity;
78+
79+ [_buttonView prepareForRecycle ];
80+
81+ // The next updateProps: comparison treats this view as having "previous"
82+ // props equal to defaults, so identical defaults across mounts won't trigger
83+ // applyStartAnimationState — leaving non-1.0 default opacity/scale/underlay
84+ // values unapplied. Force a one-shot reset on the next updateProps:.
85+ _needsAnimationStateReset = YES ;
86+
87+ [super prepareForRecycle ];
88+ }
89+
7090- (void )mountChildComponentView : (RNGHUIView<RCTComponentViewProtocol> *)childComponentView index : (NSInteger )index
7191{
7292 [_buttonView mountChildComponentView: childComponentView index: index];
@@ -143,8 +163,28 @@ - (void)updateLayoutMetrics:(const facebook::react::LayoutMetrics &)layoutMetric
143163
144164- (void )finalizeUpdates : (RNComponentViewUpdateMask)updateMask
145165{
166+ // super.finalizeUpdates calls invalidateLayer, which unconditionally sets
167+ // self.layer.opacity = _props->opacity (the React style.opacity, default
168+ // 1.0). That overwrites the alpha set by applyStartAnimationState for non-
169+ // 1.0 defaultOpacity, and would also interrupt in-flight press animations
170+ // on a mid-press React re-render. Snapshot opacity (and any animation) and
171+ // restore it atomically.
172+ float savedOpacity = self.layer .opacity ;
173+ CAAnimation *savedOpacityAnimation = [[self .layer animationForKey: @" opacity" ] copy ];
174+
146175 [super finalizeUpdates: updateMask];
147176
177+ if (savedOpacity != self.layer .opacity || savedOpacityAnimation != nil ) {
178+ [CATransaction begin ];
179+ [CATransaction setDisableActions: YES ];
180+ [self .layer removeAnimationForKey: @" opacity" ];
181+ self.layer .opacity = savedOpacity;
182+ if (savedOpacityAnimation) {
183+ [self .layer addAnimation: savedOpacityAnimation forKey: @" opacity" ];
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,13 @@ - (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+ // Re-apply the idle visual state only on first mount, after recycling, or
311+ // when one of the default values actually changed. Doing it on every commit
312+ // interrupts in-flight press animations whenever React re-renders the children
313+ // mid-press.
314+ BOOL shouldApplyStartAnimationState = !oldProps || _needsAnimationStateReset;
315+ _needsAnimationStateReset = NO ;
316+ if (oldProps && !shouldApplyStartAnimationState) {
275317 const auto &oldButtonProps = *std::static_pointer_cast<const RNGestureHandlerButtonProps>(oldProps);
276318 shouldApplyStartAnimationState = oldButtonProps.defaultOpacity != newProps.defaultOpacity ||
277319 oldButtonProps.defaultScale != newProps.defaultScale ||
0 commit comments