Skip to content

Instantly share code, notes, and snippets.

@levibuzolic
Created May 12, 2021 17:35
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save levibuzolic/a3c8d033e329cb069121f79646369d16 to your computer and use it in GitHub Desktop.
Save levibuzolic/a3c8d033e329cb069121f79646369d16 to your computer and use it in GitHub Desktop.
Experiements with gesture handlers and scroll views
// @flow
import * as React from 'react';
import {Text, View, StyleSheet, ScrollView as RNScrollView, TouchableOpacity, Platform, PixelRatio} from 'react-native';
import {
PanGestureHandler,
NativeViewGestureHandler,
TapGestureHandler,
State,
ScrollView as RNGHScrollView,
} from 'react-native-gesture-handler';
import Animated, {event, useValue, interpolate, Extrapolate} from 'react-native-reanimated';
import {DebugWrapper, DebugValue, DebugState} from './debug'; // eslint-disable-line no-unused-vars
const {useRef, useMemo, useState, useEffect} = React;
const statusBarHeight = Platform.OS === 'ios' ? 20 : 24;
// const AnimatedScrollView = Animated.createAnimatedComponent(RNScrollView);
const iteration = 0;
export default function Scrollable() {
const cancelRef = useRef<$FlowFixMe>();
const panRef = useRef<$FlowFixMe>();
const nativeRef = useRef<$FlowFixMe>();
const realScrollRef = useRef<$FlowFixMe>();
const translateY = useValue(0);
const translateYOffset = useValue(0);
const scrollY = useValue(0);
const panGestureState = useValue(State.UNDETERMINED);
const panGestureY = useValue(0);
const cancelGestureState = useValue(State.UNDETERMINED);
const cancelGestureY = useValue(0);
const handleScrollEvent = useMemo(() => event([{nativeEvent: {contentOffset: {y: scrollY}}}]), [scrollY]);
const handlePanGestureEvent = useMemo(
() => event([{nativeEvent: {state: panGestureState, translationY: panGestureY}}]),
[panGestureState, panGestureY]
);
const handleCancelGestureEvent = useMemo(
() => event([{nativeEvent: {state: cancelGestureState, translationY: cancelGestureY}}]),
[cancelGestureState, cancelGestureY]
);
return (
<View style={styles.root}>
<Animated.View style={[styles.flex]}>
<WrappedTapGestureHandler
onGestureEvent={handleCancelGestureEvent}
onHandlerStateChange={handleCancelGestureEvent}
ref={cancelRef}
maxDurationMs={1000000}
shouldCancelWhenOutside={false}
maxDeltaY={80}
>
<Animated.View
style={[
styles.flex,
{
transform: [
{
translateY: interpolate(panGestureY, {
inputRange: [0, 1],
outputRange: [0, 1],
extrapolateLeft: 'clamp',
}),
},
],
},
]}
>
<PanGestureHandler
ref={panRef}
shouldCancelWhenOutside={false}
onGestureEvent={handlePanGestureEvent}
onHandlerStateChange={handleCancelGestureEvent}
simultaneousHandlers={[nativeRef, cancelRef]}
>
<Animated.View style={[styles.flex]}>
<NativeViewGestureHandler ref={nativeRef} waitFor={cancelRef} simultaneousHandlers={panRef}>
<Animated.ScrollView
onScroll={handleScrollEvent}
ref={realScrollRef}
scrollEventThrottle={1}
style={styles.scrollView}
bounces={false}
overScrollMode="never"
>
<View style={styles.fakeHeader}>
<Text style={{color: 'black', fontSize: 20}}>{iteration}</Text>
</View>
<Content />
</Animated.ScrollView>
</NativeViewGestureHandler>
</Animated.View>
</PanGestureHandler>
</Animated.View>
</WrappedTapGestureHandler>
</Animated.View>
<DebugWrapper>
<DebugValue name="scrollY" value={scrollY} />
<DebugValue name="translateY" value={translateY} />
<DebugValue name="translateYOffset" value={translateYOffset} />
<DebugState name="panGestureState" value={panGestureState} />
<DebugValue name="panGestureY" value={panGestureY} />
<DebugState name="cancelGestureState" value={cancelGestureState} />
<DebugValue name="cancelGestureY" value={cancelGestureY} />
</DebugWrapper>
<TouchableOpacity
style={styles.buttonWrapper}
onPress={() => {
// translateY.setValue(0);
// translateYOffset.setValue(0);
// cancelRef.current?.setNativeProps({maxDetlaY: 100});
// (realScrollRef.current.getNode() ?? realScrollRef.current).scrollTo({
// x: 0,
// y: 0,
// animated: true,
// });
}}
>
<View style={styles.button} />
</TouchableOpacity>
<Ruler />
</View>
);
}
const WrappedTapGestureHandler = React.forwardRef(function WrappedTapGestureHandler(
props: React.ElementConfig<typeof TapGestureHandler>,
ref: $FlowFixMe
): React.Node {
return <TapGestureHandler {...props} ref={ref} />;
if (Platform.OS === 'android') return <TapGestureHandler {...props} ref={ref} />;
return (
<TapGestureHandler {...props} ref={ref}>
<View style={StyleSheet.absoluteFillObject} pointerEvents="box-none">
{props.children}
</View>
</TapGestureHandler>
);
});
function Content() {
return new Array(30).fill(null).map((_, index) => (
<React.Fragment key={index}>
<View style={styles.row}>
<Text style={{color: 'black', fontSize: 20, padding: 10}}>{index}</Text>
</View>
<View style={styles.row2} />
</React.Fragment>
));
}
function Ruler() {
return (
<View
// eslint-disable-next-line react-native/no-inline-styles
style={{
top: statusBarHeight,
right: 0,
bottom: 0,
width: 40,
position: 'absolute',
}}
pointerEvents="none"
accessible={false}
accessibilityLabel=""
>
{new Array(12).fill(0).map((_, index) => (
<Text
key={index}
// eslint-disable-next-line react-native/no-inline-styles
style={{
backgroundColor: 'rgba(255, 0, 0, 0.2)',
fontSize: 8,
textAlign: 'right',
position: 'absolute',
top: index * 50,
right: 0,
}}
>
{index * 50}
</Text>
))}
</View>
);
}
const clamp = (value, minValue, maxValue) => min(max(value, minValue), maxValue);
const styles = StyleSheet.create({
root: {
flex: 1,
justifyContent: 'center',
backgroundColor: '#444',
paddingTop: statusBarHeight,
overflow: 'hidden',
},
flex: {
flex: 1,
overflow: 'hidden',
},
scrollView: {
flex: 1,
borderWidth: 2,
borderColor: 'red',
overflow: 'hidden',
},
fakeHeader: {
height: 200,
overflow: 'hidden',
},
row: {
backgroundColor: '#fff',
height: PixelRatio.roundToNearestPixel(50),
},
row2: {
backgroundColor: '#eee',
height: PixelRatio.roundToNearestPixel(50),
},
buttonWrapper: {
position: 'absolute',
bottom: 20,
right: 20,
},
button: {
width: PixelRatio.roundToNearestPixel(50),
height: PixelRatio.roundToNearestPixel(50),
borderRadius: 25,
backgroundColor: 'rgba(255, 0, 0, 0.3)',
},
});
// @flow
import * as React from 'react';
import {View, StyleSheet} from 'react-native';
import {State} from 'react-native-gesture-handler';
import Animated, {concat, cond, eq, block, round} from 'react-native-reanimated';
import {ReText} from 'react-native-redash';
export function DebugValue(props: {|name: string, value: Animated.Node<number>, round?: boolean|}) {
return (
<ReText
style={styles.text}
text={concat(`${props.name}: `, props.round === false ? props.value : round(props.value))}
/>
);
}
export function DebugState({name, value}: {|name: string, value: Animated.Node<number>|}) {
return <ReText style={styles.text} text={concat(`${name}: `, getState(value))} />;
}
export function DebugWrapper(props: React.ElementConfig<typeof View>) {
return (
<View
{...props}
style={StyleSheet.compose(styles.wrapper, props.style)}
pointerEvents="none"
accessible={false}
accessibilityLabel=""
/>
);
}
function getState(state: Animated.Node<number>): Animated.Node<string> {
return block([
cond(
eq(state, State.UNDETERMINED),
concat('', 'UNDETERMINED'),
cond(
eq(state, State.BEGAN),
concat('', 'BEGAN'),
cond(
eq(state, State.ACTIVE),
concat('', 'ACTIVE'),
cond(
eq(state, State.END),
concat('', 'END'),
cond(eq(state, State.CANCELLED), concat('', 'CANCELLED'), concat('', 'FAILED'))
)
)
)
),
]);
}
const styles = StyleSheet.create({
wrapper: {
position: 'absolute',
bottom: 20,
left: 20,
backgroundColor: 'rgba(0, 0, 0, 0.8)',
padding: 5,
},
text: {
color: '#fff',
fontFamily: 'Menlo-Regular',
fontSize: 8,
padding: 0,
height: 10,
lineHeight: 10,
},
});
// @flow
import * as React from 'react';
import {Text, View, StyleSheet, ScrollView as RNScrollView, TouchableOpacity, Platform, PixelRatio} from 'react-native';
import {
PanGestureHandler,
NativeViewGestureHandler,
TapGestureHandler,
State,
ScrollView as RNGHScrollView,
} from 'react-native-gesture-handler';
import Animated, {event, useValue, interpolate, Extrapolate} from 'react-native-reanimated';
import {DebugWrapper, DebugValue, DebugState} from './debug'; // eslint-disable-line no-unused-vars
const {useRef, useMemo, useState, useEffect} = React;
const statusBarHeight = Platform.OS === 'ios' ? 20 : 24;
// const AnimatedScrollView = Animated.createAnimatedComponent(RNScrollView);
const iteration = 0;
export default class Scrollable extends React.Component<{}> {
cancelRef = React.createRef<$FlowFixMe>();
panRef = React.createRef<$FlowFixMe>();
nativeRef = React.createRef<$FlowFixMe>();
realScrollRef = React.createRef<$FlowFixMe>();
translateY: Animated.Value<number>;
translateYOffset: Animated.Value<number>;
scrollY: Animated.Value<number>;
panGestureState: Animated.Value<number>;
panGestureY: Animated.Value<number>;
cancelGestureState: Animated.Value<number>;
cancelGestureY: Animated.Value<number>;
handleScrollEvent: $ExtractReturn<typeof event>;
handlePanGestureEvent: $ExtractReturn<typeof event>;
handleCancelGestureEvent: $ExtractReturn<typeof event>;
constructor(props: {}) {
super(props);
this.translateY = new Animated.Value(0);
this.translateYOffset = new Animated.Value(0);
this.scrollY = new Animated.Value(0);
this.panGestureState = new Animated.Value(State.UNDETERMINED);
this.panGestureY = new Animated.Value(0);
this.cancelGestureState = new Animated.Value(State.UNDETERMINED);
this.cancelGestureY = new Animated.Value(0);
this.handleScrollEvent = event([{nativeEvent: {contentOffset: {y: this.scrollY}}}]);
this.handlePanGestureEvent = event([{nativeEvent: {state: this.panGestureState, translationY: this.panGestureY}}]);
this.handleCancelGestureEvent = event([
{nativeEvent: {state: this.cancelGestureState, translationY: this.cancelGestureY}},
]);
}
render() {
return (
<View style={styles.root}>
<Animated.View style={[styles.flex]}>
<WrappedTapGestureHandler
onGestureEvent={this.handleCancelGestureEvent}
onHandlerStateChange={this.handleCancelGestureEvent}
ref={this.cancelRef}
maxDurationMs={1000000}
shouldCancelWhenOutside={false}
maxDeltaY={80}
>
<Animated.View
style={[
styles.flex,
{
transform: [
{
translateY: interpolate(this.panGestureY, {
inputRange: [0, 1],
outputRange: [0, 1],
extrapolateLeft: 'clamp',
}),
},
],
},
]}
>
<PanGestureHandler
ref={this.panRef}
shouldCancelWhenOutside={false}
onGestureEvent={this.handlePanGestureEvent}
onHandlerStateChange={this.handleCancelGestureEvent}
simultaneousHandlers={[this.nativeRef, this.cancelRef]}
>
<Animated.View style={[styles.flex]}>
<NativeViewGestureHandler
ref={this.nativeRef}
waitFor={this.cancelRef}
simultaneousHandlers={this.panRef}
>
<Animated.ScrollView
onScroll={this.handleScrollEvent}
ref={this.realScrollRef}
scrollEventThrottle={1}
style={styles.scrollView}
bounces={false}
overScrollMode="never"
>
<View style={styles.fakeHeader}>
<Text style={{color: 'black', fontSize: 20}}>{iteration}</Text>
</View>
<Content />
</Animated.ScrollView>
</NativeViewGestureHandler>
</Animated.View>
</PanGestureHandler>
</Animated.View>
</WrappedTapGestureHandler>
</Animated.View>
<DebugWrapper>
<DebugValue name="scrollY" value={this.scrollY} />
<DebugValue name="translateY" value={this.translateY} />
<DebugValue name="translateYOffset" value={this.translateYOffset} />
<DebugState name="panGestureState" value={this.panGestureState} />
<DebugValue name="panGestureY" value={this.panGestureY} />
<DebugState name="cancelGestureState" value={this.cancelGestureState} />
<DebugValue name="cancelGestureY" value={this.cancelGestureY} />
</DebugWrapper>
<TouchableOpacity
style={styles.buttonWrapper}
onPress={() => {
// translateY.setValue(0);
// translateYOffset.setValue(0);
// cancelRef.current?.setNativeProps({maxDetlaY: 100});
// (realScrollRef.current.getNode() ?? realScrollRef.current).scrollTo({
// x: 0,
// y: 0,
// animated: true,
// });
}}
>
<View style={styles.button} />
</TouchableOpacity>
<Ruler />
</View>
);
}
}
const WrappedTapGestureHandler = React.forwardRef(function WrappedTapGestureHandler(
{children, ...props}: React.ElementConfig<typeof TapGestureHandler>,
ref
): React.Node {
// if (Platform.OS === 'android')
return (
<TapGestureHandler {...props} ref={ref}>
{children}
</TapGestureHandler>
);
return (
<TapGestureHandler {...props} ref={ref}>
<View style={StyleSheet.absoluteFillObject} pointerEvents="box-none">
{children}
</View>
</TapGestureHandler>
);
});
function Content() {
return new Array(30).fill(null).map((_, index) => (
<React.Fragment key={index}>
<View style={styles.row}>
<Text style={{color: 'black', fontSize: 20, padding: 10}}>{index}</Text>
</View>
<View style={styles.row2} />
</React.Fragment>
));
}
function Ruler() {
return (
<View
// eslint-disable-next-line react-native/no-inline-styles
style={{
top: statusBarHeight,
right: 0,
bottom: 0,
width: 40,
position: 'absolute',
}}
pointerEvents="none"
accessible={false}
accessibilityLabel=""
>
{new Array(12).fill(0).map((_, index) => (
<Text
key={index}
// eslint-disable-next-line react-native/no-inline-styles
style={{
backgroundColor: 'rgba(255, 0, 0, 0.2)',
fontSize: 8,
textAlign: 'right',
position: 'absolute',
top: index * 50,
right: 0,
}}
>
{index * 50}
</Text>
))}
</View>
);
}
const clamp = (value, minValue, maxValue) => min(max(value, minValue), maxValue);
const styles = StyleSheet.create({
root: {
flex: 1,
justifyContent: 'center',
backgroundColor: '#444',
paddingTop: statusBarHeight,
overflow: 'hidden',
},
flex: {
flex: 1,
overflow: 'hidden',
},
scrollView: {
flex: 1,
borderWidth: 2,
borderColor: 'red',
overflow: 'hidden',
},
fakeHeader: {
height: 200,
overflow: 'hidden',
},
row: {
backgroundColor: '#fff',
height: PixelRatio.roundToNearestPixel(50),
},
row2: {
backgroundColor: '#eee',
height: PixelRatio.roundToNearestPixel(50),
},
buttonWrapper: {
position: 'absolute',
bottom: 20,
right: 20,
},
button: {
width: PixelRatio.roundToNearestPixel(50),
height: PixelRatio.roundToNearestPixel(50),
borderRadius: 25,
backgroundColor: 'rgba(255, 0, 0, 0.3)',
},
});
// @flow
import * as React from 'react';
import {Text, View, StyleSheet, ScrollView as RNScrollView, TouchableOpacity, Platform, PixelRatio} from 'react-native';
import {
PanGestureHandler,
NativeViewGestureHandler,
TapGestureHandler,
State,
ScrollView as RNGHScrollView,
} from 'react-native-gesture-handler';
import Animated, {event, useValue, interpolate, Extrapolate} from 'react-native-reanimated';
import {DebugWrapper, DebugValue, DebugState} from './debug'; // eslint-disable-line no-unused-vars
const {useRef, useMemo, useState, useEffect} = React;
const statusBarHeight = Platform.OS === 'ios' ? 20 : 24;
// const AnimatedScrollView = Animated.createAnimatedComponent(RNScrollView);
const iteration = 0;
export default class Scrollable extends React.Component<{}> {
cancelRef = React.createRef<$FlowFixMe>();
panRef = React.createRef<$FlowFixMe>();
nativeRef = React.createRef<$FlowFixMe>();
realScrollRef = React.createRef<$FlowFixMe>();
translateY: Animated.Value<number>;
translateYOffset: Animated.Value<number>;
scrollY: Animated.Value<number>;
panGestureState: Animated.Value<number>;
panGestureY: Animated.Value<number>;
cancelGestureState: Animated.Value<number>;
cancelGestureY: Animated.Value<number>;
handleScrollEvent: $ExtractReturn<typeof event>;
handlePanGestureEvent: $ExtractReturn<typeof event>;
handleCancelGestureEvent: $ExtractReturn<typeof event>;
constructor(props: {}) {
super(props);
this.translateY = new Animated.Value(0);
this.translateYOffset = new Animated.Value(0);
this.scrollY = new Animated.Value(0);
this.panGestureState = new Animated.Value(State.UNDETERMINED);
this.panGestureY = new Animated.Value(0);
this.cancelGestureState = new Animated.Value(State.UNDETERMINED);
this.cancelGestureY = new Animated.Value(0);
this.handleScrollEvent = event([{nativeEvent: {contentOffset: {y: this.scrollY}}}]);
this.handlePanGestureEvent = event([{nativeEvent: {state: this.panGestureState, translationY: this.panGestureY}}]);
this.handleCancelGestureEvent = event([
{nativeEvent: {state: this.cancelGestureState, translationY: this.cancelGestureY}},
]);
}
render() {
return (
<View style={styles.root}>
<Animated.View style={[styles.flex]}>
<WrappedTapGestureHandler
onGestureEvent={this.handleCancelGestureEvent}
onHandlerStateChange={this.handleCancelGestureEvent}
ref={this.cancelRef}
maxDurationMs={1000000}
shouldCancelWhenOutside={false}
maxDeltaY={80}
>
<Animated.View
style={[
styles.flex,
{
transform: [
{
translateY: interpolate(this.panGestureY, {
inputRange: [0, 1],
outputRange: [0, 1],
extrapolateLeft: 'clamp',
}),
},
],
},
]}
>
<PanGestureHandler
ref={this.panRef}
shouldCancelWhenOutside={false}
onGestureEvent={this.handlePanGestureEvent}
onHandlerStateChange={this.handleCancelGestureEvent}
simultaneousHandlers={[this.nativeRef, this.cancelRef]}
>
<Animated.View style={[styles.flex]}>
<NativeViewGestureHandler
ref={this.nativeRef}
waitFor={this.cancelRef}
simultaneousHandlers={this.panRef}
>
<Animated.ScrollView
onScroll={this.handleScrollEvent}
ref={this.realScrollRef}
scrollEventThrottle={1}
style={styles.scrollView}
bounces={false}
overScrollMode="never"
>
<View style={styles.fakeHeader}>
<Text style={{color: 'black', fontSize: 20}}>{iteration}</Text>
</View>
<Content />
</Animated.ScrollView>
</NativeViewGestureHandler>
</Animated.View>
</PanGestureHandler>
</Animated.View>
</WrappedTapGestureHandler>
</Animated.View>
<DebugWrapper>
<DebugValue name="scrollY" value={this.scrollY} />
<DebugValue name="translateY" value={this.translateY} />
<DebugValue name="translateYOffset" value={this.translateYOffset} />
<DebugState name="panGestureState" value={this.panGestureState} />
<DebugValue name="panGestureY" value={this.panGestureY} />
<DebugState name="cancelGestureState" value={this.cancelGestureState} />
<DebugValue name="cancelGestureY" value={this.cancelGestureY} />
</DebugWrapper>
<TouchableOpacity
style={styles.buttonWrapper}
onPress={() => {
// translateY.setValue(0);
// translateYOffset.setValue(0);
// cancelRef.current?.setNativeProps({maxDetlaY: 100});
// (realScrollRef.current.getNode() ?? realScrollRef.current).scrollTo({
// x: 0,
// y: 0,
// animated: true,
// });
}}
>
<View style={styles.button} />
</TouchableOpacity>
<Ruler />
</View>
);
}
}
const WrappedTapGestureHandler = React.forwardRef(function WrappedTapGestureHandler(
{children, ...props}: React.ElementConfig<typeof TapGestureHandler>,
ref
): React.Node {
// if (Platform.OS === 'android')
return (
<TapGestureHandler {...props} ref={ref}>
{children}
</TapGestureHandler>
);
return (
<TapGestureHandler {...props} ref={ref}>
<View style={StyleSheet.absoluteFillObject} pointerEvents="box-none">
{children}
</View>
</TapGestureHandler>
);
});
function Content() {
return new Array(30).fill(null).map((_, index) => (
<React.Fragment key={index}>
<View style={styles.row}>
<Text style={{color: 'black', fontSize: 20, padding: 10}}>{index}</Text>
</View>
<View style={styles.row2} />
</React.Fragment>
));
}
function Ruler() {
return (
<View
// eslint-disable-next-line react-native/no-inline-styles
style={{
top: statusBarHeight,
right: 0,
bottom: 0,
width: 40,
position: 'absolute',
}}
pointerEvents="none"
accessible={false}
accessibilityLabel=""
>
{new Array(12).fill(0).map((_, index) => (
<Text
key={index}
// eslint-disable-next-line react-native/no-inline-styles
style={{
backgroundColor: 'rgba(255, 0, 0, 0.2)',
fontSize: 8,
textAlign: 'right',
position: 'absolute',
top: index * 50,
right: 0,
}}
>
{index * 50}
</Text>
))}
</View>
);
}
const clamp = (value, minValue, maxValue) => min(max(value, minValue), maxValue);
const styles = StyleSheet.create({
root: {
flex: 1,
justifyContent: 'center',
backgroundColor: '#444',
paddingTop: statusBarHeight,
overflow: 'hidden',
},
flex: {
flex: 1,
overflow: 'hidden',
},
scrollView: {
flex: 1,
borderWidth: 2,
borderColor: 'red',
overflow: 'hidden',
},
fakeHeader: {
height: 200,
overflow: 'hidden',
},
row: {
backgroundColor: '#fff',
height: PixelRatio.roundToNearestPixel(50),
},
row2: {
backgroundColor: '#eee',
height: PixelRatio.roundToNearestPixel(50),
},
buttonWrapper: {
position: 'absolute',
bottom: 20,
right: 20,
},
button: {
width: PixelRatio.roundToNearestPixel(50),
height: PixelRatio.roundToNearestPixel(50),
borderRadius: 25,
backgroundColor: 'rgba(255, 0, 0, 0.3)',
},
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment