Building Pull To Action Interaction — Part II
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.
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,