How We Animate Header Titles in React Native
When creating our parallax headers in React Native, we encountered what seemed to be an impossible task: using the Animated.interpolate()
function to transition our styles between two Flexbox properties.
Our intention to make our text transition from flex-start
to center
was unfortunately not as straightforward to implement as we had hoped. Looking at the documentation for Animated
, we came to understand that while interpolate
accepts strings for values, it is limited to colors and values with units.
We found ourselves in a place where we needed to get creative with the Animated API, in order to replicate our Flexbox transition another way.
Creating our animation
Above is a gif of what we will be building in this article. If you’d like to jump right to a working example, head over to https://snack.expo.io/SkK4P4UC-Otherwise, let’s dive in!
Initial setup
Here’s what you’ll need to get started:
- A component with a custom parallax header (for help setting one of these up check out any of these articles)
- A ScrollView, FlatList, etc in your component (we will utilize the
onScroll
event to trigger changes to our animation) - Enough content in your scrolling list to test our animation
Step 1: Setting up our animated header
There are a variety of ways to set up your header, based on your use case. Here is our setup for this example; below the code you’ll find an explanation of the important parts (highlighted in bold) that will help us animate our text.
screenWidth = Dimensions.get('window').width;<Animated.View
style={
flexDirection: 'row',
justifyContent: 'center',
paddingHorizontal: screenWidth * 0.05,
width: screenWidth,
alignItems: 'flex-end',
height: this.state.scrollOffset.interpolate({
inputRange: [0, 200],
outputRange: [120, 64],
extrapolate: 'clamp',
}),
}
>
The important parts (and why):
flexDirection: 'row'
— allows us to animate our text horizontallyjustifyContent: 'center'
— allows us to control our text alignmentpaddingHorizontal
— this gives us the exact width of the area where our title is renderedwidth: screenWidth
— to ensure this truly spans the entire screen, we want to explicitly define this value (rather than setting it to'100%'
)
Step 2: Left-aligning our text whilst centering our content
You may have noted that our header styles containjustifyContent: 'center'
— that will help us get things centered at the end of our animation.
In order to start our text in a left-aligned state, however, we must be a little clever. React Native provides a handy onLayout
callback which informs us of our view’s exact size on the screen after it is rendered. Here is how we are utilizing onLayout
for our <Animated.Text>
component rendering our text:
onLayout={e => {
const titleWidth = e.nativeEvent.layout.width;
this.setState({ titleWidth });
}}
By storing the width of our header title text when it renders, we can push it left aligned by adding an invisible view directly to the right of it in our header.
Our header has width equivalent to the screen width, with 5% padding on both the left and right. Therefore, the space where our title is rendered has a width 90% of the screen width. With some simple math, we calculate the width of our invisible view as screenWidth * 0.9 — this.state.titleWidth
.
Without any animations, we now have this:
<Animated.Text
onLayout={e => {
const titleWidth = e.nativeEvent.layout.width;
this.setState({ titleWidth });
}}
style={{
fontWeight: 'bold',
fontSize: 26,
}}
>
Header Title Here
</Animated.Text>
<Animated.View
style={{
width: screenWidth * 0.9 - this.state.titleWidth,
}}
/>
When we render this view next to our text, our text is left-aligned! Voila!
Step 3: Animating the text alignment
Next, we simply need to animate the width of our invisible view from its initial width down to zero at the conclusion of our animation, and our text will be centered. You can add any other animations you like to your text, header, etc. Here’s our implementation:
<Animated.Text
onLayout={e => {
if (this.offset === 0 && this.state.titleWidth === 0) {
const titleWidth = e.nativeEvent.layout.width;
this.setState({ titleWidth });
}
}}
style={{
fontWeight: 'bold',
fontSize: scrollOffset.interpolate({
inputRange: [0, 200],
outputRange: [26, 20],
extrapolate: 'clamp',
}),
}}>
Header Title Here
</Animated.Text>
<Animated.View
style={{
width: scrollOffset.interpolate({
inputRange: [0, 200],
outputRange: [screenWidth * 0.9 - this.state.titleWidth, 0],
extrapolate: 'clamp',
}),
}}
/>
The Final Product
Now we just need to wire up our onScroll
listener in our ScrollView and watch our animation in all its beauty! Check out the full working example here: https://snack.expo.io/SkK4P4UC-
A couple notes:
1 . We added some safeguards in the onLayout
function of our <Animated.Text>
component. We really only need to set the value of titleWidth
once, upon initialization. The width of our rendered text (in its initial state) should not change, so we added an if-statement wrapping our setState
function which confirmstitleWidth
does not yet have a value and ensures the scroll offset is equal to zero.
2. An unhandled case in this example is where titleWidth > screenWidth * 0.9
. I didn’t include it here for simplicity, but in our app we ellipse the text to fill the width, and keep the width of our invisible set at zero for the entire animation.
Thanks for reading! Please feel free to leave any questions, comments, suggestions, or concerns below.