With only a few lines of code you can create a simple expanding ScrollView header in React Native. This looks especially great if you have an image as the header as it will add a parallax effect as you scroll down and create a zooming effect on iOS as you bounce the ScrollView when it reaches the top. It works like so:
Create a couple of variables with the height you want the header to be and a state hook to track the offset of the ScrollView along the y axis
const headerHeight = 400
const [yOffset, setYOffset] = useState(0)
Wrap your header and ScrollView in a View and set the position of the header to absolute
. Add an empty view which is the height of the header as the first child of the ScrollView. Track the offset of the ScrollView with the onScroll
prop and set scrollEventThrottle
prop to something between 1-16 (changes the amount of times onScroll
is called per second, with 0 being once per scroll). Set the y offset in the onScroll
event.
<View>
// The header view
<View style={{ position: "absolute", height: headerHeight + (yOffset * -1) >
// .. the header content.. an image for example
</View>
<ScrollView
scrollEventThrottle={16}
onScroll={event => {
setYOffset(event.nativeEvent.contentOffset.y)
}}
>
<View style={{ height: headerHeight }} />
// .. other content
</ScrollView>
</View>
And that’s it! Super simple trick to make your ScrollView look much more interesting and modern.
But wait, there’s more…
Part 2
Animated.View
You might notice quite a bit of jank trying to use the above method as it’s re-rendering the view on each state change, which is being called multiple times a second. A better option is to use Animated.View
to create a buttery smooth animated view for you.
You need an Animated.Value
which will hold the current value of the yOffset
. You should use an useRef
to ensure the value persists between lifecycle changes.
const yOffset = useRef(new Animated.Value(0)).current
useRef.current
will return a mutable ref to the current value.
An Animated.Event
will map our yOffset
– the ScrollView
offset – to the height of our header view.
const yOffsetAnim = Animated.Event([
{
nativeEvent: { contentOffset: { y: yOffset } }
},
{
useNativeDriver: false
}
])
useNativeDriver
will package this animation up into native code when the app is built and run the animated on the UI thread which gives fantastic performance but unfortunately is not supported with values such as width
and height
at the moment. It must be false.
Now we have our animation built, simply replace the ScrollView.onScroll
event to point at our Animated.Event
<ScrollView
...
onScroll={yOffsetAnim}
>
Interpolation
The final part of this tutorial is mapping the value of the offset to the height of the header. To do this we will need to interpolate the yOffset to get the correct height of the header.
<View style={{
...
height: yOffset.interpolate({
inputRange: [-headerHeight, headerHeight],
outputRange: [headerHeight*2, 0],
extrapolateRight: "clamp"
})
>
Lets break this down
The input range is the value which the yOffset could be. What we are saying is “if the yOffset is equal to the negative of the header height, double the size of the header”. Negative, because as you scroll up by pulling down on the screen, the y offset of the ScrollView will be in negative once it reaches the top of its content (the ‘bouncing’ effect) and we therefore want to ‘zoom’ or enlarge the image.
Conversely, when the y offset equals the height of the header, we want the header to be hidden – or have a height of 0.
The clamp
value of extrapolateRight
means that we don’t want to go past 0, even if we scroll further down the ScrollView then the height of the image. Otherwise the image would become a negative height and although that’s not an issue as a negative height would still render the view invisible, it would be a big problem if we were translating (the image would flip upside down!). I think its good practice to specify
The default extrapolation is extend
which would mean the values are ‘guessed’ based on the most recent step your interpolation function produced.
We are really done now! Thanks for reading.