Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,14 @@
*/
- (void)applyStartAnimationState;

/**
* Resets all transient press/animation state so the button can be safely reused
* by Fabric view recycling. Cancels pending press-out blocks, removes in-flight
* animations, and unconditionally restores the animation target's transform/alpha
* and the underlay opacity to neutral values.
*/
- (void)prepareForRecycle;

/**
* Updates the underlay layer's corner radii with separate horizontal/vertical
* components per corner, supporting elliptical inner corners when border widths
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,28 @@ - (void)cancelPendingPressOutAnimation
[_underlayLayer removeAllAnimations];
}

- (void)prepareForRecycle
{
// Fabric reuses the same wrapper + button instance across mounts. Without
// this reset, residual press-in transform/alpha/underlay-opacity from a
// prior use leaks into the recycled view, and `updateProps:` won't undo it
// when defaults are unchanged between mounts.
[self cancelPendingPressOutAnimation];

RNGHUIView *target = self.animationTarget ?: self;
target.layer.transform = CATransform3DIdentity;
#if !TARGET_OS_OSX
target.alpha = 1.0;
#else
target.alphaValue = 1.0;
#endif
_underlayLayer.opacity = 0;

_isTouchInsideBounds = NO;
_suppressSuperControlActionDispatch = NO;
_pressInTimestamp = 0;
}

#if TARGET_OS_OSX
- (void)viewWillMoveToWindow:(RNGHWindow *)newWindow
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ @interface RNGestureHandlerButtonComponentView () <RCTRNGestureHandlerButtonView

@implementation RNGestureHandlerButtonComponentView {
RNGestureHandlerButton *_buttonView;
BOOL _needsAnimationStateReset;
}

#if TARGET_OS_OSX
Expand Down Expand Up @@ -67,6 +68,25 @@ - (instancetype)initWithFrame:(CGRect)frame
return self;
}

- (void)prepareForRecycle
{
// Reset the wrapper layer's transform (and any in-flight transform animation)
// before super resets _props, so the recycled view starts in a clean visual
// state regardless of whether the next updateProps: re-runs applyStartAnimationState.
[self.layer removeAnimationForKey:@"transform"];
self.layer.transform = CATransform3DIdentity;

[_buttonView prepareForRecycle];

// The next updateProps: comparison treats this view as having "previous"
// props equal to defaults, so identical defaults across mounts won't trigger
// applyStartAnimationState — leaving non-1.0 default opacity/scale/underlay
// values unapplied. Force a one-shot reset on the next updateProps:.
_needsAnimationStateReset = YES;

[super prepareForRecycle];
}

- (void)mountChildComponentView:(RNGHUIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
{
[_buttonView mountChildComponentView:childComponentView index:index];
Expand Down Expand Up @@ -143,8 +163,28 @@ - (void)updateLayoutMetrics:(const facebook::react::LayoutMetrics &)layoutMetric

- (void)finalizeUpdates:(RNComponentViewUpdateMask)updateMask
{
// super.finalizeUpdates calls invalidateLayer, which unconditionally sets
// self.layer.opacity = _props->opacity (the React style.opacity, default
// 1.0). That overwrites the alpha set by applyStartAnimationState for non-
// 1.0 defaultOpacity, and would also interrupt in-flight press animations
// on a mid-press React re-render. Snapshot opacity (and any animation) and
// restore it atomically.
float savedOpacity = self.layer.opacity;
CAAnimation *savedOpacityAnimation = [[self.layer animationForKey:@"opacity"] copy];

[super finalizeUpdates:updateMask];

if (savedOpacity != self.layer.opacity || savedOpacityAnimation != nil) {
[CATransaction begin];
[CATransaction setDisableActions:YES];
[self.layer removeAnimationForKey:@"opacity"];
self.layer.opacity = savedOpacity;
Comment thread
j-piasecki marked this conversation as resolved.
Outdated
if (savedOpacityAnimation) {
[self.layer addAnimation:savedOpacityAnimation forKey:@"opacity"];
}
[CATransaction commit];
}

// Resolve per-corner border radii from props and forward to the button
// so its underlay CALayer gets the matching shape.
const auto borderMetrics = _props->resolveBorderMetrics(_layoutMetrics);
Expand Down Expand Up @@ -267,11 +307,13 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &
{
const auto &newProps = *std::static_pointer_cast<const RNGestureHandlerButtonProps>(props);

// Re-apply the idle visual state only on first mount or when one of the default
// values actually changed. Doing it on every commit interrupts in-flight press
// animations whenever React re-renders the children mid-press.
BOOL shouldApplyStartAnimationState = !oldProps;
if (oldProps) {
// Re-apply the idle visual state only on first mount, after recycling, or
// when one of the default values actually changed. Doing it on every commit
// interrupts in-flight press animations whenever React re-renders the children
// mid-press.
BOOL shouldApplyStartAnimationState = !oldProps || _needsAnimationStateReset;
_needsAnimationStateReset = NO;
if (oldProps && !shouldApplyStartAnimationState) {
Comment thread
j-piasecki marked this conversation as resolved.
Outdated
const auto &oldButtonProps = *std::static_pointer_cast<const RNGestureHandlerButtonProps>(oldProps);
shouldApplyStartAnimationState = oldButtonProps.defaultOpacity != newProps.defaultOpacity ||
oldButtonProps.defaultScale != newProps.defaultScale ||
Expand Down
Loading