Skip to content

Instantly share code, notes, and snippets.

@rajivnarayana
Created May 23, 2020 11:21
Show Gist options
  • Save rajivnarayana/46948af25e235ba30aa02b8a9a5ac87a to your computer and use it in GitHub Desktop.
Save rajivnarayana/46948af25e235ba30aa02b8a9a5ac87a to your computer and use it in GitHub Desktop.
A clamped header scrolling with dynamic header height for React Native.
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