Skip to content

Commit

Permalink
Fix Swipeable misalignment after resizing on web (#3341)
Browse files Browse the repository at this point in the history
## Description

This PR allows Swipeable to be dynamically resized.

Fixes #3333

## Test plan

- open any swipeable example on web
- resize window
- see how the `ReanimatedSwipeable` still works, while the legacy
`Swipeable` is misaligned
  • Loading branch information
latekvo authored Jan 22, 2025
1 parent cb2c528 commit 44f6315
Showing 1 changed file with 83 additions and 64 deletions.
147 changes: 83 additions & 64 deletions src/components/ReanimatedSwipeable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,18 @@ import {
import type { PanGestureHandlerProps } from '../handlers/PanGestureHandler';
import type { PanGestureHandlerEventPayload } from '../handlers/GestureHandlerEventPayload';
import Animated, {
ReduceMotion,
SharedValue,
interpolate,
measure,
runOnJS,
runOnUI,
useAnimatedRef,
useAnimatedStyle,
useSharedValue,
withSpring,
} from 'react-native-reanimated';
import {
Dimensions,
I18nManager,
LayoutChangeEvent,
StyleProp,
Expand Down Expand Up @@ -257,20 +260,9 @@ const Swipeable = forwardRef<SwipeableMethods, SwipeableProps>(
const leftWidth = useSharedValue<number>(0);
const rightWidth = useSharedValue<number>(0);

// used for synchronizing layout measurements between JS and UI
const rightOffset = useSharedValue<number | null>(null);

const showLeftProgress = useSharedValue<number>(0);
const showRightProgress = useSharedValue<number>(0);

const updateRightElementWidth = useCallback(() => {
'worklet';
if (rightOffset.value === null) {
rightOffset.value = rowWidth.value;
}
rightWidth.value = Math.max(0, rowWidth.value - rightOffset.value);
}, [rightOffset, rightWidth, rowWidth]);

const updateAnimatedEvent = useCallback(() => {
'worklet';

Expand Down Expand Up @@ -372,11 +364,12 @@ const Swipeable = forwardRef<SwipeableMethods, SwipeableProps>(
'worklet';

const translationSpringConfig = {
duration: 1000,
dampingRatio: 0.9,
stiffness: 500,
mass: 2,
damping: 1000,
stiffness: 700,
velocity: velocityX,
overshootClamping: true,
reduceMotion: ReduceMotion.System,
...animationOptions,
};

Expand Down Expand Up @@ -412,16 +405,17 @@ const Swipeable = forwardRef<SwipeableMethods, SwipeableProps>(
}
);

const progressTarget = toValue === 0 ? 0 : 1;
const progressTarget = toValue === 0 ? 0 : 1 * Math.sign(toValue);

showLeftProgress.value =
showLeftProgress.value > 0
? withSpring(progressTarget, progressSpringConfig)
: 0;
showRightProgress.value =
showRightProgress.value > 0
? withSpring(progressTarget, progressSpringConfig)
: 0;
showLeftProgress.value = withSpring(
Math.max(progressTarget, 0),
progressSpringConfig
);

showRightProgress.value = withSpring(
Math.max(-progressTarget, 0),
progressSpringConfig
);

dispatchImmediateEvents(frozenRowState, toValue);

Expand All @@ -440,20 +434,66 @@ const Swipeable = forwardRef<SwipeableMethods, SwipeableProps>(
]
);

const leftLayoutRef = useAnimatedRef();
const leftWrapperLayoutRef = useAnimatedRef();
const rightLayoutRef = useAnimatedRef();

const updateElementWidths = useCallback(() => {
'worklet';
const leftLayout = measure(leftLayoutRef);
const leftWrapperLayout = measure(leftWrapperLayoutRef);
const rightLayout = measure(rightLayoutRef);
leftWidth.value =
(leftLayout?.pageX ?? 0) - (leftWrapperLayout?.pageX ?? 0);

rightWidth.value =
rowWidth.value -
(rightLayout?.pageX ?? rowWidth.value) +
(leftWrapperLayout?.pageX ?? 0);
}, [
leftLayoutRef,
leftWrapperLayoutRef,
rightLayoutRef,
leftWidth,
rightWidth,
rowWidth.value,
]);

const swipeableMethods = useMemo<SwipeableMethods>(
() => ({
close() {
'worklet';
animateRow(0);
if (_WORKLET) {
animateRow(0);
return;
}
runOnUI(() => {
animateRow(0);
})();
},
openLeft() {
'worklet';
animateRow(leftWidth.value);
if (_WORKLET) {
updateElementWidths();
animateRow(leftWidth.value);
return;
}
runOnUI(() => {
updateElementWidths();
animateRow(leftWidth.value);
})();
},
openRight() {
'worklet';
// rightOffset and rowWidth are already much sooner than rightWidth
animateRow((rightOffset.value ?? 0) - rowWidth.value);
if (_WORKLET) {
updateElementWidths();
animateRow(-rightWidth.value);
return;
}
runOnUI(() => {
updateElementWidths();
animateRow(-rightWidth.value);
})();
},
reset() {
'worklet';
Expand All @@ -464,14 +504,14 @@ const Swipeable = forwardRef<SwipeableMethods, SwipeableProps>(
},
}),
[
animateRow,
updateElementWidths,
leftWidth,
rightOffset,
rowWidth,
rightWidth,
userDrag,
showLeftProgress,
appliedTranslation,
rowState,
animateRow,
]
);

Expand All @@ -484,38 +524,31 @@ const Swipeable = forwardRef<SwipeableMethods, SwipeableProps>(

// As stated in `Dimensions.get` docstring, this function should be called on every render
// since dimensions may change (e.g. orientation change)
const hiddenSwipeableOffset = Dimensions.get('window').width + 1;

const leftActionAnimation = useAnimatedStyle(() => {
return {
transform: [
{
translateX:
showLeftProgress.value === 0 ? -hiddenSwipeableOffset : 0,
},
],
opacity: showLeftProgress.value === 0 ? 0 : 1,
};
});

const leftElement = useCallback(
() => (
<Animated.View style={[styles.leftActions, leftActionAnimation]}>
<Animated.View
ref={leftWrapperLayoutRef}
style={[styles.leftActions, leftActionAnimation]}>
{renderLeftActions?.(
showLeftProgress,
appliedTranslation,
swipeableMethods
)}
<View
onLayout={({ nativeEvent }) =>
(leftWidth.value = nativeEvent.layout.x)
}
/>
<Animated.View ref={leftLayoutRef} />
</Animated.View>
),
[
appliedTranslation,
leftActionAnimation,
leftWidth,
leftLayoutRef,
leftWrapperLayoutRef,
renderLeftActions,
showLeftProgress,
swipeableMethods,
Expand All @@ -524,12 +557,7 @@ const Swipeable = forwardRef<SwipeableMethods, SwipeableProps>(

const rightActionAnimation = useAnimatedStyle(() => {
return {
transform: [
{
translateX:
showRightProgress.value === 0 ? hiddenSwipeableOffset : 0,
},
],
opacity: showRightProgress.value === 0 ? 0 : 1,
};
});

Expand All @@ -541,18 +569,14 @@ const Swipeable = forwardRef<SwipeableMethods, SwipeableProps>(
appliedTranslation,
swipeableMethods
)}
<View
onLayout={({ nativeEvent }) => {
rightOffset.value = nativeEvent.layout.x;
}}
/>
<Animated.View ref={rightLayoutRef} />
</Animated.View>
),
[
appliedTranslation,
renderRightActions,
rightActionAnimation,
rightOffset,
rightLayoutRef,
showRightProgress,
swipeableMethods,
]
Expand All @@ -564,8 +588,6 @@ const Swipeable = forwardRef<SwipeableMethods, SwipeableProps>(
const { velocityX } = event;
userDrag.value = event.translationX;

updateRightElementWidth();

const leftThresholdProp = leftThreshold ?? leftWidth.value / 2;
const rightThresholdProp = rightThreshold ?? rightWidth.value / 2;

Expand Down Expand Up @@ -603,7 +625,6 @@ const Swipeable = forwardRef<SwipeableMethods, SwipeableProps>(
rightWidth,
rowState,
userDrag,
updateRightElementWidth,
]
);

Expand Down Expand Up @@ -632,9 +653,7 @@ const Swipeable = forwardRef<SwipeableMethods, SwipeableProps>(
.enabled(enabled !== false)
.enableTrackpadTwoFingerGesture(enableTrackpadTwoFingerGesture)
.activeOffsetX([-dragOffsetFromRightEdge, dragOffsetFromLeftEdge])
.onStart(() => {
updateRightElementWidth();
})
.onStart(updateElementWidths)
.onUpdate(
(event: GestureUpdateEvent<PanGestureHandlerEventPayload>) => {
userDrag.value = event.translationX;
Expand Down Expand Up @@ -679,7 +698,7 @@ const Swipeable = forwardRef<SwipeableMethods, SwipeableProps>(
onSwipeableOpenStartDrag,
rowState,
updateAnimatedEvent,
updateRightElementWidth,
updateElementWidths,
userDrag,
]
);
Expand Down

0 comments on commit 44f6315

Please sign in to comment.