@@ -70,18 +70,13 @@ - (instancetype)initWithFrame:(CGRect)frame
7070
7171- (void )prepareForRecycle
7272{
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.
7673 [self .layer removeAnimationForKey: @" transform" ];
7774 self.layer .transform = CATransform3DIdentity;
7875
7976 [_buttonView prepareForRecycle ];
8077
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:.
78+ // Force the next updateProps: to re-run applyStartAnimationState even if
79+ // the new mount's defaults match the previous mount's.
8580 _needsAnimationStateReset = YES ;
8681
8782 [super prepareForRecycle ];
@@ -163,24 +158,29 @@ - (void)updateLayoutMetrics:(const facebook::react::LayoutMetrics &)layoutMetric
163158
164159- (void )finalizeUpdates : (RNComponentViewUpdateMask)updateMask
165160{
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.
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.
172166 float savedOpacity = self.layer .opacity ;
173- CAAnimation *savedOpacityAnimation = [[ self .layer animationForKey: @" opacity" ] copy ];
167+ CAAnimation *savedOpacityAnimation = [self .layer animationForKey: @" opacity" ];
174168
175169 [super finalizeUpdates: updateMask];
176170
177- if (savedOpacity != self.layer .opacity || savedOpacityAnimation != nil ) {
171+ BOOL opacityChanged = savedOpacity != self.layer .opacity ;
172+ BOOL animationChanged = savedOpacityAnimation != [self .layer animationForKey: @" opacity" ];
173+ if (opacityChanged || animationChanged) {
178174 [CATransaction begin ];
179175 [CATransaction setDisableActions: YES ];
180- [self .layer removeAnimationForKey: @" opacity" ];
181- self.layer .opacity = savedOpacity;
182- if (savedOpacityAnimation) {
183- [self .layer addAnimation: savedOpacityAnimation forKey: @" opacity" ];
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;
184184 }
185185 [CATransaction commit ];
186186 }
@@ -307,13 +307,15 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &
307307{
308308 const auto &newProps = *std::static_pointer_cast<const RNGestureHandlerButtonProps>(props);
309309
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;
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;
315313 _needsAnimationStateReset = NO ;
316- if (oldProps && !shouldApplyStartAnimationState) {
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) {
317319 const auto &oldButtonProps = *std::static_pointer_cast<const RNGestureHandlerButtonProps>(oldProps);
318320 shouldApplyStartAnimationState = oldButtonProps.defaultOpacity != newProps.defaultOpacity ||
319321 oldButtonProps.defaultScale != newProps.defaultScale ||
@@ -345,7 +347,7 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &
345347 // This is necessary because pointerEvents is redefined in the spec,
346348 // which shadows the base property with a different, incompatible type.
347349 const auto &newViewProps = static_cast <const ViewProps &>(newProps);
348- if (!oldProps ) {
350+ if (treatAsFirstMount ) {
349351 _buttonView.pointerEvents = RCTPointerEventsToEnum (newViewProps.pointerEvents );
350352 } else {
351353 const auto &oldButtonProps = *std::static_pointer_cast<const RNGestureHandlerButtonProps>(oldProps);
0 commit comments