Implementing Collapsing Toolbar Using React Native

Habib Ridho
3 min readFeb 11, 2018

--

Collapsing toolbar is really cool, both visually and in user experience point of view. We can start with initial view with huge header, giving the user a bit of context, then collapsing it when the user start scrolling the content, giving her the screen real estate to enjoy the page’s content. In this article, I will explore how we can achieve that using React Native.

View Structure

Let’s start with a very simple view structure. We will have a header that we will collapse and a ScrollView that will contain our long content.

import React, { Component } from 'react';
...
import content from './content';
export default class App extends Component {
render(){
return(
<View style={styles.container}>
<View style={{height: 300}}/>
<ScrollView contentContainerStyle={styles.scrollContainer}>
<Text style={styles.title}>This is Title</Text>
<Text style={styles.content}>{content}</Text>
</ScrollView>
</View>
)
}
}
const styles = StyleSheet.create({
container: {
flex: 1
},
scrollContainer: {
padding: 16
},
title: {
fontSize: 24,
marginVertical: 16
}
})

Our first attempt doesn’t look so good as the header stays where it is. So, we will make the height changes as we scroll the content up using RN’s Animated library.

Animated and onScroll Combination

The Animated library provides several useful APIs to create an animation, but we will focus on the event and interpolate method. The event method can map scrolling events into animated values. We will then calculate the header’s height based on these values using the interpolate method.

...
const HEADER_EXPANDED_HEIGHT = 300
const HEADER_COLLAPSED_HEIGHT = 60
export default class App extends Component {
constructor() {
super()
this.state = {
scrollY: new Animated.Value(0)
}
}
render() {
const headerHeight = this.state.scrollY.interpolate({
inputRange: [0, HEADER_EXPANDED_HEIGHT-HEADER_COLLAPSED_HEIGHT],
outputRange: [HEADER_EXPANDED_HEIGHT, HEADER_COLLAPSED_HEIGHT],
extrapolate: 'clamp'
})
return(
<View style={styles.container}>
<Animated.View style={{height: headerHeight}}/>
<ScrollView
contentContainerStyle={styles.scrollContainer}
onScroll={Animated.event(
[{ nativeEvent: {
contentOffset: {
y: this.state.scrollY
}
}
}])}
scrollEventThrottle={16}>
...
</ScrollView>
</View>
)
}
}

We subscribe Animated.event to the onScroll event of our ScrollView component. This will assign a new value to the scrollY state every time the scrolling event fired, thus invoking a re-render.

The scrollY value will be mapped into headerHeight value using the interpolate method. In the above example, we are only interested in the scrolling event in the area between the expanded and the collapsed header height. Thus, scrolling beyond those area would not affect our header.

Now, we have a collapsing header. However, it’s a little bit off since the content scroll right away without waiting the header collapse completely. In order to fix it, we will set the header’s position as absolute and set a top padding of the ScrollView equal to height of the expanded header.

const { width: SCREEN_WIDTH } = Dimensions.get('screen')
...
return(
<View style={styles.container}>
<Animated.View style={{height: headerHeight, width: SCREEN_WIDTH, position: 'absolute', top: 0, left: 0}}/>
<ScrollView
contentContainerStyle={{padding: 16, paddingTop: HEADER_EXPANDED_HEIGHT}}
...>
...
</ScrollView>
</View>)
...

Using the same basic principle of animating the header, we could animate other things inside the header.

...
render() {
...
const headerTitleOpacity = this.state.scrollY.interpolate({
inputRange: [0, HEADER_EXPANDED_HEIGHT-HEADER_COLLAPSED_HEIGHT],
outputRange: [0, 1],
extrapolate: 'clamp'
});
const heroTitleOpacity = this.state.scrollY.interpolate({
inputRange: [0, HEADER_EXPANDED_HEIGHT-HEADER_COLLAPSED_HEIGHT],
outputRange: [1, 0],
extrapolate: 'clamp'
});
return (
<View style={styles.container}>
<Animated.View style={[styles.header, { height: headerHeight }]}>
<Animated.Text style={{textAlign: 'center', marginTop: 28, opacity: headerTitleOpacity}}>{headerTitle}</Animated.Text>
<Animated.Text style={{position: 'absolute', bottom: 16, left: 16, opacity: heroTitleOpacity}}>{headerTitle}</Animated.Text>
</Animated.View>
...
</View>
)
}

You can access the full code here.

--

--