Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Slider example to Reanimated Cookbook #6094

Merged
merged 4 commits into from
Jun 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions packages/docs-reanimated/blog/slider.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
---
slug: slider
title: Slider
---

Slider allows users to adjust a value or control a setting by sliding a handle along a track. It is commonly used to adjust settings such as volume, brightness, or in this case, the width of a box.

import Slider from '@site/static/examples/Slider';
import SliderSrc from '!!raw-loader!@site/static/examples/Slider';
import ExampleVideo from '@site/src/components/ExampleVideo';
import CollapsibleCode from '@site/src/components/CollapsibleCode';

<InteractiveExample src={SliderSrc} component={Slider} />

We use the `useSharedValue` hook to store the offset of the slider handle, allowing for smooth animation during sliding.

<CollapsibleCode src={SliderSrc} showLines={[18,18]}/>

This example is done using [Pan gesture](https://docs.swmansion.com/react-native-gesture-handler/docs/gestures/pan-gesture) from `react-native-gesture-handler` library. It adjusts the handle's position and width of the box accordingly to the current offset. The offset is a [shared value](/docs/fundamentals/glossary#shared-value) and is updated during the `onChange` event of the pan gesture.

<samp id="Slider">Slider</samp>

<CollapsibleCode src={SliderSrc} showLines={[28,41]}/>

<ExampleVideo
sources={{
android: "/react-native-reanimated/recordings/examples/slider_android.mov",
ios: "/react-native-reanimated/recordings/examples/slider_ios.mov"
}}
/>

The `useAnimatedStyle` hook is used to create animated styles for both the box and the slider handle. This ensures that changes to the offset value result in smooth animations for both components.

<samp id="Slider">Slider</samp>

<CollapsibleCode src={SliderSrc} showLines={[40,50]}/>

Leveraging animated props allows us to run them on the UI thread instead of the JS thread. To prevent unnecessary re-renders when the text displaying the current width of the box changes, we used the `useAnimatedProps` hook.

Additionally, we opted for **TextInput** instead of **Text** because **TextInput** has a `value` property that can be animated, whereas **Text** only has children.

This approach also enabled us to animate **TextInput** using [shared values](fundamentals/glossary#shared-value).

<samp id="Slider">Slider</samp>

<CollapsibleCode src={SliderSrc} showLines={[53,59]}/>
112 changes: 112 additions & 0 deletions packages/docs-reanimated/static/examples/Slider.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import React from 'react';
import { View, StyleSheet } from 'react-native';
import { TextInput } from 'react-native-gesture-handler';
import {
GestureHandlerRootView,
GestureDetector,
Gesture,
} from 'react-native-gesture-handler';
import Animated, {
useSharedValue,
useAnimatedStyle,
useAnimatedProps,
} from 'react-native-reanimated';

const INITIAL_BOX_SIZE = 50;
const SLIDER_WIDTH = 300;

Animated.addWhitelistedNativeProps({ text: true });

const AnimatedTextInput = Animated.createAnimatedComponent(TextInput);

const Slider = () => {
const offset = useSharedValue(0);
const boxWidth = useSharedValue(INITIAL_BOX_SIZE);
const MAX_VALUE = SLIDER_WIDTH - INITIAL_BOX_SIZE;

const pan = Gesture.Pan().onChange((event) => {
offset.value =
Math.abs(offset.value) <= MAX_VALUE
? offset.value + event.changeX <= 0
? 0
: offset.value + event.changeX >= MAX_VALUE
? MAX_VALUE
: offset.value + event.changeX
: offset.value;

const newWidth = INITIAL_BOX_SIZE + offset.value;
boxWidth.value = newWidth;
});

const boxStyle = useAnimatedStyle(() => {
return {
width: INITIAL_BOX_SIZE + offset.value,
};
});

const sliderStyle = useAnimatedStyle(() => {
return {
transform: [{ translateX: offset.value }],
};
});

const animatedProps = useAnimatedProps(() => {
return {
text: `Box width: ${Math.round(boxWidth.value)}`,
defaultValue: `Box width: ${boxWidth.value}`,
};
});

return (
<GestureHandlerRootView style={styles.container}>
<AnimatedTextInput
animatedProps={animatedProps}
style={styles.boxWidthText}
editable={false}
/>
<Animated.View style={[styles.box, boxStyle]} />
<View style={styles.sliderTrack}>
<GestureDetector gesture={pan}>
<Animated.View style={[styles.sliderHandle, sliderStyle]} />
</GestureDetector>
</View>
</GestureHandlerRootView>
);
};

const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
gap: 32,
},
sliderTrack: {
width: SLIDER_WIDTH,
height: 50,
backgroundColor: '#82cab2',
borderRadius: 25,
justifyContent: 'center',
padding: 5,
},
sliderHandle: {
width: 40,
height: 40,
backgroundColor: '#f8f9ff',
borderRadius: 20,
position: 'absolute',
left: 5,
},
box: {
height: INITIAL_BOX_SIZE,
backgroundColor: '#b58df1',
borderRadius: 10,
},
boxWidthText: {
textAlign: 'center',
fontSize: 18,
color: '#001a72',
},
});

export default Slider;
Binary file not shown.
Binary file not shown.