EDIT: I hate googling for answers and finding some question that never got solved from 10 years ago so I am answering my own question for those that might want to know. In my case, I simply disabled the bounces
prop for the scrollview. Since FlatList extends React's ScrollView, setting bounces
to false
in the animated FlatList component that I created stopped it from bouncing and solved my problem. Have a nice day.
hope you're having a great day. I am trying to animate my header dynamically but for some reason whenever I scroll beyond the beginning or the end of the scrollview, the bounce effect messes with the Animation.(as shown in the gif below)
GIF
Same GIF but higher resolution
As you can see, when I scroll to the top and enable the bounce animation, the header thinks that i am scrolling down as the bounce returns the first element in the list back to the top. How do I fix this? I saw on the web somewhere that adding an interpolator to the animated value would help, though I don't really understand.
Below is my code. Thank You
const AnimatedFlatList = Animated.createAnimatedComponent(FlatList)
const tempArray = [
...(an array of my data)
]
export default class TempScreen extends React.Component {
static navigationOptions = {
header: null
}
constructor(props) {
super(props)
this.state = {
animatedHeaderValue: new Animated.Value(0),
}
}
render() {
const animatedHeaderHeight = Animated.diffClamp(this.state.animatedHeaderValue, 0, 60)
.interpolate({
inputRange: [0, 70],
outputRange: [70, 0],
})
return ( <
View >
<
Animated.View style = {
{
backgroundColor: 'white',
borderBottomColor: '#DEDEDE',
borderBottomWidth: 1,
padding: 15,
width: Dimensions.get('window').width,
height: animatedHeaderHeight,
}
} >
<
/Animated.View> <
AnimatedFlatList scrollEventThrottle = {
16
}
onScroll = {
Animated.event(
[{
nativeEvent: {
contentOffset: {
y: this.state.animatedHeaderValue
}
}
}]
)
}
data = {
tempArray
}
renderItem = {
({
item
}) =>
<
View style = {
{
flex: 1
}
} >
<
Text style = {
{
fontWeight: 'bold',
fontSize: 30
}
} > {
item.name
} < /Text> <
Text > {
item.year
} < /Text> <
/View>
}
/>
<
/View>
)
}
}
If you want to solve the "bounce" problem only, the problem is that iOS gives to diffClamp negative scrollY values. You need to filter these and ensure scrollY remains >= 0 to avoid diffClamp being affected by overscroll.
const clampedScrollY = scrollY.interpolate({
inputRange: [0, 1],
outputRange: [0, 1],
extrapolateLeft: 'clamp',
});
Another nice trick is to use a "cliff" technique, so that the header only disappear after a minimum scrollY position.
Here is code from my app:
const minScroll = 100;
const clampedScrollY = scrollY.interpolate({
inputRange: [minScroll, minScroll + 1],
outputRange: [0, 1],
extrapolateLeft: 'clamp',
});
const minusScrollY = Animated.multiply(clampedScrollY, -1);
const translateY = Animated.diffClamp(
minusScrollY,
-AnimatedHeaderHeight,
0,
);
const opacity = translateY.interpolate({
inputRange: [-AnimatedHeaderHeight, 0],
outputRange: [0.4, 1],
extrapolate: 'clamp',
});
clampedScrollY
will be:
- 0 when scrollY=0
- 0 when scrollY=50
- 0 when scrollY=100
- 30 when scrollY=130
- 170 when scrollY=270
You get the idea. So diffClamp
will only be > 0 if scrollY > 100, and increment 1 by 1 after that threshold.
I had the same problem like two hour ago...
You can set Scrollview
property bounces=false
but if you want a RefreshControl
for refreshing the ScrollView
content (like in my case), the bounce property has to stay active.
I fixed this following this cool article: https://medium.com/appandflow/react-native-collapsible-navbar-e51a049b560a.
I'm not an expert of the Animated library, so I post my code:
constructor(props) {
const scrollAnim = new Animated.Value(0);
const offsetAnim = new Animated.Value(0);
this.state = {
scrollAnim,
offsetAnim,
AnimatedViewHeight: 1,
clampedScroll: Animated.diffClamp(
Animated.add(
scrollAnim.interpolate({
inputRange: [0, 1],
outputRange: [0, 1],
extrapolateLeft: 'clamp',
}),
offsetAnim
),0, 1
)
}
}
render() {
const minScroll = this.state.AnimatedViewHeight;
const navbarTranslate = this.state.clampedScroll.interpolate({
inputRange: [0, minScroll],
outputRange: [0, -minScroll],
extrapolate: 'clamp',
});
return (
<View style={{
flex: 1
}}>
<Animated.View
onLayout={(event) => {
var { height } = event.nativeEvent.layout;
this.setState({
AnimatedViewHeight: height,
clampedScroll: Animated.diffClamp(
Animated.add(
this.state.scrollAnim.interpolate({
inputRange: [0, 1],
outputRange: [0, 1],
extrapolateLeft: 'clamp',
}),
this.state.offsetAnim
), 0, height)
})
}}
style={[{ transform: [{ translateY: navbarTranslate }] }]}>
<View><Text>THIS IS YOUR HEADER</Text></View>
</Animated.View>
<AnimatedFlatList
// iOS offset for RefreshControl
contentInset={{
top: this.state.AnimatedViewHeight,
}}
contentOffset={{
y: -this.state.AnimatedViewHeight,
}}
scrollEventThrottle={1}
onScroll={
Animated.event(
[{ nativeEvent: { contentOffset: { y: this.state.scrollAnim } } }],
{ useNativeDriver: true },
)}
data={this.state.data}
keyExtractor={(item, idx) => idx}
ListFooterComponent={this.renderFooter}
renderItem={this.renderItem}
onEndReached={this.handleLoadMore}
refreshControl={
<RefreshControl
refreshing={this.state.refreshing}
onRefresh={this.onRefresh}
// Android offset for RefreshControl
progressViewOffset={this.state.AnimatedViewHeight}
/>
}
onEndReachedThreshold={0.5} />
</View>
)
}
this.state.AnimatedViewHeight
is the height of the header, retrieved by calling onLayout
function. Inside of this function I also set a new clampedScroll because I have a new height (In my case, the header doesn't have a fixed size).
After that, in render()
, define a variable (navbarTranslate
) for control the headerSize based on the scroll position of your Animated Scrollview.