Hi!
I've read the docs about 20 times now, and I still can't seem to get my <PanGestureHandler> working on Android.

The View is in a Modal, but that Modal is wrapped with the gesture handler root HOC. I've also updated my MainActivity. Other PanGestureHandlers work, so it must be something with my code specifically, but I can't pinpoint it.
import React, { useMemo, useEffect, useCallback, useState } from 'react';
import { View, StyleSheet, LayoutChangeEvent, LayoutRectangle, ViewStyle, StyleProp, TextInput, Platform } from 'react-native';
import { PanGestureHandler, State } from 'react-native-gesture-handler';
import { usePanGestureHandler } from 'react-native-redash';
import Reanimated, {
useValue,
useCode,
cond,
eq,
set,
Extrapolate,
call,
interpolate,
concat,
round,
divide,
neq,
and,
greaterThan,
sub,
} from 'react-native-reanimated';
const THUMB_DIAMETER = Platform.OS === 'ios' ? 30 : 20;
const THUMB_RADIUS = THUMB_DIAMETER / 2;
const RAIL_HEIGHT = 3;
const GESTURE_HANDLER_ACTIVE_OFFSET_X = [-3, 3];
const GESTURE_HANDLER_FAIL_OFFSET_Y = [-5, 5];
const ReanimatedTextInput = Reanimated.createAnimatedComponent(TextInput);
export interface HighlightedSliderProps {
style?: StyleProp<ViewStyle>;
minValue: number;
maxValue: number;
value: number;
onValueChange: (value: number) => void;
colors: SliderColors;
showLabel?: boolean;
onReanimatedValueNodeChange?: (reanimatedValueNode: Reanimated.Node<number>) => void;
textPrefix?: string;
textSuffix?: string;
}
export interface SliderColors {
thumbColor: string;
activeRailColor: string;
inactiveRailColor: string;
}
// TODO: Set reanimated values thumbX, thumbValue and offsetX if prop "value" changes.
export default function HighlightedSlider(props: HighlightedSliderProps): JSX.Element {
const { style, minValue, maxValue, value, onValueChange, colors, textPrefix, textSuffix, onReanimatedValueNodeChange, showLabel } = props;
const { gestureHandler, state, position } = usePanGestureHandler();
const [layout, setLayout] = useState<LayoutRectangle>({ height: 0, width: 0, x: 0, y: 0 });
const step = useMemo(() => (layout.width > 0 ? layout.width / (maxValue - minValue) : 1), [layout.width, maxValue, minValue]);
const lower = useMemo(() => Math.max(minValue * step, 0), [minValue, step]);
const upper = useMemo(() => Math.max(maxValue * step - THUMB_DIAMETER, 0), [maxValue, step]);
const upperWidth = useMemo(() => Math.max(layout.width - THUMB_DIAMETER, 0), [layout.width]);
//#region Animations
const thumbX = useValue(value);
const thumbValue = useMemo(() => {
return interpolate(thumbX, {
inputRange: [lower, upper],
outputRange: [minValue, maxValue],
extrapolate: Extrapolate.CLAMP,
});
}, [lower, maxValue, minValue, thumbX, upper]);
const thumbValueString = useMemo(
() =>
showLabel
? concat(
textPrefix ?? '',
round(
interpolate(thumbX, {
inputRange: [lower, upper],
outputRange: [minValue, maxValue],
extrapolate: Extrapolate.CLAMP,
}),
),
textSuffix ?? '',
)
: undefined,
[showLabel, lower, maxValue, minValue, textPrefix, textSuffix, thumbX, upper],
);
useCode(
() => [
cond(greaterThan(layout.width, 1), [
cond(and(neq(state, State.ACTIVE), neq(state, State.END), neq(round(thumbValue), round(value))), [
set(
thumbX,
interpolate(value, {
inputRange: [minValue, maxValue],
outputRange: [lower, upper],
extrapolate: Extrapolate.CLAMP,
}),
),
]),
cond(eq(state, State.ACTIVE), [
set(
thumbX,
interpolate(sub(position.x, THUMB_RADIUS), {
inputRange: [0, upperWidth],
outputRange: [lower, upper],
extrapolate: Extrapolate.CLAMP,
}),
),
]),
cond(eq(state, State.END), [call([thumbValue], ([_thumbValue]) => onValueChange(_thumbValue))]),
]),
],
[layout.width, lower, maxValue, minValue, onValueChange, position.x, state, thumbValue, thumbX, upper, upperWidth, value],
);
const activeRailScaleX = useMemo(() => {
return Reanimated.interpolate(thumbX, {
inputRange: [lower, upper],
outputRange: [0, layout.width],
});
}, [thumbX, lower, upper, layout.width]);
const activeRailTranslateX = useMemo(() => {
return Reanimated.interpolate(thumbX, {
inputRange: [lower, upper],
outputRange: [0, divide(layout.width, 2, activeRailScaleX)],
});
}, [activeRailScaleX, layout.width, lower, thumbX, upper]);
//#endregion
//#region Memos
const inactiveRailStyle = useMemo(() => [styles.inactiveRail, { backgroundColor: colors.inactiveRailColor }], [colors.inactiveRailColor]);
const activeRailStyle = useMemo(
() => [
styles.activeRail,
{ backgroundColor: colors.activeRailColor, transform: [{ scaleX: activeRailScaleX }, { translateX: activeRailTranslateX }] },
],
[activeRailScaleX, activeRailTranslateX, colors.activeRailColor],
);
const thumbStyle = useMemo(
() => [styles.thumb, { shadowColor: colors.thumbColor, backgroundColor: colors.thumbColor, transform: [{ translateX: thumbX }] }],
[colors.thumbColor, thumbX],
);
const viewStyle = useMemo(() => [styles.slider, style], [style]);
//#endregion
//#region Callbacks
const onViewLayout = useCallback(
({ nativeEvent }: LayoutChangeEvent) => {
if (JSON.stringify(layout) !== JSON.stringify(nativeEvent.layout)) setLayout(nativeEvent.layout);
},
[layout],
);
//#endregion
//#region Effects
useEffect(() => {
if (onReanimatedValueNodeChange != null) onReanimatedValueNodeChange(thumbValue);
}, [onReanimatedValueNodeChange, thumbValue]);
//#endregion
return (
<PanGestureHandler {...gestureHandler} activeOffsetX={GESTURE_HANDLER_ACTIVE_OFFSET_X} failOffsetY={GESTURE_HANDLER_FAIL_OFFSET_Y}>
<Reanimated.View style={viewStyle} onLayout={onViewLayout}>
{showLabel && <ReanimatedTextInput style={styles.text} text={thumbValueString} editable={false} underlineColorAndroid="transparent" />}
<View style={inactiveRailStyle}>
<Reanimated.View style={activeRailStyle} />
<Reanimated.View style={thumbStyle} />
</View>
</Reanimated.View>
</PanGestureHandler>
);
}
const styles = StyleSheet.create({
slider: {
width: '100%',
alignItems: 'center',
},
text: {
paddingVertical: 0,
fontSize: 12,
marginBottom: THUMB_RADIUS + 7,
fontWeight: 'bold',
color: 'black',
},
thumb: {
height: THUMB_DIAMETER,
width: THUMB_DIAMETER,
borderRadius: THUMB_RADIUS,
top: -THUMB_RADIUS + 1,
position: 'absolute',
shadowOffset: {
height: 1,
width: 0,
},
shadowOpacity: 0.7,
shadowRadius: 2,
},
inactiveRail: {
width: '100%',
height: RAIL_HEIGHT,
borderRadius: RAIL_HEIGHT,
},
activeRail: {
width: 1,
height: RAIL_HEIGHT,
borderRadius: RAIL_HEIGHT,
position: 'absolute',
left: 0,
},
});
I've added debug statements everywhere and noticed that the <PanGestureHandler>'s onHandlerStateChange and onGestureEvent callbacks never get called. The gesture never gets recognized. Anyone know why?
I'm pretty sure it has something to do with pointerEvents, not sure why that would get overriden though. I've played around with the property and got it recognizing my gesture once, but it hasn't worked since then. Is this a bug?
Maybe because my thumb has position: 'absolute'?
same thing happened to me for React Native 0.63.2
it was fine on React Native 0.62
same thing happened to me for React Native 0.63.2
it was fine on React Native 0.62
I missed the installation step whereby I need to update MainAcivity.java.
It works ok now.
I didn't miss the installation step in MainActivity.java, but it still doesn't work. It _sometimes_ works on my HomeScreen, and it _never_ works in my Modal.
Because RNGH doesn't work in modals at the moment.
If you do an Animated.duration(myValue, {toValue: 0, duration: 0}) as soon as you mount the component does it start working?
@evanc I'm sorry, what?

oops, sorry, I meant Animated.timing. You'll also need to set the property useNativeDriver or it will warn.
basically just do a throwaway animation on the value. I have seen a similar issue where the value wouldn't work until it had been Animated at least once.
@evanc Oh I see.
I've added:
useEffect(() => {
Reanimated.timing(thumbX, { toValue: 0, duration: 0, easing: Easing.linear });
}, [thumbX]);
And it still didn't work.
EDIT: What do you mean with useNativeDriver? Do you mean react-native's Animated library instead of Reanimated?
I have a similar issue, can't make a TouchableOpacity work if wrapped in a PanGestureHandler.
No issue on iOS.
Does someone have a clue on this ?
Same issue here even after following the installation instructions. PanGestureHandler works fine on iOS but doesn't even fire any events on Android.
@MrGVSV, please provide some repro showing this problem.
this works for me on Android, it's an age slider
const { width } = Dimensions.get('window');
export default function AgeEditPage(props) {
const [rangeMeter, setRangeMeter] = useState(100);
const [maxAge, setMaxAge] = useState(18);
useEffect(() => {
updateRange(100);
}, []);
const updateRange = (meterRange) => {
const meterPercentage = (meterRange - 90) / (width - 150);
const meterAge = Math.floor(18 + meterPercentage * 62);
setMaxAge(meterAge);
setRangeMeter(meterRange);
};
const updateDelta = (event) => {
const { absoluteX } = event.nativeEvent;
const meterRange = clamp(absoluteX, 90, width - 60);
updateRange(meterRange);
};
return (
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
<View style={styles.info}>
<Text style={styles.title}>I'm {maxAge} years old</Text>
<PanGestureHandler onGestureEvent={updateDelta} activeOffsetX={[-10, 10]}>
<View style={[styles.range, { width: rangeMeter }]}>
<Text style={styles.rangetext}>Age</Text>
</View>
</PanGestureHandler>
</View>
</ScrollView>
);
}
@mrousavy, is this issue still relevant?
(I mean if it isn't connected to the modal issue I linked. We're transferring issues to discussions and I want to close anything that may be outdated by now)
@jakub-gonet not for me, since I'm not using <Modal> components anymore. In a react-native-navigation Modal screen everything works as expected. And we can definitely close this one since it's a duplicate of #139 (my bad) 馃憤
Thanks for the fast follow-up!
Adding activeOffsetX={[0, 0]} to PanGestureHandler fixed it for me. :)
Most helpful comment
I missed the installation step whereby I need to update MainAcivity.java.
It works ok now.