react-native-gesture-handler: Android: `` not working in ``
Description
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.
Code
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,
},
});
Package versions
- React: 16.13.1
- React Native: 0.63.2
- React Native Gesture Handler: 1.7.0
About this issue
- Original URL
- State: closed
- Created 4 years ago
- Reactions: 6
- Comments: 23 (10 by maintainers)
it worked for me when i did it like this
Adding
activeOffsetX={[0, 0]}
toPanGestureHandler
fixed it for me. 😃Just to complement @shoki61 solution if anyone needs more clarification, this is mentioned on documentation: https://docs.swmansion.com/react-native-gesture-handler/docs/installation#usage-with-modals-on-android
They recommend using gestureHandlerRootHOC, which is the equivalent of wrapping with <GestureHandlerRootView style={{ flex: 1 }}>. You can check here: https://github.com/software-mansion/react-native-gesture-handler/blob/main/src/gestureHandlerRootHOC.tsx
Timesaver! GestureHandlerRootView helps
I missed the installation step whereby I need to update MainAcivity.java.
It works ok now.
@shoki61 work for me, thanks for your contribution!
I had the problem of moving the Animated.View on a Modal on Android (on iOS, no problem), after I tried @shoki61’s answer, it worked, so just wrap the <PanGestureHandler> component with <GestureHandlerRootView>
@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) 👍same thing happened to me for React Native 0.63.2 it was fine on React Native 0.62