Skip to content

Instantly share code, notes, and snippets.

@iam-rohid
Created April 14, 2023 16:26
Show Gist options
  • Save iam-rohid/56fa7f3c2c8a9b81cfea526e52f514c0 to your computer and use it in GitHub Desktop.
Save iam-rohid/56fa7f3c2c8a9b81cfea526e52f514c0 to your computer and use it in GitHub Desktop.
import {View, Text, StyleSheet, LayoutChangeEvent} from 'react-native';
import React, {useCallback, useState} from 'react';
import Animated, {
Extrapolate,
SharedValue,
interpolate,
runOnJS,
useAnimatedGestureHandler,
useAnimatedStyle,
useSharedValue,
withSpring,
} from 'react-native-reanimated';
import {
PanGestureHandlerGestureEvent,
PanGestureHandler,
GestureEvent,
PanGestureHandlerEventPayload,
} from 'react-native-gesture-handler';
import {
DEFAULT_END_REMINDER_TIME,
DEFAULT_START_REMINDER_TIME,
MAX_REMINDER_TIME,
MIN_REMINDER_TIME,
} from '../utils/constants';
const useHandleAnimation = ({
position,
minPos,
maxPos,
onChange,
}: {
position: SharedValue<number>;
minPos: SharedValue<number>;
maxPos: SharedValue<number>;
onChange: (value: number) => void;
}): [
(event: GestureEvent<PanGestureHandlerEventPayload>) => void,
SharedValue<boolean>,
] => {
const isPanning = useSharedValue(false);
const panEvent = useAnimatedGestureHandler<
PanGestureHandlerGestureEvent,
{prevousPos: number}
>(
{
onStart(_, context) {
context.prevousPos = position.value;
isPanning.value = true;
},
onActive(event, context) {
position.value = Math.min(
maxPos.value,
Math.max(minPos.value, context.prevousPos + event.translationX),
);
runOnJS(onChange)(position.value);
},
onFinish() {
isPanning.value = false;
runOnJS(onChange)(position.value);
},
},
[minPos, maxPos, onChange],
);
return [panEvent, isPanning];
};
const useIndicatorStyle = ({
position,
panning,
}: {
position: SharedValue<number>;
panning: SharedValue<boolean>;
}) => {
const handleStyle = useAnimatedStyle(
() => ({
left: position.value,
}),
[position],
);
const highlightStyle = useAnimatedStyle(
() => ({
transform: [
{
scale: withSpring(panning.value ? 2 : 1),
},
],
}),
[panning],
);
return {
handleStyle,
highlightStyle,
};
};
export default function PrecisionTimeRangeSlider() {
const barWidth = useSharedValue(0);
const leftHandlePos = useSharedValue(0);
const rightHandlePos = useSharedValue(100);
const [startAt, setStartAt] = useState(DEFAULT_START_REMINDER_TIME);
const [endAt, setEndAt] = useState(DEFAULT_END_REMINDER_TIME);
const [leftHandleEvent, leftHandlePanning] = useHandleAnimation({
position: leftHandlePos,
minPos: useSharedValue(0),
maxPos: rightHandlePos,
onChange(value) {
const newTime = interpolate(
value,
[0, barWidth.value],
[MIN_REMINDER_TIME, MAX_REMINDER_TIME],
Extrapolate.CLAMP,
);
setStartAt(newTime - (newTime % 15));
},
});
const [rightHandleEvent, rightHandlePanning] = useHandleAnimation({
position: rightHandlePos,
minPos: leftHandlePos,
maxPos: barWidth,
onChange(value) {
const newTime = interpolate(
value,
[0, barWidth.value],
[MIN_REMINDER_TIME, MAX_REMINDER_TIME],
Extrapolate.CLAMP,
);
setEndAt(newTime - (newTime % 15));
},
});
const leftHandleStyles = useIndicatorStyle({
position: leftHandlePos,
panning: leftHandlePanning,
});
const rightHandleStyles = useIndicatorStyle({
position: rightHandlePos,
panning: rightHandlePanning,
});
const highlightedBarAnimatedStyle = useAnimatedStyle(
() => ({
left: leftHandlePos.value,
right: barWidth.value - rightHandlePos.value,
}),
[],
);
const getTime = useCallback((value: number) => {
const h = Math.floor(value / 60);
const m = Math.floor(value % 60);
const isPM = h >= 12 && h < 24;
return `${h - (h > 12 ? 12 : 0)}:${
m.toString().length === 1 ? `0${m}` : m
} ${isPM ? 'PM' : 'AM'}`;
}, []);
const onLayout = useCallback(
(event: LayoutChangeEvent) => {
barWidth.value = event.nativeEvent.layout.width;
console.log('Layout changed');
leftHandlePos.value = interpolate(
DEFAULT_START_REMINDER_TIME,
[MIN_REMINDER_TIME, MAX_REMINDER_TIME],
[0, event.nativeEvent.layout.width],
Extrapolate.CLAMP,
);
rightHandlePos.value = interpolate(
DEFAULT_END_REMINDER_TIME,
[MIN_REMINDER_TIME, MAX_REMINDER_TIME],
[0, event.nativeEvent.layout.width],
Extrapolate.CLAMP,
);
},
[barWidth, leftHandlePos, rightHandlePos],
);
return (
<View>
<View style={styles.labelsWrapper}>
<View style={styles.leftLabels}>
<Text style={styles.title}>{getTime(startAt)}</Text>
<Text style={styles.subtitle}>Start At</Text>
</View>
<View style={styles.rightLabels}>
<Text style={styles.title}>{getTime(endAt)}</Text>
<Text style={styles.subtitle}>End At</Text>
</View>
</View>
<View style={styles.barWrapper}>
<View style={styles.bar} onLayout={onLayout}>
<Animated.View
style={[styles.highlightedBar, highlightedBarAnimatedStyle]}
/>
<PanGestureHandler onGestureEvent={leftHandleEvent}>
<Animated.View
style={[styles.handle, leftHandleStyles.handleStyle]}>
<Animated.View
style={[
styles.handleIndicator,
leftHandleStyles.highlightStyle,
]}
/>
</Animated.View>
</PanGestureHandler>
<PanGestureHandler onGestureEvent={rightHandleEvent}>
<Animated.View
style={[styles.handle, rightHandleStyles.handleStyle]}>
<Animated.View
style={[
styles.handleIndicator,
rightHandleStyles.highlightStyle,
]}
/>
</Animated.View>
</PanGestureHandler>
</View>
</View>
</View>
);
}
const styles = StyleSheet.create({
labelsWrapper: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 32,
},
leftLabels: {
alignItems: 'flex-start',
},
rightLabels: {
alignItems: 'flex-end',
},
title: {
fontSize: 24,
textAlign: 'center',
fontWeight: '700',
fontFamily: 'Nunito',
color: '#fff',
},
subtitle: {
fontSize: 16,
textAlign: 'center',
fontWeight: '700',
fontFamily: 'Nunito',
color: '#fff',
textTransform: 'uppercase',
opacity: 0.5,
},
barWrapper: {
paddingHorizontal: 12,
},
bar: {
position: 'relative',
height: 8,
backgroundColor: 'rgba(255,255,255,0.1)',
borderRadius: 8,
},
highlightedBar: {
position: 'absolute',
backgroundColor: '#fff',
top: 0,
bottom: 0,
},
handle: {
width: 24,
height: 24,
borderRadius: 24,
backgroundColor: '#fff',
top: 4,
position: 'absolute',
alignItems: 'center',
justifyContent: 'center',
transform: [
{
translateX: -12,
},
{
translateY: -12,
},
],
},
handleIndicator: {
borderRadius: 48,
backgroundColor: 'rgba(255,255,255,0.1)',
position: 'absolute',
left: 0,
right: 0,
top: 0,
bottom: 0,
},
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment