Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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,20 @@ - (instancetype)initWithFrame:(CGRect)frame
return self;
}

- (void)prepareForRecycle
{
[self.layer removeAnimationForKey:@"transform"];
self.layer.transform = CATransform3DIdentity;

[_buttonView prepareForRecycle];

// Force the next updateProps: to re-run applyStartAnimationState even if
// the new mount's defaults match the previous mount's.
_needsAnimationStateReset = YES;

[super prepareForRecycle];
}

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

- (void)finalizeUpdates:(RNComponentViewUpdateMask)updateMask
{
// super's invalidateLayer (called via finalizeUpdates) unconditionally sets
// self.layer.opacity to the React style.opacity, overwriting our
// applyStartAnimationState alpha and interrupting in-flight press
// animations. Save/restore around super, but only touch what super actually
// disturbed — re-adding an unchanged animation resets its progress.
float savedOpacity = self.layer.opacity;
CAAnimation *savedOpacityAnimation = [self.layer animationForKey:@"opacity"];

[super finalizeUpdates:updateMask];

BOOL opacityChanged = savedOpacity != self.layer.opacity;
BOOL animationChanged = savedOpacityAnimation != [self.layer animationForKey:@"opacity"];
if (opacityChanged || animationChanged) {
[CATransaction begin];
[CATransaction setDisableActions:YES];
if (animationChanged) {
[self.layer removeAnimationForKey:@"opacity"];
if (savedOpacityAnimation) {
[self.layer addAnimation:savedOpacityAnimation forKey:@"opacity"];
}
}
if (opacityChanged) {
self.layer.opacity = savedOpacity;
}
[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,15 @@ - (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) {
// After recycling, treat diffing branches as a fresh mount so values that
// survived recycling on _buttonView (e.g. pointerEvents) get re-applied.
BOOL treatAsFirstMount = !oldProps || _needsAnimationStateReset;
_needsAnimationStateReset = NO;

// Avoid re-running applyStartAnimationState on every commit — it would
// interrupt in-flight press animations during mid-press re-renders.
BOOL shouldApplyStartAnimationState = treatAsFirstMount;
if (!treatAsFirstMount) {
Comment thread
j-piasecki marked this conversation as resolved.
const auto &oldButtonProps = *std::static_pointer_cast<const RNGestureHandlerButtonProps>(oldProps);
shouldApplyStartAnimationState = oldButtonProps.defaultOpacity != newProps.defaultOpacity ||
oldButtonProps.defaultScale != newProps.defaultScale ||
Expand Down Expand Up @@ -303,7 +347,7 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &
// This is necessary because pointerEvents is redefined in the spec,
// which shadows the base property with a different, incompatible type.
const auto &newViewProps = static_cast<const ViewProps &>(newProps);
if (!oldProps) {
if (treatAsFirstMount) {
_buttonView.pointerEvents = RCTPointerEventsToEnum(newViewProps.pointerEvents);
} else {
const auto &oldButtonProps = *std::static_pointer_cast<const RNGestureHandlerButtonProps>(oldProps);
Expand Down
Loading