In the following simple slider (typescript) example the PanGestureHandler from react-native-gesture-handler will only be set after the gesture was started. The user would need to move the finger.
This is what I would like to achieve: Taps should also set the slider value (including tap and drag). This is a common pattern, e.g. when seeking through a video file or setting volume to max and then adjusting.
I guess I could wrap this in a TapGestureHandler but I'm seeking the most elegant way to achieve this without too much boilerplate.
// example extracted from https://www.npmjs.com/package/react-native-reanimated-slider
import React, { Component } from 'react';
import Animated from 'react-native-reanimated';
import { PanGestureHandler, State } from 'react-native-gesture-handler';
const { Value, event, cond, eq, Extrapolate, interpolate } = Animated;
interface IProps {
minimumTrackTintColor: string;
maximumTrackTintColor: string;
cacheTrackTintColor: string;
value: number;
style: any;
cache;
onSlidingStart;
onSlidingComplete;
}
class Slider extends Component<IProps, {}> {
static defaultProps = {
minimumTrackTintColor: '#f3f',
maximumTrackTintColor: 'transparent',
cacheTrackTintColor: '#777',
};
private gestureState;
private x;
private width;
private clamped_x;
private onGestureEvent;
public constructor(props: IProps) {
super(props);
this.gestureState = new Value(State.UNDETERMINED);
this.x = new Value(0);
this.width = new Value(0);
this.clamped_x = cond(
eq(this.width, 0),
0,
interpolate(this.x, {
inputRange: [0, this.width],
outputRange: [0, this.width],
extrapolate: Extrapolate.CLAMP,
})
);
this.onGestureEvent = event([
{
nativeEvent: {
state: this.gestureState,
x: this.x,
},
},
]);
}
onLayout = ({ nativeEvent }) => {
this.width.setValue(nativeEvent.layout.width);
};
render() {
const { style, minimumTrackTintColor, maximumTrackTintColor } = this.props;
return (
<PanGestureHandler
onGestureEvent={this.onGestureEvent}
onHandlerStateChange={this.onGestureEvent}
>
<Animated.View
style={[
{
flex: 1,
height: 30,
overflow: 'visible',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#3330',
},
style,
]}
onLayout={this.onLayout}
>
<Animated.View
style={{
width: '100%',
height: 5,
borderRadius: 2,
overflow: 'hidden',
borderWidth: 1,
backgroundColor: maximumTrackTintColor,
}}
>
<Animated.View
style={{
backgroundColor: minimumTrackTintColor,
height: '100%',
maxWidth: '100%',
width: this.clamped_x,
position: 'absolute',
}}
/>
</Animated.View>
</Animated.View>
</PanGestureHandler>
);
}
}
export default Slider;
Thanks in advance!
Edit: This works as intended, but has a visible render quirk and also a small delay.
import React, { Component } from 'react';
import Animated from 'react-native-reanimated';
import { PanGestureHandler, TapGestureHandler, State } from 'react-native-gesture-handler';
const { Value, event, cond, eq, Extrapolate, interpolate } = Animated;
interface IProps {
minimumTrackTintColor?: string;
maximumTrackTintColor?: string;
cacheTrackTintColor?: string;
value: number;
style?: any;
onSlidingStart;
onSlidingComplete;
}
class Slider extends Component<IProps, {}> {
static defaultProps = {
minimumTrackTintColor: '#f3f',
maximumTrackTintColor: 'transparent',
cacheTrackTintColor: '#777',
};
private gestureState;
private x;
private width;
private clamped_x;
private onGestureEvent;
private onTapGesture;
public constructor(props: IProps) {
super(props);
this.gestureState = new Value(State.UNDETERMINED);
this.x = new Value(0);
this.width = new Value(0);
this.clamped_x = cond(
eq(this.width, 0),
0,
interpolate(this.x, {
inputRange: [0, this.width],
outputRange: [0, this.width],
extrapolate: Extrapolate.CLAMP,
})
);
this.onGestureEvent = event([
{
nativeEvent: {
state: this.gestureState,
x: this.x,
},
},
]);
this.onTapGesture = event([
{
nativeEvent: {
state: this.gestureState,
x: this.x,
},
},
]);
}
onLayout = ({ nativeEvent }) => {
this.width.setValue(nativeEvent.layout.width);
};
render() {
const { style, minimumTrackTintColor, maximumTrackTintColor } = this.props;
return (
<TapGestureHandler
onGestureEvent={this.onTapGesture}
onHandlerStateChange={this.onTapGesture}
>
<Animated.View>
<PanGestureHandler
onGestureEvent={this.onGestureEvent}
onHandlerStateChange={this.onGestureEvent}
>
<Animated.View
style={[
{
flex: 1,
height: 30,
overflow: 'visible',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#3330',
},
style,
]}
onLayout={this.onLayout}
>
<Animated.View
style={{
width: '100%',
height: 5,
borderRadius: 2,
overflow: 'hidden',
borderWidth: 1,
backgroundColor: maximumTrackTintColor,
}}
>
<Animated.View
style={{
backgroundColor: minimumTrackTintColor,
height: '100%',
maxWidth: '100%',
width: this.clamped_x,
position: 'absolute',
}}
/>
</Animated.View>
</Animated.View>
</PanGestureHandler>
</Animated.View>
</TapGestureHandler>
);
}
}
export default Slider;
After reading the documentation several times I figured it out. It's simpler than expected :)
<PanGestureHandler
onGestureEvent={this.onGestureEvent}
onHandlerStateChange={this.onGestureEvent}
minDist={0}
>
The property minDist
can be set to 0
.
Actually one needs to use the LongPressGestureHandler, as the PanHandler only changes it's state after some initial movement and not on touch begin.
The solution is to use something like:
<LongPressGestureHandler
onGestureEvent={this.onGestureEvent}
onHandlerStateChange={this.onGestureEvent}
minDurationMs={0}
maxDist={Number.MAX_SAFE_INTEGER}
shouldCancelWhenOutside={false}
hitSlop={10}
>
{...}
</LongPressGestureHandler>