A while ago, I had the pleasure of implementing a feature that allows users to choose their favorite color which will be associated with their profile.
This article will be dedicated to how I went about creating such a feature and boosting it with some cool animations using Reanimated v2 in React Native.
To make things easier, I prepared a repository with the starter and the completed code. See link
The master branch contains the final animation, while the starter branch features basic setup for starters.
First things first, let’s clone the repo:
Install packages:
For iOS also:
Run the project:
At this point the project looks like this, there’s no animation, the bubbles render on top of each other and we can’t move them around just yet.
Please note that this project uses TypeScript, however you do not need to be familiar with TypeScript in order to follow and understand the content of this article.
There are two components that we’ll focus on in this article:
React Context is used to keep the information about the user’s chosen color available to all the components (See documentation).
All dependencies have already been added to the project, including react-native-reanimated. Refer to the official docs for the installation and configuration guidelines.
Let’s start with rendering the bubbles in random positions with a random delay. We can achieve this by implementing two simple functions.
First, to calculate the initial position of each bubble within our screen view.
Then, to calculate render delay of each bubble. Max delay set for this animation is 600ms, feel free to adjust it to your preferences.
Let’s move to BubbleContainer.tsx, where we begin with parsing variable COLORS to have an additional property: position. Initially, COLORS is an array of objects in format:
Parsed and saved to state:
Here’s where our function randomFromRange comes into use. We set the range for x-axis to be from 0px (C.BUBBLES_OFFSET_LEFT) to the width of the window reduced by 56px (C.BUBBLE_SIZE). The range for y-axis varies from 130px (that’s the space that the description message takes - C.BUBBLES_OFFSET_TOP) to the height of the window reduced by the height of the drop area plus the size of the bubble (C.BUBBLES_OFFSET_BOTTOM). By doing so, we prevent rendering any bubble outside of the screen view.
At this point our bubbles render in random positions within the dark grey space.
Now, let’s move to implementing a random render delay.
We move to Bubble.tsx component.
First, we need to change View from react-native to Animated.View from react-native-reanimated and wrap it all with PanGestureHandler from react-native-gesture-handler, so the component looks like this:
Now, we implement the actual animation and update the return method accordingly.
Here, useAnimatedStyle comes in handy. As per the documentation, it allows to create an association between Shared Values and View properties and subscribes to any changes and styles. It takes in a function as an argument and returns an object with new style properties. We use two animation helpers withDelay and withTiming to smoothly animate any style changes.
To create an impression that the bubbles render asynchronously, their initial size is set to 0px and we animate the change of width, height and borderRadius. This is controlled by the hook useSharedValue that creates the reference to Shared Value, which can then be modified by worklets.
To read the data from the Shared Value reference, we use .value attribute.
At this point our animation looks like this:
The bubbles render in random places with random delay (up to 600ms) but we can’t move them around just yet. Let’s implement that now.
To allow the user to drag the bubbles around, we need to use another hook useAnimatedGestureHandler. This hook requires react-native-gesture-handler added and configured in our project. Read more
First, we pass onGestureEvent prop to PanGestureHander, like so:
useAnimatedGestureHandler takes in an object with worklets - gesture handlers that are defined under certain keys and triggered based on the current state of the animation. In this article we’ll cover onStart, onActive and onEnd. Each of them receive event and context as arguments. Event objects hold the event payload and context is able to store some data and keep them available for all worklet handlers.
To make an animated transition of each bubble, we create animation objects (translateX and translateY) that run updates on Shared Value resulting in a smooth change between the two values.
See the code below:
At this point we can already drag around our bubbles but they stop immediately once we release the press, which makes the whole experience quite unnatural.
Let’s try to improve that.
To do so, we can add the onEnd method to gestureHandler and use it with withDecay animation helper to smoothly decelerate the bubble’s speed after the user’s press has been released. We set the velocity to be the same as the velocity of the animation, which basically means that the faster we drag the bubble, the longer it’ll take for it to stop fully.
By using clamp prop, we provide animation the boundaries, which guarantee that the bubble will not leave the screen. The logic for defining boundries is similar to the one already used in randomFromRange.
To indicate that the user can trigger some action by dropping the bubble over the drop area, we can modify its scale and make it slightly bigger.
To achieve this effect, we’ll add some logic to the onActive method in gestureHandler. We’ll check if the current position of the dragged bubble on y-axis is below the top of the drop area (that information is passed from BubbleContainer component via props). If so, we’ll change the shared value of the draggedBubbleScale to be 1.2. If not, it’ll stay or come back to the initial 1. To smooth the animation, we’ll use withTiming and duration of 200ms.
We cannot forget to pass the transform prop to BubbleStyle.
Now, that we have a fully responsive animation, it’s time to implement some logic to allow the user to actually save their chosen color.
The idea of this feature is to drag the bubble and drop it in the light grey drop area at the bottom of the screen.
In order for our light grey area to change color and grow to full screen, we modify the drop area from View to Animated.View in wrapper component BubbleContainer:
To implement the logic, we can simply add a conditional statement to onEnd in gestureHandler. We check if the current position of the dragged bubble is on top of the drop area and if its scale is bigger than 1 (in this case it should equal 1.2), we trigger handleSelection function.
In order to call a function from JS thread, we need to use runOnJS from Reanimated. Read more
handleSelection function sets the user’s color and navigates to the previous screen.
If you’d like to know more about how the navigation in React Native works, please refer to the official documentation or have a look at one of the articles in our blog that covers this topic.
To take our feature one step further, we will animate the drop area (changing the color and growing to full screen). In addition, as a nice touch, we’ll change the selected bubble’s opacity, so it blends nicely.
To change the opacity, we’ll create a shared value bubbleOpacity and set its initial value to 1.
Once the color is selected, the bubble will disappear as we change its opacity to 0.
Feel free to add some additional animations here.
Let’s focus on the drop area now.
In BubbleContainer we set shared values for the drop area’s top position and size (height). The trick is to move dropAreaTop from initial bottom offset of 175px (C.DROP_AREA_OFFSET) to 50px above the top of the screen (here C.DROP_AREA_OFFSET_TOP equals -50px) while changing its size simultaneously.
We pass animateDropArea via props to the Bubble component so we can call it inside handleSelection.
To change the color of the drop area after selection, we call animateDropArea in handleSelection function. We wrap goBack() with setTimeout() in order to change the screen just after the animation is completed.
We’ve reached the end of this article. I hope you’ve found some of the information useful.
You are very welcome to play around with my code. You can clone this repository from GitHub.
React Native Reanimated is a powerful library that gives endless possibilities.