Parallax and Expanding ScrollView Header with React Native and Animated.Value Interpolation

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.

Leave a Reply

Your email address will not be published. Required fields are marked *