Skip to content

Commit e861e92

Browse files
feat: implemented continuous and discrete sliders
1 parent 576e052 commit e861e92

File tree

12 files changed

+352
-1
lines changed

12 files changed

+352
-1
lines changed

src/index.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,11 @@ export {Divider, type DividerProps} from './divider/Divider.component';
66

77
export {InputChip, type InputChipProps} from './chips/input-chip/InputChip.component';
88
export {FilterChip, type FilterChipProps} from './chips/filter-chip/FilterChip.component';
9-
export {SuggestionChip, type SuggestionChipProps} from './chips/suggestion-chip/SuggestionChip.component';
109
export {AssistChip, type AssistChipProps, IconType} from './chips/assist-chip/AssistChip.component';
10+
export {SuggestionChip, type SuggestionChipProps} from './chips/suggestion-chip/SuggestionChip.component';
11+
12+
export {DiscreteSlider, type DiscreteSliderProps} from './sliders/discrete-slider/DiscreteSlider.component';
13+
export {ContinuousSlider, type ContinuousSliderProps} from './sliders/continuous-slider/ContinuousSlider.component';
1114

1215
export {
1316
CenterAlignedTopAppBar,
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import {View} from 'react-native';
2+
import React, {useState} from 'react';
3+
import {Gesture, GestureDetector} from 'react-native-gesture-handler';
4+
import {type LayoutChangeEvent, type ViewProps, type StyleProp, type ViewStyle, type TextStyle} from 'react-native';
5+
import {interpolate, runOnJS, useAnimatedReaction, useAnimatedStyle, useSharedValue, withSpring, withTiming} from 'react-native-reanimated';
6+
7+
import {styles} from './base-slider.styles';
8+
import {useTheme} from '../../theme/useTheme.hook';
9+
import {SliderTrack} from './slider-track/SliderTrack.component';
10+
import {SliderIndicator} from './slider-indicator/SliderIndicator.component';
11+
import {SliderTrackPoint} from './slider-track-point/SliderTrackPoint.component';
12+
13+
export interface BaseSliderProps extends ViewProps {
14+
min: number;
15+
max: number;
16+
17+
value?: number;
18+
trackPoints: number[];
19+
20+
thumbStyle?: StyleProp<ViewStyle>;
21+
valueStyle?: StyleProp<TextStyle>;
22+
indicatorStyle?: StyleProp<ViewStyle>;
23+
trackPointStyle?: StyleProp<ViewStyle>;
24+
trackPointsStyle?: StyleProp<ViewStyle>;
25+
filledTrackStyle?: StyleProp<ViewStyle>;
26+
remainingTrackStyle?: StyleProp<ViewStyle>;
27+
valueContainerStyle?: StyleProp<ViewStyle>;
28+
29+
onChangeValue?: (value: number) => void;
30+
}
31+
32+
export const BaseSlider: React.FC<BaseSliderProps> = ({
33+
min,
34+
max,
35+
36+
value = 0,
37+
trackPoints = [],
38+
39+
thumbStyle,
40+
valueStyle,
41+
indicatorStyle,
42+
trackPointStyle,
43+
trackPointsStyle,
44+
filledTrackStyle,
45+
valueContainerStyle,
46+
remainingTrackStyle,
47+
48+
onChangeValue,
49+
onLayout,
50+
style,
51+
...props
52+
}) => {
53+
const [sliderWidth, setSliderWith] = useState(0);
54+
const [selectedValue, setSelectedValue] = useState(value);
55+
56+
const {primary, secondaryContainer} = useTheme();
57+
58+
const sliding = useSharedValue(0);
59+
const thumbTranslationX = useSharedValue(0);
60+
const thumbTranslationXContext = useSharedValue(0);
61+
62+
const filledTrackAnimatedStyle = useAnimatedStyle(() => ({flex: interpolate(thumbTranslationX.value, [0, sliderWidth], [0, 1])}), [sliderWidth]);
63+
const remainingTrackAnimatedStyle = useAnimatedStyle(() => ({flex: interpolate(thumbTranslationX.value, [0, sliderWidth], [1, 0])}), [sliderWidth]);
64+
65+
const calcAndUpdateValueBasedOnThumbTranslationX = (translationX: number, onChange: (value: number) => void) => {
66+
const currrentValue = Math.round(interpolate(translationX, [0, sliderWidth], [min, max]));
67+
68+
onChange(currrentValue);
69+
};
70+
71+
useAnimatedReaction(
72+
() => thumbTranslationX.value,
73+
(currentThumbValue) => {
74+
runOnJS(calcAndUpdateValueBasedOnThumbTranslationX)(currentThumbValue, setSelectedValue);
75+
}
76+
);
77+
78+
const gesture = Gesture.Pan()
79+
.onStart(() => {
80+
sliding.value = withTiming(1);
81+
})
82+
.onUpdate((e) => {
83+
thumbTranslationX.value = Math.max(0, Math.min(Math.round(thumbTranslationXContext.value + e.translationX), sliderWidth));
84+
85+
runOnJS(calcAndUpdateValueBasedOnThumbTranslationX)(thumbTranslationX.value, setSelectedValue);
86+
})
87+
.onEnd(() => {
88+
thumbTranslationXContext.value = thumbTranslationX.value;
89+
sliding.value = withTiming(0);
90+
91+
if (onChangeValue) {
92+
runOnJS(calcAndUpdateValueBasedOnThumbTranslationX)(thumbTranslationX.value, onChangeValue);
93+
}
94+
});
95+
96+
const handleLayoutChange = (e: LayoutChangeEvent) => {
97+
const width = e.nativeEvent.layout.width;
98+
99+
initializeThumbPosition(width);
100+
setSliderWith(width);
101+
102+
if (onLayout) {
103+
onLayout(e);
104+
}
105+
};
106+
107+
const initializeThumbPosition = (width: number) => {
108+
const initialTthumbTranslationX = (value * width) / max;
109+
thumbTranslationX.value = initialTthumbTranslationX;
110+
thumbTranslationXContext.value = initialTthumbTranslationX;
111+
};
112+
113+
const slideToTrackPoint = (pointValue: number) => {
114+
const thumbPointValueTranslationX = ((pointValue - min) / (max - min)) * sliderWidth;
115+
116+
sliding.value = withTiming(1);
117+
thumbTranslationX.value = withSpring(thumbPointValueTranslationX, {damping: 20}, () => {
118+
sliding.value = withTiming(0);
119+
thumbTranslationXContext.value = thumbPointValueTranslationX;
120+
121+
if (onChangeValue) {
122+
runOnJS(onChangeValue)(pointValue);
123+
}
124+
});
125+
};
126+
127+
const renderTrackPoint = (pointValue: number) => (
128+
<SliderTrackPoint
129+
key={pointValue}
130+
value={pointValue}
131+
onPress={slideToTrackPoint}
132+
style={[{backgroundColor: selectedValue > pointValue ? secondaryContainer.background : secondaryContainer.text}, trackPointStyle]}
133+
/>
134+
);
135+
136+
return (
137+
<GestureDetector gesture={gesture}>
138+
<View style={[styles.container, style]} onLayout={handleLayoutChange} {...props}>
139+
<SliderTrack style={[{backgroundColor: primary.background}, styles.filledTrack, filledTrackAnimatedStyle, filledTrackStyle]} />
140+
<SliderIndicator
141+
value={selectedValue}
142+
sliding={sliding}
143+
style={[styles.thumb, indicatorStyle]}
144+
thumbStyle={thumbStyle}
145+
valueStyle={valueStyle}
146+
valueContainerStyle={valueContainerStyle}
147+
/>
148+
<SliderTrack
149+
style={[{backgroundColor: secondaryContainer.background}, styles.remainingTrack, remainingTrackAnimatedStyle, remainingTrackStyle]}
150+
/>
151+
<View style={[styles.trackPoints, trackPointsStyle]}>{trackPoints.map(renderTrackPoint)}</View>
152+
</View>
153+
</GestureDetector>
154+
);
155+
};
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import {StyleSheet} from 'react-native';
2+
3+
export const styles = StyleSheet.create({
4+
container: {
5+
flexDirection: 'row',
6+
alignItems: 'center',
7+
},
8+
thumb: {
9+
zIndex: 1,
10+
},
11+
trackPoints: {
12+
position: 'absolute',
13+
14+
flexDirection: 'row',
15+
justifyContent: 'space-between',
16+
17+
width: '100%',
18+
paddingHorizontal: 4,
19+
},
20+
filledTrack: {
21+
borderTopEndRadius: 2,
22+
borderBottomEndRadius: 2,
23+
borderTopStartRadius: 16,
24+
borderBottomStartRadius: 16,
25+
},
26+
remainingTrack: {
27+
justifyContent: 'center',
28+
29+
borderTopEndRadius: 16,
30+
borderBottomEndRadius: 16,
31+
borderTopStartRadius: 2,
32+
borderBottomStartRadius: 2,
33+
},
34+
});
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import React from 'react';
2+
import {View, Text, type ViewProps, type StyleProp, type ViewStyle, type TextStyle} from 'react-native';
3+
import Animated, {interpolate, useAnimatedStyle, type SharedValue} from 'react-native-reanimated';
4+
5+
import {styles} from './slider-indicator.styles';
6+
import {useTheme} from '../../../theme/useTheme.hook';
7+
import {useTypography} from '../../../typography/useTypography.component';
8+
9+
interface SliderIndicatorProps extends ViewProps {
10+
value: number;
11+
sliding: SharedValue<number>;
12+
13+
thumbStyle?: StyleProp<ViewStyle>;
14+
valueStyle?: StyleProp<TextStyle>;
15+
valueContainerStyle?: StyleProp<ViewStyle>;
16+
}
17+
18+
export const SliderIndicator: React.FC<SliderIndicatorProps> = ({value, sliding, thumbStyle, valueStyle, valueContainerStyle, ...props}) => {
19+
const {labelLarge} = useTypography();
20+
const {surface, primary} = useTheme();
21+
22+
const valueAnimatedStyle = useAnimatedStyle(
23+
() => ({
24+
opacity: sliding.value,
25+
transform: [{scale: sliding.value}],
26+
top: interpolate(sliding.value, [0, 1], [-24, -48]),
27+
}),
28+
[]
29+
);
30+
31+
const thumbAnimatedStyle = useAnimatedStyle(
32+
() => ({
33+
width: interpolate(sliding.value, [0, 1], [4, 2]),
34+
}),
35+
[]
36+
);
37+
38+
return (
39+
<View {...props}>
40+
<Animated.View style={[styles.valueContainer, {backgroundColor: surface.backgroundInverse}, valueAnimatedStyle, valueContainerStyle]}>
41+
<Text style={[labelLarge, {color: surface.textInverse}, valueStyle]}>{value}</Text>
42+
</Animated.View>
43+
<Animated.View style={[styles.thumb, {backgroundColor: primary.background}, thumbAnimatedStyle, thumbStyle]} />
44+
</View>
45+
);
46+
};
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import {StyleSheet} from 'react-native';
2+
3+
export const styles = StyleSheet.create({
4+
thumb: {
5+
width: 4,
6+
height: 44,
7+
marginHorizontal: 4,
8+
borderRadius: 2,
9+
},
10+
valueContainer: {
11+
position: 'absolute',
12+
top: -48,
13+
14+
justifyContent: 'center',
15+
alignItems: 'center',
16+
alignSelf: 'center',
17+
18+
width: 48,
19+
height: 44,
20+
borderRadius: 100,
21+
},
22+
});
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import React from 'react';
2+
import {Pressable, type PressableProps, type StyleProp, type ViewStyle} from 'react-native';
3+
4+
import {styles} from './slider-track-point.styles';
5+
import {useTheme} from '../../../theme/useTheme.hook';
6+
7+
interface SliderTrackProps extends Omit<PressableProps, 'onPress'> {
8+
value: number;
9+
10+
style?: StyleProp<ViewStyle>;
11+
12+
onPress: (value: number) => void;
13+
}
14+
15+
export const SliderTrackPoint: React.FC<SliderTrackProps> = ({value, onPress, style, ...props}) => {
16+
const {secondaryContainer} = useTheme();
17+
18+
const handleTrackPointPress = () => onPress(value);
19+
20+
return (
21+
<Pressable
22+
hitSlop={16}
23+
onPress={handleTrackPointPress}
24+
style={[styles.trackPoint, {backgroundColor: secondaryContainer.text}, style]}
25+
{...props}
26+
/>
27+
);
28+
};
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import {StyleSheet} from 'react-native';
2+
3+
export const styles = StyleSheet.create({
4+
trackPoint: {
5+
width: 4,
6+
height: 4,
7+
borderRadius: 2,
8+
},
9+
});
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import {type ViewProps} from 'react-native';
2+
import React, {type PropsWithChildren} from 'react';
3+
4+
import {styles} from './slider-track.styles';
5+
import Animated, {type AnimatedProps} from 'react-native-reanimated';
6+
7+
export const SliderTrack: React.FC<PropsWithChildren<AnimatedProps<ViewProps>>> = ({children, style, ...props}) => (
8+
<Animated.View style={[styles.track, style]} {...props}>
9+
{children}
10+
</Animated.View>
11+
);
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import {StyleSheet} from 'react-native';
2+
3+
export const styles = StyleSheet.create({
4+
track: {
5+
height: 16,
6+
},
7+
});
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import React from 'react';
2+
3+
import {styles} from './continuous-slider.styles';
4+
import {BaseSlider, type BaseSliderProps} from '../base-slider/BaseSlider.component';
5+
6+
export type ContinuousSliderProps = Omit<BaseSliderProps, 'trackPoints'>;
7+
8+
export const ContinuousSlider: React.FC<ContinuousSliderProps> = ({max, trackPointsStyle, ...props}) => {
9+
const trackPoints = [max];
10+
11+
return <BaseSlider trackPoints={trackPoints} max={max} trackPointsStyle={[styles.trackPoints, trackPointsStyle]} {...props} />;
12+
};

0 commit comments

Comments
 (0)