Created
May 23, 2020 11:21
-
-
Save rajivnarayana/46948af25e235ba30aa02b8a9a5ac87a to your computer and use it in GitHub Desktop.
A clamped header scrolling with dynamic header height for React Native.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, {Component} from 'react'; | |
import { | |
Text, | |
View, | |
Animated, | |
StyleSheet, | |
SafeAreaView, | |
FlatList, | |
LayoutChangeEvent, | |
} from 'react-native'; | |
const DATA = [ | |
{ | |
id: 'bd7acbea-c1b1-46c2-aed5-3ad53abb28ba', | |
title: ' ', | |
header: true, | |
}, | |
{ | |
id: '3ac68afc-c605-48d3-a4f8-fbd91aa97f60', | |
title: 'Second Item', | |
header: false, | |
}, | |
{ | |
id: '58694a0f-3da1-471f-bd96-145571e29d72', | |
title: 'Third Item', | |
header: false, | |
}, | |
{ | |
id: '3ac68afc-c605-48d3-a4f8-fbd92aa97f61', | |
title: 'Second Item', | |
header: false, | |
}, | |
{ | |
id: '58694a0f-3da1-471f-bd96-145571e29d73', | |
title: 'Third Item', | |
header: false, | |
}, | |
{ | |
id: '3ac68afc-c605-48d3-a4f8-fbd93aa97f62', | |
title: 'Second Item', | |
header: false, | |
}, | |
{ | |
id: '58694a0f-3da1-471f-bd96-145571e29d74', | |
title: 'Third Item', | |
header: false, | |
}, | |
{ | |
id: '3ac68afc-c605-48d3-a4f8-fbd94aa97f63', | |
title: 'Second Item', | |
header: false, | |
}, | |
{ | |
id: '58694a0f-3da1-471f-bd96-145571e29d75', | |
title: 'Third Item', | |
header: false, | |
}, | |
{ | |
id: '3ac68afc-c605-48d3-a4f8-fbd95aa97f64', | |
title: 'Second Item', | |
header: false, | |
}, | |
{ | |
id: '58694a0f-3da1-471f-bd96-145571e29d78', | |
title: 'Third Item', | |
header: false, | |
}, | |
{ | |
id: '3ac68afc-c605-48d3-a4f8-fbd96aa97f65', | |
title: 'Second Item', | |
header: false, | |
}, | |
{ | |
id: '58694a0f-3da1-471f-bd96-145571e29d76', | |
title: 'Third Item', | |
header: false, | |
}, | |
]; | |
function Item({ | |
title, | |
header = false, | |
headerItemHeight = MAX_BACKGROUND_ITEM_HEIGHT, | |
}: { | |
title: any; | |
header: boolean; | |
headerItemHeight: number; | |
}) { | |
return ( | |
<View | |
style={[ | |
styles.item, | |
header ? styles.blank : {}, | |
header ? {height: headerItemHeight} : {}, | |
]}> | |
<Text style={styles.title}>{title}</Text> | |
</View> | |
); | |
} | |
class BackgroundItem extends Component<BackgroundItemProps> { | |
headerOffsetY: any; | |
constructor(props: Readonly<BackgroundItemProps>) { | |
super(props); | |
this.headerOffsetY = this.props.y.interpolate({ | |
inputRange: [0, 100], | |
outputRange: [0, -100], | |
extrapolateRight: 'extend', | |
extrapolateLeft: 'extend', | |
}); | |
this.handleOnLayout = this.handleOnLayout.bind(this); | |
} | |
handleOnLayout({nativeEvent}: LayoutChangeEvent) { | |
this.props.onHeightCalculated(nativeEvent.layout.height); | |
} | |
render() { | |
return ( | |
<Animated.View | |
style={[ | |
styles.backgroundItem, | |
{ | |
height: this.props.height, | |
transform: [{translateY: this.headerOffsetY}], | |
}, | |
]}> | |
<View onLayout={this.handleOnLayout}> | |
<Text>To prevent the spread of COVID-19:</Text> | |
<Text>Clean your hands often. </Text> | |
<Text>Use soap and water, or an alcohol-based hand rub. </Text> | |
<Text> | |
Maintain a safe distance from anyone who is coughing or sneezing. | |
</Text> | |
<Text>Don’t touch your eyes, nose or mouth.</Text> | |
</View> | |
</Animated.View> | |
); | |
} | |
} | |
declare type HeaderProps = { | |
y: Animated.Value; | |
height: number; | |
}; | |
declare type BackgroundItemProps = HeaderProps & { | |
onHeightCalculated: (height: number) => void; | |
}; | |
const TABBAR_HEIGHT = 40; | |
class Header extends Component<HeaderProps, {headerOffsetY: any}> { | |
constructor(props: Readonly<HeaderProps>) { | |
super(props); | |
const scrollDistance = this.props.height - TABBAR_HEIGHT; | |
this.state = { | |
headerOffsetY: this.props.y.interpolate({ | |
inputRange: [0, scrollDistance], | |
outputRange: [0, -scrollDistance], | |
extrapolateRight: 'clamp', | |
extrapolateLeft: 'extend', | |
}), | |
}; | |
} | |
componentDidUpdate(props: any) { | |
if (this.props.height != props.height) { | |
const scrollDistance = this.props.height - TABBAR_HEIGHT; | |
this.setState({ | |
headerOffsetY: this.props.y.interpolate({ | |
inputRange: [0, scrollDistance], | |
outputRange: [0, -scrollDistance], | |
extrapolateRight: 'clamp', | |
extrapolateLeft: 'extend', | |
}), | |
}); | |
} | |
} | |
render() { | |
return ( | |
<Animated.View | |
style={[ | |
styles.header, | |
{ | |
height: this.props.height, | |
transform: [{translateY: this.state.headerOffsetY}], | |
}, | |
]}> | |
<Text style={styles.tabbar}>Tab Bar</Text> | |
</Animated.View> | |
); | |
} | |
} | |
const MAX_BACKGROUND_ITEM_HEIGHT = 100; | |
export default class ClampedHeaderScrolling extends Component<{}> { | |
y: Animated.Value; | |
state = { | |
backgroundItemHeight: MAX_BACKGROUND_ITEM_HEIGHT, | |
}; | |
handleScrollEvent: (...args: any[]) => void; | |
constructor(props: Readonly<{}>) { | |
super(props); | |
this.y = new Animated.Value(0); | |
this.handleBackgroundItemHeight = this.handleBackgroundItemHeight.bind( | |
this, | |
); | |
this.handleScrollEvent = Animated.event([ | |
{ | |
nativeEvent: {contentOffset: {y: this.y}}, | |
}, | |
]); | |
} | |
handleBackgroundItemHeight(height: number) { | |
if (this.state.backgroundItemHeight != height + TABBAR_HEIGHT) { | |
this.setState({backgroundItemHeight: height + TABBAR_HEIGHT}); | |
} | |
} | |
render() { | |
return ( | |
<> | |
<SafeAreaView style={styles.container}> | |
<View style={styles.container}> | |
<FlatList | |
style={{marginTop: 0}} | |
onScroll={this.handleScrollEvent} | |
scrollEventThrottle={16} | |
data={DATA} | |
renderItem={({item}) => ( | |
<Item | |
title={item.title} | |
header={item.header} | |
headerItemHeight={this.state.backgroundItemHeight} | |
/> | |
)} | |
keyExtractor={item => item.id} | |
/> | |
<Header y={this.y} height={this.state.backgroundItemHeight} /> | |
<BackgroundItem | |
y={this.y} | |
height={this.state.backgroundItemHeight} | |
onHeightCalculated={this.handleBackgroundItemHeight} | |
/> | |
</View> | |
</SafeAreaView> | |
</> | |
); | |
} | |
} | |
const styles = StyleSheet.create({ | |
container: { | |
flex: 1, | |
}, | |
item: { | |
backgroundColor: '#f9c2ff', | |
padding: 20, | |
marginHorizontal: 16, | |
}, | |
title: { | |
fontSize: 32, | |
}, | |
tabbar: { | |
backgroundColor: 'red', | |
height: TABBAR_HEIGHT, | |
}, | |
header: { | |
position: 'absolute', | |
top: 0, | |
right: 0, | |
left: 0, | |
overflow: 'hidden', | |
justifyContent: 'flex-end', | |
}, | |
blank: { | |
backgroundColor: 'transparent', | |
}, | |
backgroundItem: { | |
position: 'absolute', | |
top: 0, | |
right: 0, | |
left: 0, | |
overflow: 'hidden', | |
justifyContent: 'flex-start', | |
}, | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment