diff --git a/package.json b/package.json
index 42e74b9..85ebbc6 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@wowmaking/react-native-ios-scroll-picker",
- "version": "1.0.3",
+ "version": "1.0.6",
"description": "Scroll picker like IOS",
"scripts": {
"build": "tsc --project ./tsconfig.json",
@@ -40,7 +40,7 @@
"react-native-builder-bob": "^0.18.1",
"react-native-gesture-handler": "^1.10.3",
"react-native-haptic-feedback": "^1.11.0",
- "react-native-reanimated": "^2.2.2",
+ "react-native-reanimated": "^2.3.1",
"typescript": "^4.4.3"
},
"main": "lib/module/index.js",
@@ -70,4 +70,4 @@
"lib/"
],
"license": "MIT"
-}
+}
\ No newline at end of file
diff --git a/src/animationHelpers.js b/src/animationHelpers.js
new file mode 100644
index 0000000..ead437c
--- /dev/null
+++ b/src/animationHelpers.js
@@ -0,0 +1,60 @@
+import { Clock, Value, add, block, cond, eq, set, startClock, and, not, clockRunning, timing, EasingNode, stopClock, or, call, } from 'react-native-reanimated';
+import { State } from 'react-native-gesture-handler';
+import RNHapticFeedback from 'react-native-haptic-feedback';
+import _throttle from 'lodash/throttle';
+import { snapPoint } from './redash';
+export const withDecay = (params) => {
+ const { itemHeight, value, velocity, state: gestureState, offset, snapPoints, values, defaultValue = 1 } = {
+ ...params,
+ };
+ const init = new Value(0);
+ const clock = new Clock();
+ const state = {
+ finished: new Value(0),
+ position: new Value(0),
+ time: new Value(0),
+ frameTime: new Value(0),
+ };
+ const config = {
+ toValue: new Value(0),
+ duration: new Value(1000),
+ easing: EasingNode.bezier(0.22, 1, 0.36, 1),
+ };
+ const defaultIndex = values.findIndex((v) => v.value === defaultValue);
+ const vibrate = _throttle(() => {
+ RNHapticFeedback.trigger('selection', {
+ enableVibrateFallback: false,
+ ignoreAndroidSystemSettings: false
+ });
+ }, 100);
+ let val = defaultValue;
+ return block([
+ cond(not(init), [
+ set(offset, -itemHeight * defaultIndex),
+ set(state.position, offset),
+ set(init, 1),
+ ]),
+ cond(eq(gestureState, State.BEGAN), set(offset, state.position)),
+ cond(eq(gestureState, State.ACTIVE), [
+ set(state.position, add(offset, value)),
+ set(state.time, 0),
+ set(state.frameTime, 0),
+ set(state.finished, 0),
+ set(config.toValue, snapPoint(state.position, velocity, snapPoints)),
+ ]),
+ cond(and(not(state.finished), eq(gestureState, State.END)), [
+ cond(not(clockRunning(clock)), [startClock(clock)]),
+ timing(clock, state, config),
+ cond(state.finished, stopClock(clock)),
+ ]),
+ cond(or(eq(gestureState, State.ACTIVE), state.finished), call([state.position], (currentValue) => {
+ const selectedIndex = Math.round(-currentValue / itemHeight);
+ const newValue = values[selectedIndex]?.value;
+ if (newValue && newValue !== val) {
+ val = newValue;
+ vibrate();
+ }
+ })),
+ state.position,
+ ]);
+};
diff --git a/src/gestureHandler.js b/src/gestureHandler.js
new file mode 100644
index 0000000..da86022
--- /dev/null
+++ b/src/gestureHandler.js
@@ -0,0 +1,32 @@
+import React, { useMemo } from 'react';
+import { StyleSheet } from 'react-native';
+import Animated, { useCode, set, Value, add, call, } from 'react-native-reanimated';
+import { PanGestureHandler } from 'react-native-gesture-handler';
+import { usePanGestureHandler } from './redash';
+import { withDecay } from './animationHelpers';
+const GestureHandler = ({ value, max, onValueChange, defaultValue, values, visibleItems, itemHeight }) => {
+ const { gestureHandler, translation, velocity, state, } = usePanGestureHandler();
+ const snapPoints = new Array(max).fill(0).map((_, i) => i * -itemHeight);
+ const translateY = useMemo(() => withDecay({
+ itemHeight,
+ value: translation.y,
+ velocity: velocity.y,
+ state,
+ snapPoints,
+ defaultValue,
+ values,
+ offset: new Value(0),
+ }), [values]);
+ useCode(() => [set(value, add(translateY, itemHeight * Math.floor(visibleItems / 2)))], []);
+ useCode(() => call([translateY], ([currentValue]) => {
+ const selectedIndex = Math.round(-currentValue / itemHeight);
+ const newValue = values[selectedIndex]?.value;
+ if (typeof onValueChange === 'function' && newValue !== null && newValue !== undefined) {
+ onValueChange(newValue);
+ }
+ }), [translateY]);
+ return (
+
+ );
+};
+export default GestureHandler;
diff --git a/src/gestureHandler.tsx b/src/gestureHandler.tsx
index 7fba9af..1ad5a3c 100644
--- a/src/gestureHandler.tsx
+++ b/src/gestureHandler.tsx
@@ -43,7 +43,7 @@ const GestureHandler = ({ value, max, onValueChange, defaultValue, values, visib
values,
offset: new Value(0),
}),
- [values],
+ [values, defaultValue],
);
useCode(() => [set(value, add(translateY, itemHeight * Math.floor(visibleItems / 2)))], []);
@@ -52,7 +52,7 @@ const GestureHandler = ({ value, max, onValueChange, defaultValue, values, visib
const selectedIndex = Math.round(-currentValue / itemHeight);
const newValue = values[selectedIndex]?.value;
- if (typeof onValueChange === 'function' && newValue) {
+ if (typeof onValueChange === 'function' && newValue !== null && newValue !== undefined) {
onValueChange(newValue);
}
}), [translateY]);
diff --git a/src/index.js b/src/index.js
new file mode 100644
index 0000000..43322c5
--- /dev/null
+++ b/src/index.js
@@ -0,0 +1,2 @@
+import Picker from './picker';
+export default Picker;
diff --git a/src/picker.js b/src/picker.js
new file mode 100644
index 0000000..9981a16
--- /dev/null
+++ b/src/picker.js
@@ -0,0 +1,81 @@
+import React, { useMemo } from 'react';
+import { View, StyleSheet, Text } from 'react-native';
+import Animated, { interpolateNode, Extrapolate, sub, divide, useValue, multiply, asin, cos, } from 'react-native-reanimated';
+import { translateZ } from './redash';
+import GestureHandler from './gestureHandler';
+const perspective = 1600;
+const Picker = ({ containerWidth, values, defaultValue, visibleItems, itemHeight, onChange, withTranslateZ, withScale, withOpacity, deviderStyle, labelStyle, }) => {
+ const translateY = useValue(0);
+ const roundedItems = Math.floor(visibleItems / 2);
+ const radiusRel = visibleItems * 0.5;
+ const radius = radiusRel * itemHeight;
+ const renderItems = useMemo(() => (
+ {values.map((v, i) => {
+ const transform = [];
+ const node = divide(sub(translateY, itemHeight * roundedItems), -itemHeight);
+ const opacity = !withOpacity ? 1 : interpolateNode(node, {
+ inputRange: [i - visibleItems, i, i + visibleItems],
+ outputRange: [0.2, 1, 0.2],
+ extrapolate: Extrapolate.CLAMP,
+ });
+ if (withScale) {
+ const scale = interpolateNode(node, {
+ inputRange: [i - visibleItems, i, i + visibleItems],
+ outputRange: [0.5, 1, 0.5],
+ extrapolate: Extrapolate.CLAMP,
+ });
+ transform.push({ scale });
+ }
+ if (withTranslateZ) {
+ transform.push({ perspective });
+ const y = interpolateNode(node, {
+ inputRange: [i - radiusRel, i, i + radiusRel],
+ outputRange: [-1, 0, 1],
+ extrapolate: Extrapolate.CLAMP,
+ });
+ const rotateX = asin(y);
+ transform.push({ rotateX });
+ const z = sub(multiply(radius, cos(rotateX)), radius);
+ transform.push(translateZ(perspective, z));
+ }
+ return (
+ {v.label}
+ );
+ })}
+ ), []);
+ return (
+
+
+
+ {renderItems}
+
+ );
+};
+export default Picker;
+const styles = StyleSheet.create({
+ container: {
+ overflow: 'hidden',
+ },
+ item: {
+ justifyContent: 'center',
+ },
+ label: {
+ color: '#000000',
+ fontWeight: '500',
+ fontSize: 24,
+ textAlign: 'center',
+ textAlignVertical: 'center',
+ },
+});
diff --git a/src/picker.tsx b/src/picker.tsx
index 93466ed..6204d32 100644
--- a/src/picker.tsx
+++ b/src/picker.tsx
@@ -49,7 +49,7 @@ const Picker = ({
const radius = radiusRel * itemHeight;
const renderItems = useMemo(() => (
-
+
{values.map((v, i) => {
const transform = [];
const node = divide(sub(translateY, itemHeight * roundedItems), -itemHeight);
diff --git a/src/redash/index.js b/src/redash/index.js
new file mode 100644
index 0000000..1102260
--- /dev/null
+++ b/src/redash/index.js
@@ -0,0 +1,62 @@
+import { useRef } from "react";
+import Animated from "react-native-reanimated";
+import { State } from "react-native-gesture-handler";
+const { divide, sub, Value, event, multiply, add, min, abs, cond, eq } = Animated;
+export const translateZ = (perspective, z) => ({ scale: divide(perspective, sub(perspective, z)) });
+function useConst(initialValue) {
+ const ref = useRef();
+ if (ref.current === undefined) {
+ // Box the value in an object so we can tell if it's initialized even if the initializer
+ // returns/is undefined
+ ref.current = {
+ value: typeof initialValue === "function"
+ ? // eslint-disable-next-line @typescript-eslint/ban-types
+ initialValue()
+ : initialValue,
+ };
+ }
+ return ref.current.value;
+}
+const create = (x, y) => ({
+ x: x ?? 0,
+ y: y ?? x ?? 0,
+});
+const createValue = (x = 0, y) => create(new Value(x), new Value(y ?? x));
+const onGestureEvent = (nativeEvent) => {
+ const gestureEvent = event([{ nativeEvent }]);
+ return {
+ onHandlerStateChange: gestureEvent,
+ onGestureEvent: gestureEvent,
+ };
+};
+const panGestureHandler = () => {
+ const position = createValue(0);
+ const translation = createValue(0);
+ const velocity = createValue(0);
+ const state = new Value(State.UNDETERMINED);
+ const gestureHandler = onGestureEvent({
+ x: position.x,
+ translationX: translation.x,
+ velocityX: velocity.x,
+ y: position.y,
+ translationY: translation.y,
+ velocityY: velocity.y,
+ state,
+ });
+ return {
+ position,
+ translation,
+ velocity,
+ state,
+ gestureHandler,
+ };
+};
+export const usePanGestureHandler = () => useConst(() => panGestureHandler());
+export const snapPoint = (value, velocity, points) => {
+ const point = add(value, multiply(0.2, velocity));
+ const diffPoint = (p) => abs(sub(point, p));
+ const deltas = points.map((p) => diffPoint(p));
+ // @ts-ignore
+ const minDelta = min(...deltas);
+ return points.reduce((acc, p) => cond(eq(diffPoint(p), minDelta), p, acc), new Value());
+};