Skip to content
Open
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
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -70,4 +70,4 @@
"lib/"
],
"license": "MIT"
}
}
60 changes: 60 additions & 0 deletions src/animationHelpers.js
Original file line number Diff line number Diff line change
@@ -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,
]);
};
32 changes: 32 additions & 0 deletions src/gestureHandler.js
Original file line number Diff line number Diff line change
@@ -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 (<PanGestureHandler {...gestureHandler}>
<Animated.View style={StyleSheet.absoluteFill}/>
</PanGestureHandler>);
};
export default GestureHandler;
4 changes: 2 additions & 2 deletions src/gestureHandler.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)))], []);
Expand All @@ -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]);
Expand Down
2 changes: 2 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import Picker from './picker';
export default Picker;
81 changes: 81 additions & 0 deletions src/picker.js
Original file line number Diff line number Diff line change
@@ -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(() => (<Animated.View style={{ transform: [{ translateY }] }}>
{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 (<Animated.View key={v.value} style={[
styles.item,
{
height: itemHeight,
opacity,
transform,
},
]}>
<Text style={[styles.label, labelStyle]}>{v.label}</Text>
</Animated.View>);
})}
</Animated.View>), []);
return (<View style={[styles.container, { width: containerWidth, height: itemHeight * visibleItems }]}>
<View style={StyleSheet.absoluteFill}>
<View style={[{
top: itemHeight * roundedItems,
height: itemHeight,
},
deviderStyle,
]}/>
</View>
{renderItems}
<GestureHandler values={values} max={values.length} value={translateY} onValueChange={onChange} defaultValue={defaultValue} visibleItems={visibleItems} itemHeight={itemHeight}/>
</View>);
};
export default Picker;
const styles = StyleSheet.create({
container: {
overflow: 'hidden',
},
item: {
justifyContent: 'center',
},
label: {
color: '#000000',
fontWeight: '500',
fontSize: 24,
textAlign: 'center',
textAlignVertical: 'center',
},
});
2 changes: 1 addition & 1 deletion src/picker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ const Picker = ({
const radius = radiusRel * itemHeight;

const renderItems = useMemo(() => (
<Animated.View style={{ transform: [{ translateY }] }}>
<Animated.View style={{ transform: [{ translateY: translateY }] }}>
{values.map((v, i) => {
const transform = [];
const node = divide(sub(translateY, itemHeight * roundedItems), -itemHeight);
Expand Down
62 changes: 62 additions & 0 deletions src/redash/index.js
Original file line number Diff line number Diff line change
@@ -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());
};