Skip to content

Commit 587dea7

Browse files
committed
Handle view recycling
1 parent 9b87dbc commit 587dea7

3 files changed

Lines changed: 77 additions & 5 deletions

File tree

packages/react-native-gesture-handler/apple/RNGestureHandlerButton.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,14 @@
4949
*/
5050
- (void)applyStartAnimationState;
5151

52+
/**
53+
* Resets all transient press/animation state so the button can be safely reused
54+
* by Fabric view recycling. Cancels pending press-out blocks, removes in-flight
55+
* animations, and unconditionally restores the animation target's transform/alpha
56+
* and the underlay opacity to neutral values.
57+
*/
58+
- (void)prepareForRecycle;
59+
5260
/**
5361
* Updates the underlay layer's corner radii with separate horizontal/vertical
5462
* components per corner, supporting elliptical inner corners when border widths

packages/react-native-gesture-handler/apple/RNGestureHandlerButton.mm

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,28 @@ - (void)cancelPendingPressOutAnimation
117117
[_underlayLayer removeAllAnimations];
118118
}
119119

120+
- (void)prepareForRecycle
121+
{
122+
// Fabric reuses the same wrapper + button instance across mounts. Without
123+
// this reset, residual press-in transform/alpha/underlay-opacity from a
124+
// prior use leaks into the recycled view, and `updateProps:` won't undo it
125+
// when defaults are unchanged between mounts.
126+
[self cancelPendingPressOutAnimation];
127+
128+
RNGHUIView *target = self.animationTarget ?: self;
129+
target.layer.transform = CATransform3DIdentity;
130+
#if !TARGET_OS_OSX
131+
target.alpha = 1.0;
132+
#else
133+
target.alphaValue = 1.0;
134+
#endif
135+
_underlayLayer.opacity = 0;
136+
137+
_isTouchInsideBounds = NO;
138+
_suppressSuperControlActionDispatch = NO;
139+
_pressInTimestamp = 0;
140+
}
141+
120142
#if TARGET_OS_OSX
121143
- (void)viewWillMoveToWindow:(RNGHWindow *)newWindow
122144
{

packages/react-native-gesture-handler/apple/RNGestureHandlerButtonComponentView.mm

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)