Building Pull To Action Interaction — Part III

Karthik Balasubramanian
Timeless
6 min readJul 31, 2023

--

Hey All!

Welcome to the final blog of the series, “Building Pull To Action Interaction”.

Have a look at the previous blog to get a more in-depth understanding of the Pan Gesture in building Interactions.

In this blog, we will see how the gesture handling is translated into animated styles using the useAnimatedStyle() from Reanimated, which is applied to the components to improve the gesture-based interaction.

The setAction()

This function is called at the end of the user gesture with a single parameter, which is the Action Type.

It is a simple function which sets the Action to a React State, using setCurrentActionTarget, if the action is “Cancel”, we don't have to perform anything.

If it is “Refresh”, we will have to do a small rotation of the icon, to indicate the refreshing state and reset it.

I dont recommend using setTimeout() in code, which would cause memory leaks and degrade performances. I think this piece will have to be rewritten in a better way, and can be tweaked to their own cases of handling refresh states. Probably it will have to load until the fetch/axios request is completed. As a POC a setTimeout case works.

  const setAction = (action: ACTION_TYPE) => {
setCurrentActionTarget(action);
if (action === "Cancel") {
setCurrentActionTarget("");
return;
}
if (action !== "Search") {
if (action === "Refresh") {
refreshRotationValue.value = withRepeat(
withTiming(360, { duration: 1000, easing: Easing.linear }),
4,
false,
);
}
setTimeout(() => {
refreshTranslateValue.value = withTiming(
0,
{
duration: 300,
easing: Easing.inOut(Easing.ease),
},
finished => {
if (finished) {
runOnJS(setCurrentActioinTarget)("");
}
},
);
refreshRotationValue.value = 0;
}, 3000);
}
};

The useAnimatedStyles()

We have five different animated styles created using the useAnimatedStyle() from the reanimated library.

The first one is used to control the height of the view based on the translation value. If the set action target is refresh, it gets the height value from refreshTranslateValue, or otherwise it is the default translate.value.

  const animatedViewStyle = useAnimatedStyle(() => {
return {
height:
currentActionTarget === "Refresh"
? refreshTranslateValue.value
: translateValue.value,
};
});

The next is the currentSegmentAnimatedStyle which controls the opacity and horizontal translation and vertical translation (appear and sets to 0 when the action is fully activated) of the Animated.View which corresponds to the current selected segment.

  const currentSegmentAnimatedStyle = useAnimatedStyle(() => {
return {
opacity: interpolate(
translateValue.value,
[70, 80],
[0, 0.5],
Extrapolation.CLAMP,
),
transform: [
{
translateX: translateX.value,
},
{
translateY: interpolate(
translateValue.value,
[70, 80],
[-30, 0],
Extrapolation.CLAMP,
),
},
],
};
});

Next is the refreshIconAnimatedStyle which controls the opacity, the horizontal translation, appearing from the center and moving towards the left, and rotation of the icon (representing the refresh action). The translateX animation depends on the currentActionTarget, if it is “Refresh”, it moves the icon to the center of the screen, and otherwise the icon leaves the screen by moving towards the center (diagonally).

  const refreshIconAnimatedStyle = useAnimatedStyle(() => {
return {
opacity: interpolate(
refreshTranslateValue.value,
[40, 70],
[0, 1],
Extrapolation.CLAMP,
),
transform: [
{
translateX:
currentActionTarget === "Refresh"
? withTiming(REFRESH_TRANSLATE, {
duration: 300,
easing: Easing.linear,
})
: interpolate(
refreshTranslateValue.value,
[0, 80],
[80, 0],
Extrapolation.CLAMP,
),
},
{ rotate: `${refreshRotationValue.value}deg` },
],
};
});

Next is cancelIconAnimatedStyle, which also controls the opacity and the translateX, slightly different interpolation values from the refresh animation style because it appears from the center and move towards the right.

  const cancelIconAnimatedStyle = useAnimatedStyle(() => {
return {
opacity: interpolate(
translateValue.value,
[40, 70],
[0, 1],
Extrapolation.CLAMP,
),
transform: [
{
translateX: interpolate(
translateValue.value,
[0, 80],
[-80, 0],
Extrapolation.CLAMP,
),
},
],
};
});

The final one is the searchIconAnimatedStyle, which also controls the opacity and the translation, similar to the other two icons, but also slightly gives a rotational and scale effect when the whole “Pull to action” has been activated.

  const searchIconAnimatedStyle = useAnimatedStyle(() => {
return {
opacity: interpolate(
translateValue.value,
[40, 70],
[0, 1],
Extrapolation.CLAMP,
),
transform: [
{
rotate: `${interpolate(
translateValue.value,
[0, 80],
[-30, 0],
Extrapolation.CLAMP,
)}deg`,
},
{
scale: interpolate(
translateValue.value,
[0, 80],
[0.9, 1],
Extrapolation.CLAMP,
),
},
],
};
});

The Search Command Palette

I added Search Command Interaction similar to the iPhone Spotlight Search, which appears on pull down.

Search Command in Interaction (left) iOS Search Spotlight (right)

This is a simple mockup code, with set actions based on the “Discussions” Page we assumed we are in.

You can make it appear when the user sets the currentActionTarget to “Search”.

      {currentActionTarget === "Search" ? (
<AnimatedBlurView
intensity={10}
entering={FadeIn}
exiting={FadeOut}
style={[tailwind.style("absolute inset-0 z-10")]}
>
<AnimatedBlurView
style={tailwind.style("absolute inset-0")}
intensity={100}
/>
<Pressable
style={[tailwind.style("absolute inset-0 z-20")]}
onPress={() => setCurrentActionTarget("")}
/>
<Animated.View
entering={FadeInDown.springify().damping(18).stiffness(140)}
exiting={FadeOutDown.springify().damping(18).stiffness(140)}
style={tailwind.style(
"bg-white py-2 mx-4 rounded-[19px] z-20",
`mt-[${top}px]`,
)}
>
<Animated.View
style={tailwind.style("relative mx-2 flex flex-row items-center")}
>
<Animated.View
style={tailwind.style(
"absolute justify-center items-center pl-3 z-10",
)}
>
<SearchIcon />
</Animated.View>
<TextInput
autoFocus
returnKeyType="go"
onSubmitEditing={() => setCurrentActionTarget("")}
placeholder="Search"
placeholderTextColor={"#707070"}
style={[styles.textInputStyle]}
/>
</Animated.View>
<Animated.ScrollView>
<Animated.View style={tailwind.style("mt-3 mx-1.5")}>
<Text
style={tailwind.style(
"px-2 text-sm text-[#858585] tracking-wide",
)}
>
Actions
</Text>
<Animated.View style={tailwind.style("px-2")}>
{actionsList.map(value => {
return (
<Animated.View
key={value.title}
style={tailwind.style(
"flex flex-row items-center py-2",
)}
>
{value.icon}
<Text
style={tailwind.style(
"px-2 text-[16px] text-[#171717]",
)}
>
{value.title}
</Text>
</Animated.View>
);
})}
</Animated.View>
</Animated.View>
<Animated.View style={tailwind.style("mt-3 mx-1.5")}>
<Text
style={tailwind.style(
"px-2 text-sm text-[#858585] tracking-wide",
)}
>
People with access
</Text>
<Animated.View style={tailwind.style("px-2")}>
{people.map(value => {
return (
<Animated.View
key={value.name}
style={tailwind.style(
"flex flex-row items-center justify-between py-2",
)}
>
<Animated.View
style={tailwind.style(
"flex flex-row items-center justify-between",
)}
>
<Animated.View
style={tailwind.style(
"h-7 w-7 rounded-full overflow-hidden",
)}
>
<Animated.Image
style={tailwind.style("h-full w-full")}
source={{ uri: value.avatar }}
/>
</Animated.View>
<Text
style={tailwind.style(
"px-2 text-[16px] text-[#171717]",
)}
>
{value.name}
</Text>
</Animated.View>
<Animated.View
style={tailwind.style("flex flex-row items-center")}
>
<Text
style={tailwind.style(
"pr-0.5 text-[15px] font-medium text-[#7C7C7C]",
)}
>
{value.access}
</Text>
{value.access !== "Owner" ? <ChevronDown /> : null}
</Animated.View>
</Animated.View>
);
})}
</Animated.View>
</Animated.View>
</Animated.ScrollView>
</Animated.View>
</AnimatedBlurView>
) : null}

Setting autofocus on in the TextInput will make the Keyboard appear, and you just have to make sure to reset the currentActionTarget when the user has performed the desired search.

This wraps the complete interaction. I hope it was helpful. :)

Will see you all in a new Interaction sometime next month!

This blog is part of the UI Interaction List, take a look at my other blogs on various other Interactions here.

UI Interactions With React Native and Reanimated

26 stories

This is Karthik from Timeless

I hope you found this blog post helpful and informative. If you have any feedback or suggestions, please leave a comment below. I’d love to hear your thoughts and opinions on the topic.

And if you have any ideas for future blog posts, please let me know! I’m constantly looking for new and interesting topics to explore.

Thank you for reading, and I look forward to hearing from you. :)

--

--