Skip to content

Commit 22757e6

Browse files
chore: improved slider performance
1 parent b6116b6 commit 22757e6

File tree

11 files changed

+183
-102
lines changed

11 files changed

+183
-102
lines changed

src/index.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,17 @@
1-
21
export * from './icons';
32

43
export {Badge, type BadgeProps} from './badge/Badge.component';
54

65
export {Divider, type DividerProps} from './divider/Divider.component';
76

87
export {Slider, type SliderProps} from './sliders/slider /Slider.component';
8+
export {RangeSlider, type RangeSliderProps} from './sliders/range-slider/RangeSlider.component';
99

1010
export {InputChip, type InputChipProps} from './chips/input-chip/InputChip.component';
1111
export {FilterChip, type FilterChipProps} from './chips/filter-chip/FilterChip.component';
1212
export {AssistChip, type AssistChipProps, IconType} from './chips/assist-chip/AssistChip.component';
1313
export {SuggestionChip, type SuggestionChipProps} from './chips/suggestion-chip/SuggestionChip.component';
1414

15-
1615
export {
1716
CenterAlignedTopAppBar,
1817
type CenterAlignedTopAppBarProps,

src/sliders/slider /Slider.component.tsx

Lines changed: 35 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
1+
import React from 'react';
12
import {View} from 'react-native';
2-
import React, {useState} from 'react';
3-
import {Gesture, GestureDetector} from 'react-native-gesture-handler';
3+
import {GestureDetector} from 'react-native-gesture-handler';
44
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';
65

76
import {styles} from './slider.styles';
7+
import {useSlider} from './useSlider.hook';
88
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';
9+
import {useSliderTrackPoints} from './useSliderTrackPoints.hook';
10+
import {SliderTrack} from '../ui/slider-track/SliderTrack.component';
11+
import {SliderIndicator} from '../ui/slider-indicator/SliderIndicator.component';
12+
import {SliderTrackPoint} from '../ui/slider-track-point/SliderTrackPoint.component';
1213

1314
export interface SliderProps extends ViewProps {
1415
max: number;
@@ -25,7 +26,8 @@ export interface SliderProps extends ViewProps {
2526
trackPointsStyle?: StyleProp<ViewStyle>;
2627
filledTrackStyle?: StyleProp<ViewStyle>;
2728
remainingTrackStyle?: StyleProp<ViewStyle>;
28-
valueContainerStyle?: StyleProp<ViewStyle>;
29+
30+
throttleDelay?: number;
2931

3032
onChangeValue?: (value: number) => void;
3133
}
@@ -44,124 +46,70 @@ export const Slider: React.FC<SliderProps> = ({
4446
trackPointStyle,
4547
trackPointsStyle,
4648
filledTrackStyle,
47-
valueContainerStyle,
4849
remainingTrackStyle,
4950

5051
onChangeValue,
5152
onLayout,
5253
style,
5354
...props
5455
}) => {
55-
const [sliderWidth, setSliderWith] = useState(0);
56-
const [selectedValue, setSelectedValue] = useState(value);
57-
5856
const {primary, secondaryContainer} = useTheme();
5957

60-
const sliding = useSharedValue(0);
61-
const thumbTranslationX = useSharedValue(0);
62-
const thumbTranslationXContext = useSharedValue(0);
63-
64-
const filledTrackAnimatedStyle = useAnimatedStyle(() => ({flex: interpolate(thumbTranslationX.value, [0, sliderWidth], [0, 1])}), [sliderWidth]);
65-
const remainingTrackAnimatedStyle = useAnimatedStyle(() => ({flex: interpolate(thumbTranslationX.value, [0, sliderWidth], [1, 0])}), [sliderWidth]);
66-
67-
const getDiscreteTrackPoints = (discreteStep: number) => {
68-
const totalPoints = Math.ceil((max - min) / discreteStep) + 1;
69-
70-
return Array.from({length: totalPoints}, (_, index) => Math.min(min + index * discreteStep, max));
71-
};
72-
73-
const trackPoints = step ? getDiscreteTrackPoints(step) : centered ? [min, (min + max) / 2, max] : [max];
74-
75-
const calcAndUpdateValueBasedOnThumbTranslationX = (translationX: number, onChange: (value: number) => void) => {
76-
const currrentValue = Math.round(interpolate(translationX, [0, sliderWidth], [min, max]));
77-
78-
onChange(currrentValue);
79-
};
80-
81-
useAnimatedReaction(
82-
() => thumbTranslationX.value,
83-
(currentThumbValue) => {
84-
runOnJS(calcAndUpdateValueBasedOnThumbTranslationX)(currentThumbValue, setSelectedValue);
85-
}
86-
);
58+
const trackPoints = useSliderTrackPoints({max, min, step, centered});
59+
const {
60+
gesture,
61+
sliding,
62+
selectedValue,
63+
animValueProps,
64+
filledTrackAnimatedStyle,
65+
remainingTrackAnimatedStyle,
66+
slideToTrackPoint,
67+
setUpSliderLayout,
68+
} = useSlider({max, min, step}, value, onChangeValue);
8769

88-
const gesture = Gesture.Pan()
89-
.onStart(() => {
90-
sliding.value = withTiming(1);
91-
})
92-
.onUpdate((e) => {
93-
thumbTranslationX.value = Math.max(0, Math.min(Math.round(thumbTranslationXContext.value + e.translationX), sliderWidth));
94-
95-
runOnJS(calcAndUpdateValueBasedOnThumbTranslationX)(thumbTranslationX.value, setSelectedValue);
96-
})
97-
.onEnd(() => {
98-
thumbTranslationXContext.value = thumbTranslationX.value;
99-
sliding.value = withTiming(0);
100-
101-
if (onChangeValue) {
102-
runOnJS(calcAndUpdateValueBasedOnThumbTranslationX)(thumbTranslationX.value, onChangeValue);
103-
}
104-
});
70+
const trackPointsJustifyContent = trackPoints.length > 1 ? 'space-between' : 'flex-end';
10571

10672
const handleLayoutChange = (e: LayoutChangeEvent) => {
107-
const width = e.nativeEvent.layout.width;
108-
109-
initializeThumbPosition(width);
110-
setSliderWith(width);
73+
setUpSliderLayout(e.nativeEvent.layout.width);
11174

11275
if (onLayout) {
11376
onLayout(e);
11477
}
11578
};
11679

117-
const initializeThumbPosition = (width: number) => {
118-
const initialTthumbTranslationX = ((value - min) / (max - min)) * width;
119-
120-
thumbTranslationX.value = initialTthumbTranslationX;
121-
thumbTranslationXContext.value = initialTthumbTranslationX;
122-
};
123-
124-
const slideToTrackPoint = (pointValue: number) => {
125-
const thumbPointValueTranslationX = ((pointValue - min) / (max - min)) * sliderWidth;
126-
127-
sliding.value = withTiming(1);
128-
thumbTranslationX.value = withSpring(thumbPointValueTranslationX, {damping: 20}, () => {
129-
sliding.value = withTiming(0);
130-
thumbTranslationXContext.value = thumbPointValueTranslationX;
131-
132-
if (onChangeValue) {
133-
runOnJS(onChangeValue)(pointValue);
134-
}
135-
});
136-
};
137-
13880
const renderTrackPoint = (pointValue: number) => (
13981
<SliderTrackPoint
14082
key={pointValue}
83+
selectedValue={selectedValue}
14184
value={pointValue}
14285
onPress={slideToTrackPoint}
143-
style={[{backgroundColor: selectedValue > pointValue ? secondaryContainer.background : secondaryContainer.text}, trackPointStyle]}
86+
disableColorChange={centered}
87+
style={trackPointStyle}
14488
/>
14589
);
14690

14791
return (
14892
<GestureDetector gesture={gesture}>
14993
<View style={[styles.container, style]} onLayout={handleLayoutChange} {...props}>
150-
<SliderTrack style={[{backgroundColor: primary.background}, styles.filledTrack, filledTrackAnimatedStyle, filledTrackStyle]} />
94+
<SliderTrack
95+
style={[
96+
{backgroundColor: centered ? secondaryContainer.background : primary.background},
97+
styles.filledTrack,
98+
filledTrackAnimatedStyle,
99+
filledTrackStyle,
100+
]}
101+
/>
151102
<SliderIndicator
152-
value={selectedValue}
153103
sliding={sliding}
104+
animValueProps={animValueProps}
154105
style={[styles.thumb, indicatorStyle]}
155106
thumbStyle={thumbStyle}
156107
valueStyle={valueStyle}
157-
valueContainerStyle={valueContainerStyle}
158108
/>
159109
<SliderTrack
160110
style={[{backgroundColor: secondaryContainer.background}, styles.remainingTrack, remainingTrackAnimatedStyle, remainingTrackStyle]}
161111
/>
162-
<View style={[styles.trackPoints, {justifyContent: trackPoints.length > 1 ? 'space-between' : 'flex-end'}, trackPointsStyle]}>
163-
{trackPoints.map(renderTrackPoint)}
164-
</View>
112+
<View style={[styles.trackPoints, {justifyContent: trackPointsJustifyContent}, trackPointsStyle]}>{trackPoints.map(renderTrackPoint)}</View>
165113
</View>
166114
</GestureDetector>
167115
);
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export interface SliderConfig {
2+
max: number;
3+
min: number;
4+
5+
step?: number;
6+
centered?: boolean;
7+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import {useState} from 'react';
2+
import {Gesture} from 'react-native-gesture-handler';
3+
import {interpolate, runOnJS, useAnimatedProps, useAnimatedStyle, useSharedValue, withSpring, withTiming} from 'react-native-reanimated';
4+
5+
import {type SliderConfig} from './slider-config.type';
6+
7+
export const useSlider = ({max, min, step}: SliderConfig, value: number, onChangeValue?: (value: number) => void) => {
8+
const [sliderWidth, setSliderWith] = useState(0);
9+
10+
const sliding = useSharedValue(0);
11+
const selectedValue = useSharedValue(value);
12+
const thumbTranslationX = useSharedValue(0);
13+
const thumbTranslationXContext = useSharedValue(0);
14+
15+
const filledTrackAnimatedStyle = useAnimatedStyle(() => ({flex: interpolate(thumbTranslationX.value, [0, sliderWidth], [0, 1])}), [sliderWidth]);
16+
const remainingTrackAnimatedStyle = useAnimatedStyle(() => ({flex: interpolate(thumbTranslationX.value, [0, sliderWidth], [1, 0])}), [sliderWidth]);
17+
18+
const calcValueBasedOnThumbTranslationX = (translationX: number) => {
19+
'worklet';
20+
21+
const valueBaseOnThumbTranslateX = Math.round(interpolate(translationX, [0, sliderWidth], [min, max]));
22+
const roundedValue = step ? Math.round(valueBaseOnThumbTranslateX / step) * step : valueBaseOnThumbTranslateX;
23+
24+
selectedValue.value = roundedValue;
25+
26+
return roundedValue;
27+
};
28+
29+
const gesture = Gesture.Pan()
30+
.onStart(() => {
31+
sliding.value = withTiming(1);
32+
})
33+
.onUpdate((e) => {
34+
if (step) {
35+
const stepSize = (sliderWidth / (max - min)) * step;
36+
const currentTranslationX = Math.max(0, Math.min(Math.round(thumbTranslationXContext.value + e.translationX), sliderWidth));
37+
const stepSnap = Math.round(currentTranslationX / stepSize) * stepSize;
38+
39+
thumbTranslationX.value = stepSnap;
40+
} else {
41+
thumbTranslationX.value = Math.max(0, Math.min(Math.round(thumbTranslationXContext.value + e.translationX), sliderWidth));
42+
}
43+
})
44+
.onEnd(() => {
45+
thumbTranslationXContext.value = thumbTranslationX.value;
46+
sliding.value = withTiming(0);
47+
48+
if (onChangeValue) {
49+
const valueBasedOnPos = calcValueBasedOnThumbTranslationX(thumbTranslationX.value);
50+
runOnJS(onChangeValue)(valueBasedOnPos);
51+
}
52+
});
53+
54+
const setUpSliderLayout = (width: number) => {
55+
initializeThumbPosition(width);
56+
setSliderWith(width);
57+
};
58+
59+
const initializeThumbPosition = (width: number) => {
60+
const initialTthumbTranslationX = ((value - min) / (max - min)) * width;
61+
62+
thumbTranslationX.value = initialTthumbTranslationX;
63+
thumbTranslationXContext.value = initialTthumbTranslationX;
64+
};
65+
66+
const slideToTrackPoint = (pointValue: number) => {
67+
const thumbPointValueTranslationX = ((pointValue - min) / (max - min)) * sliderWidth;
68+
69+
sliding.value = withTiming(1);
70+
thumbTranslationX.value = withSpring(thumbPointValueTranslationX, {damping: 20}, () => {
71+
sliding.value = withTiming(0);
72+
thumbTranslationXContext.value = thumbPointValueTranslationX;
73+
74+
if (onChangeValue) {
75+
runOnJS(onChangeValue)(pointValue);
76+
}
77+
});
78+
};
79+
80+
const animValueProps = useAnimatedProps(() => ({
81+
text: `${calcValueBasedOnThumbTranslationX(thumbTranslationX.value)}`,
82+
}));
83+
84+
return {
85+
gesture,
86+
sliding,
87+
animValueProps,
88+
selectedValue,
89+
filledTrackAnimatedStyle,
90+
remainingTrackAnimatedStyle,
91+
92+
slideToTrackPoint,
93+
setUpSliderLayout,
94+
};
95+
};
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import {type SliderConfig} from './slider-config.type';
2+
3+
export const useSliderTrackPoints = ({max, min, step, centered}: SliderConfig) => {
4+
const getDiscreteSliderTrackPoints = (discreteStep: number) => {
5+
const totalPoints = Math.ceil((max - min) / discreteStep) + 1;
6+
7+
return Array.from({length: totalPoints}, (_, index) => Math.min(min + index * discreteStep, max));
8+
};
9+
10+
const getContinuousSliderTrackPoints = () => (centered ? [min, (min + max) / 2, max] : [max]);
11+
12+
const trackPoints = step ? getDiscreteSliderTrackPoints(step) : getContinuousSliderTrackPoints();
13+
14+
return trackPoints;
15+
};

src/sliders/slider /slider-indicator/SliderIndicator.component.tsx renamed to src/sliders/ui/slider-indicator/SliderIndicator.component.tsx

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,23 @@
11
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';
2+
import Animated, {interpolate, useAnimatedStyle, type AnimatedProps, type SharedValue} from 'react-native-reanimated';
3+
import {View, TextInput, type ViewProps, type TextInputProps, type StyleProp, type ViewStyle, type TextStyle} from 'react-native';
44

55
import {styles} from './slider-indicator.styles';
66
import {useTheme} from '../../../theme/useTheme.hook';
77
import {useTypography} from '../../../typography/useTypography.component';
88

99
interface SliderIndicatorProps extends ViewProps {
10-
value: number;
1110
sliding: SharedValue<number>;
11+
animValueProps: AnimatedProps<Pick<TextInputProps, 'value' | 'defaultValue'>>;
1212

1313
thumbStyle?: StyleProp<ViewStyle>;
1414
valueStyle?: StyleProp<TextStyle>;
15-
valueContainerStyle?: StyleProp<ViewStyle>;
1615
}
1716

18-
export const SliderIndicator: React.FC<SliderIndicatorProps> = ({value, sliding, thumbStyle, valueStyle, valueContainerStyle, ...props}) => {
17+
Animated.addWhitelistedNativeProps({text: true});
18+
const AnimatedTextInput = Animated.createAnimatedComponent(TextInput);
19+
20+
export const SliderIndicator: React.FC<SliderIndicatorProps> = ({animValueProps, sliding, thumbStyle, valueStyle, ...props}) => {
1921
const {labelLarge} = useTypography();
2022
const {surface, primary} = useTheme();
2123

@@ -37,9 +39,11 @@ export const SliderIndicator: React.FC<SliderIndicatorProps> = ({value, sliding,
3739

3840
return (
3941
<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>
42+
<AnimatedTextInput
43+
editable={false}
44+
animatedProps={animValueProps}
45+
style={[labelLarge, styles.value, {backgroundColor: surface.backgroundInverse}, valueAnimatedStyle, {color: surface.textInverse}, valueStyle]}
46+
/>
4347
<Animated.View style={[styles.thumb, {backgroundColor: primary.background}, thumbAnimatedStyle, thumbStyle]} />
4448
</View>
4549
);

src/sliders/slider /slider-indicator/slider-indicator.styles.ts renamed to src/sliders/ui/slider-indicator/slider-indicator.styles.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export const styles = StyleSheet.create({
77
marginHorizontal: 4,
88
borderRadius: 2,
99
},
10-
valueContainer: {
10+
value: {
1111
position: 'absolute',
1212
top: -48,
1313

@@ -18,5 +18,7 @@ export const styles = StyleSheet.create({
1818
width: 48,
1919
height: 44,
2020
borderRadius: 100,
21+
22+
textAlign: 'center',
2123
},
2224
});

0 commit comments

Comments
 (0)