Building Pull To Action Interaction — Part II

Karthik Balasubramanian
Timeless
6 min readJul 29, 2023

--

Hey folks. Welcome back.

This is the continuation of my previous blog where we would have started setting up to build Pull To Action Interaction.

In this blog, we will create the base component, and breakdown the Gesture using Gesture Handler Library and Reanimated from Software Mansion.

Cover Image

State Management

We use useState and useSharedValue to manage the Interaction states in UI and JS threads.

  const translateValue = useSharedValue(0);
const refreshTranslateValue = useSharedValue(0);

const currentSegment = useSharedValue(-1);
const translateX = useSharedValue(0);
const refreshRotationValue = useSharedValue(0);

const startXDistance = useSharedValue(0);

const selectionActive = useSharedValue(0);

const { bottom, top } = useSafeAreaInsets();
const [currentActionTarget, setCurrentActioinTarget] = useState<
ACTION_TYPE | ""
>("");

const hapticActive = useHaptic("medium");
const hapticSelection = useHaptic();

useHaptic is a hook I wrote to simplify things, check this blog out for more.

Simultaneous Gesture Handling

We will be using the Gesture.Pan() and also Gesture.Native() to handle gestures with the native scroll view gestures.

This gesture is more specific to a screen which has a scrolling view, and we intend to show the actions when the ScrollView is pulled down after reaching the top.

<GestureDetector
gesture={Gesture.Simultaneous(panGesture, scrollViewGesture)}
>
<Animated.ScrollView
style={tailwind.style("flex-1 overflow-visible")}
scrollEventThrottle={16}
>
<--- Action Section--->
<Animated.View style={tailwind.style("")}>
<Text style={tailwind.style("text-3xl font-bold px-5 pt-[18px]")}>
Discussion
</Text>
</Animated.View>
</Animated.ScrollView>
</GestureDetector>

The ScrollView is wrapped inside a GestureDetector with a gesture prop which is set to the Gesture.Simultaneous(panGesture, scrollViewGesture), scrollViewGesture is set to Gesture.Native().

A Native Gesture is the one that allows other touch handling components to participate in RNGH’s gesture system. When used, the other component should be the direct child of a GestureDetector. Here you can see the ScrollView is the direct child, so it allows all the gestures of ScrollView with the PanGesture we want. Check more here.

ScrollView Child Components

We have our “Actions Section” inside the ScrollView. Initially, the section height is set to 0, and as it is pulled down the height is increased, revealing the set of Actions, which are Refresh, Search, and Cancel.

<Animated.View
style={[
tailwind.style("flex flex-row items-center justify-between px-3"),
animatedViewStyle,
]}
>
<AnimatedBlurView
intensity={100}
style={[
tailwind.style(
"absolute h-15 w-15 overflow-hidden bg-white rounded-full",
),
currentSegmentAnimatedStyle,
]}
/>
<Animated.View
style={[
tailwind.style("flex-1 items-center"),
refreshIconAnimatedStyle,
]}
>
<RefreshIcon />
</Animated.View>
<Animated.View
style={[
tailwind.style("flex-1 items-center"),
searchIconAnimatedStyle,
]}
>
<SearchActionIcon />
</Animated.View>
<Animated.View
style={[
tailwind.style("flex-1 items-center"),
cancelIconAnimatedStyle,
]}
>
<CancelAction />
</Animated.View>
</Animated.View>

This wraps the basic code required to start working on the Gesture. Let us now handle the Gesture.

Handling the Pan Gesture

This is the heart of every interaction we will build, where we decide what happens with a movement of a finger 👆🏼.

  const panGesture = Gesture.Pan()
.onBegin(event => {
const segment = 2;
const calculatedTranslateValue =
SEGMENT_WIDTH * (segment - 1) + SEGMENT_WIDTH / 2 + PADDING - 60 / 2;
currentSegment.value = segment - 1;
translateX.value = calculatedTranslateValue;
startXDistance.value = event.x;
})
.onChange(event => {
translateValue.value = event.translationY > 0 ? event.translationY : 0;
refreshTranslateValue.value = translateValue.value;

const activatePullToAction = interpolate(
translateValue.value,
[0, 80],
[0, 180],
Extrapolation.CLAMP,
);
if (activatePullToAction === 180) {
selectionActive.value = 1;
} else {
selectionActive.value = 0;
}

const segment = getCurrentSegment(event.x);
if (Math.abs(event.translationX) >= 50 && event.translationY >= 80) {
if (segment - 1 < ACTIONS) {
currentSegment.value = segment - 1;
const calculatedTranslateValue =
SEGMENT_WIDTH * (segment - 1) +
SEGMENT_WIDTH / 2 +
PADDING -
60 / 2;

translateX.value = withSpring(calculatedTranslateValue, {
damping: 24,
stiffness: 250,
mass: 1,
});
}
}
})
.onEnd(event => {
if (selectionActive.value) {
runOnJS(setAction)(ACTIONS_LIST[currentSegment.value]);
}
if (
ACTIONS_LIST[currentSegment.value] === "Refresh" &&
event.translationY >= 80
) {
refreshTranslateValue.value = withTiming(80, {
duration: 300,
easing: Easing.inOut(Easing.ease),
});
} else {
refreshTranslateValue.value = withTiming(0, {
duration: 300,
easing: Easing.inOut(Easing.ease),
});
}
currentSegment.value = -1;
selectionActive.value = 0;
translateValue.value = withTiming(0, {
duration: 350,
easing: Easing.linear,
});
})
.enabled(setCurrentActioinTarget === "");

The above code sets up a pan gesture using the Gesture Handler library, allowing the user to interact with our ScrollView by dragging their finger vertically and horizontally on the screen.

The panGesture is enabled based on the value of currentActionTarget, if it's not set to anything, i.e., there is no ongoing action, it is disabled otherwise.

onBegin() Callback

This is triggered when the user starts dragging their finger on the screen.

We initially set the current segment to 2, which is the middle segment, i.e., the Search.

Calculate the X-translation to the current segment, simple math, where we multiply the translation/displacement value from the edge of the screen and subtract the value width of the moving segment to center the container.

We update the currentSegment, the change in value is used to give a haptic feedback to the user.

We don't activate the pull to action yet because we need the finger to move a certain distance in the y-direction.

onChange() Callback

This is triggered when the user is dragging their finger on the screen.

The translateValue is updated only with the positive vertical translation of the pan gesture, and otherwise it is set to 0, to prevent negative values.

The refreshTranslateValue.value is also updated with the same value as translateValue. This is because the refresh action is a special action, which when selected has to perform another translation. So we manage it with a separate SharedValue variable.

Next, is a very specific interpolation case of the translate.value, which represents the progress of the pull-to-refresh action. If the value has reached 180, it means the action is activated and the selctionActive.value is set to 1, triggering a haptic feedback using the useAnimatedReaction().

useAnimatedReaction(
() => selectionActive.value,
(next, _prev) => {
if (next === 1) {
hapticActive && runOnJS(hapticActive)();
}
},
);

Then we calculate the segment based on the x-position value, using a worklet function.

const segment = getCurrentSegment(event.x);
const getCurrentSegment = (gestureX: number) => {
"worklet";
return Math.ceil(gestureX / ((SCREEN_WIDTH - PADDING * 2) / ACTIONS));
};

And now if the x-translation exceeds 50 and the y-translation exceeds 80 (pull to action is activated), we calculate a new translation value based on the current segment, and set it to translateX value using withSpring().

We also check if the segment value is less than the no. of actions to avoid any errors on accessing the array.

onEnd() Callback

Well, this is the place we wrap the gesture, activate the action or cancel depending upon how it ends.

If the selectionActive.value is true, that is 1, means that the user activated the pull to action by dragging vertically beyond a certain distance, so we set the corresponding action from the ACTIONS_LIST based on the currentSegment.value.

We reset all the animated shared values to their default.

And now, as the Refresh Action is a special case, we write a translation case which snaps the ScrollView to a top, but the Action Section has the height, so we get to see the refreshing state.

Well, there you go. We have wrapped our Gesture Handling, set values to Shared Values. In the next blog, we will see how we can use these to manipulate the styles and wrap out our Interaction. :)

Check it out here,

This is Karthik from Timeless

See you guys in the final part of my blog. 👋🏻

Thank you for reading!

--

--