Skip to content

With Reanimated ScrollView

The other way to settle the scroll conflict: keep Animated.ScrollView and turn its scrollEnabled off from the UI thread while a slider is being dragged. Each slider gets its own Gesture.Pan() — one gesture cannot be shared between components.

Code

WithReanimatedScrollView.tsx
import React, { useState } from 'react';
import { Platform, Text, View } from 'react-native';
import { Gesture } from 'react-native-gesture-handler';
import Animated, { useAnimatedProps, useSharedValue } from 'react-native-reanimated';
import type { ColorFormatsObject } from 'reanimated-color-picker';
import ColorPicker, { HSLSaturationSlider, HueSlider, LuminanceSlider, OpacitySlider } from 'reanimated-color-picker';
import { useContainerBackgroundColor } from './components/BaseContainer';
import { colorPickerStyle } from './components/colorPickerStyle';
/*
* Using react-native-reanimated ScrollView and cancelling the scroll while using the color picker
*/
export default function Example() {
const [resultColor, setResultColor] = useState('#f00');
const currentColor = useContainerBackgroundColor('#f00');
// runs on the ui thread on color change
const onColorChange = (color: ColorFormatsObject) => {
'worklet';
currentColor.value = color.hex;
};
// runs on the js thread on color pick
const onColorPick = (color: ColorFormatsObject) => {
setResultColor(color.hex);
};
const isScrollEnabled = useSharedValue<boolean | undefined>(true);
//? NOTE:
// - You cannot use the same gesture for multiple color picker components.
// - Either pass a new gesture or deep clone the gesture constructor.
const [hueGesture, saturationGesture, lumGesture, opacityGesture] = Array.from({ length: 4 }, () =>
Gesture.Pan()
.onBegin(() => (isScrollEnabled.value = false))
.onFinalize(() => (isScrollEnabled.value = true)),
);
const animatedProps = useAnimatedProps(
() => ({ scrollEnabled: isScrollEnabled.value }),
Platform.OS === 'web' ? [isScrollEnabled] : undefined,
);
return (
<Animated.ScrollView animatedProps={animatedProps} contentContainerStyle={{ height: '150%', justifyContent: 'center' }}>
<Text style={colorPickerStyle.title}>Color Picker in a Animated.ScrollView</Text>
<View style={colorPickerStyle.pickerContainer}>
<ColorPicker
value={resultColor}
sliderThickness={30}
thumbSize={30}
onChange={onColorChange}
onCompleteJS={onColorPick}
style={colorPickerStyle.picker}
boundedThumb
>
<View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>
<View style={{ alignItems: 'center' }}>
<Text style={colorPickerStyle.sliderTitle}>H</Text>
<HueSlider gestures={[hueGesture]} style={colorPickerStyle.sliderVerticalStyle} vertical reverse />
</View>
<View style={{ alignItems: 'center' }}>
<Text style={colorPickerStyle.sliderTitle}>S</Text>
<HSLSaturationSlider gestures={[saturationGesture]} style={colorPickerStyle.sliderVerticalStyle} vertical reverse />
</View>
<View style={{ alignItems: 'center' }}>
<Text style={colorPickerStyle.sliderTitle}>L</Text>
<LuminanceSlider gestures={[lumGesture]} style={colorPickerStyle.sliderVerticalStyle} vertical reverse />
</View>
<View style={{ alignItems: 'center' }}>
<Text style={colorPickerStyle.sliderTitle}>A</Text>
<OpacitySlider gestures={[opacityGesture]} style={colorPickerStyle.sliderVerticalStyle} vertical reverse />
</View>
</View>
</ColorPicker>
</View>
</Animated.ScrollView>
);
}

The colorPickerStyle object is shared by every example. Divider is a 1px line, and useContainerBackgroundColor only tints the modal of the example app.

components/colorPickerStyle.ts
import { Platform, StyleSheet } from "react-native";
const shadow = Platform.select({
web: { boxShadow: "rgba(0, 0, 0, 0.3) 0px 0px 2px" },
default: {
shadowColor: "#000",
shadowOffset: {
width: 0,
height: 1,
},
shadowOpacity: 0.2,
shadowRadius: 1.41,
elevation: 2,
},
});
export const colorPickerStyle = StyleSheet.create({
title: {
textAlign: "center",
fontFamily: "Quicksand",
fontWeight: "bold",
marginVertical: 20,
color: "gray",
},
picker: {
gap: 20,
},
pickerContainer: {
alignSelf: "center",
width: 300,
backgroundColor: "#eee",
padding: 20,
borderRadius: 20,
...shadow,
},
panelStyle: {
borderRadius: 16,
...shadow,
},
sliderStyle: {
borderRadius: 20,
...shadow,
},
sliderVerticalStyle: {
borderRadius: 20,
height: 300,
...shadow,
},
sliderTitle: {
color: "#000",
fontWeight: "bold",
marginBottom: 5,
paddingHorizontal: 4,
fontFamily: "Quicksand",
},
previewStyle: {
height: 40,
borderRadius: 14,
},
previewTxt: {
color: "#707070",
fontFamily: "Quicksand",
},
inputStyle: {
color: "#707070",
paddingVertical: 2,
borderColor: "#707070",
fontSize: 12,
marginLeft: 5,
},
swatchesContainer: {
alignItems: "center",
flexWrap: "nowrap",
gap: 10,
},
swatchStyle: {
borderRadius: 20,
height: 30,
width: 30,
margin: 0,
marginBottom: 0,
marginHorizontal: 0,
marginVertical: 0,
},
});