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.

o

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)

Most upvoted comments

<Modal transparent> 
   <GestureHandlerRootView style={{flex:1}}> 
      <PanGestureHandler>
         <Animated.View>
            {/* Your components */}
         </Animated.View>
      </PanGestureHandler>
   </GestureHandlerRootView>
</Modal>

it worked for me when i did it like this

Adding activeOffsetX={[0, 0]} to PanGestureHandler fixed it for me. 😃

<Modal transparent> 
   <GestureHandlerRootView style={{flex:1}}> 
      <PanGestureHandler>
         <Animated.View>
            {/* Your components */}
         </Animated.View>
      </PanGestureHandler>
   </GestureHandlerRootView>
</Modal>

it worked for me when i did it like this

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

<Modal transparent> 
   <GestureHandlerRootView style={{flex:1}}> 
      <PanGestureHandler>
         <Animated.View>
            {/* Your components */}
         </Animated.View>
      </PanGestureHandler>
   </GestureHandlerRootView>
</Modal>

it worked for me when i did it like this

Timesaver! GestureHandlerRootView helps

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.

@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