react-native : change view corresponding to scroll position
I guess there's no direct way in RN if you want to animated a change of view, however, in your case I can think of a little trick using the mix of opacity
, position: absolute
and interpolate()
, here is a minimal example which you can directly copy and paste to test it:
import React, { Component } from 'react';
import { StyleSheet, Animated, View, ScrollView } from 'react-native';
class AnimationExample extends Component {
constructor(props) {
super(props)
this.state = {
showBlueView: false,
animatedOpacityValue: new Animated.Value(0),
}
}
handleScroll = (event) => {
const { animatedOpacityValue, showBlueView } = this.state;
const scrollPosition = event.nativeEvent.contentOffset.y;
if (scrollPosition > 100 && !showBlueView) {
Animated.timing(animatedOpacityValue, {
toValue: 1,
}).start(() => this.setState({ showBlueView: true }))
}
if (scrollPosition < 100 && showBlueView) {
Animated.timing(animatedOpacityValue, {
toValue: 0,
}).start(() => this.setState({ showBlueView: false }))
}
}
render() {
const { animatedOpacityValue } = this.state;
return (
<ScrollView
style={styles.scrollView}
onScroll={this.handleScroll}
scrollEventThrottle={16}
>
<View style={styles.green} />
<View style={styles.animatedViewsPositioner}>
<Animated.View
style={{
...styles.red,
opacity: animatedOpacityValue.interpolate({
inputRange: [0, 1],
outputRange: [1, 0],
}),
}}
/>
<Animated.View
style={{
...styles.blue,
opacity: animatedOpacityValue.interpolate({
inputRange: [0, 1],
outputRange: [0, 1],
}),
}}
/>
</View>
</ScrollView>
)
}
}
const styles = StyleSheet.create({
scrollView: {
flex: 1,
},
green: {
height: 600,
width: '100%',
backgroundColor: 'green',
},
red: {
height: 300,
width: '100%',
backgroundColor: 'red',
},
blue: {
position: 'absolute',
height: 300,
width: '100%',
backgroundColor: 'blue',
},
animatedViewsPositioner: {
position: 'relative',
},
})
In the example above, I first access the scroll position by applying a handleScroll
function to the scrollView
. Make sure you have scrollEventThrottle
set to 16 to ensure the function is triggered every second, but beware of possible performance issue caused by that (if you care, you might take a look at this for more info).
To achieve a view change triggered when user scroll to certain position (which is actually not, but it looks like that), I use a view
to wrap both red and blue views, the red one is default with opacity: 1
, while the blue one with default opacity: 0
, sitting on top of the red one.
I hide the red view and show the blue one by animating their opacity
using interpolate()
. With the help of that, both opacity values are controlled by one animatedValue animatedOpacityValue
put in the state. I added a state showBlueView
to optimise the performance by avoid constantly setting states triggered by onScroll.
Here's an update to add touchableOpacities
on both views, simply achieve by hiding the blue view when it's unused.
First, add a log function:
log = (stringToPrint) => () => {
console.log(stringToPrint)
}
Next, change the scrollView
like this by adding two touchableOpacity
<ScrollView
style={styles.scrollView}
onScroll={this.handleScroll}
scrollEventThrottle={16}
>
<View style={styles.green} />
<View style={styles.animatedViewsPositioner}>
<Animated.View
style={{
...styles.red,
opacity: animatedOpacityValue.interpolate({
inputRange: [0, 1],
outputRange: [1, 0],
}),
}}
>
<TouchableOpacity
style={{ backgroundColor: 'black', width: 80, height: 30 }}
onPress={this.log('click on red')}
/>
</Animated.View>
{showBlueView && (
<Animated.View
style={{
...styles.blue,
opacity: animatedOpacityValue.interpolate({
inputRange: [0, 1],
outputRange: [0, 1],
}),
}}
>
<TouchableOpacity
style={{ backgroundColor: 'black', width: 80, height: 30 }}
onPress={this.log('click on blue')}
/>
</Animated.View>
)}
</View>
</ScrollView>
Note that I added showBlueView &&
to hide the blue view when its opacity is 0, so that it will not block any click event applied to the red view (even though the blue view is hidden, it is actually on top of the red view with opacity: 0
).
@Andus 's ans with Animated.event
The idea is to get the latest scrollY then wrap it to view's opacity. The example input range of blue target is 0-50 and got opacity 1 to 0. That means it would fade out when scrolling down the first 50 px.
The red one is the reverse one with input range 0-200 and out to opacity 0 to 1.(fade in)
import React, { Component } from 'react';
import { StyleSheet, Animated, View, ScrollView, SafeAreaView } from 'react-native';
export default class AnimationExample extends Component {
constructor(props) {
super(props)
this.state = {
scrollY: new Animated.Value(0)
}
}
render() {
const {scrollY} = this.state;
return (
<SafeAreaView style={{flex: 1}}>
<ScrollView
style={styles.scrollView}
onScroll={Animated.event(
[{nativeEvent: {contentOffset: {y: this.state.scrollY}}}]
)}
scrollEventThrottle={16}
>
<View style={styles.animatedViewsPositioner}>
<Animated.View
style={[styles.box, styles.target, {
opacity: scrollY.interpolate({
inputRange: [0, 50],
outputRange: [1, 0],
}),
}]}
/>
<Animated.View
style={[styles.box, styles.origin, {
opacity: scrollY.interpolate({
inputRange: [0, 200],
outputRange: [0, 1],
}),
}]}
/>
</View>
</ScrollView>
</SafeAreaView>
)
}
}
const styles = StyleSheet.create({
scrollView: {
flex: 1,
},
box: {
height: 1000,
width: '100%',
position: 'absolute'
},
origin: {
backgroundColor: 'red',
zIndex: 1
},
target: {
backgroundColor: 'blue',
zIndex: 2
},
animatedViewsPositioner: {
position: 'relative',
backgroundColor: 'pink',
height: 10000
},
})