Created
April 14, 2023 16:26
-
-
Save iam-rohid/56fa7f3c2c8a9b81cfea526e52f514c0 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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