react-native-reanimated#useDerivedValue TypeScript Examples
The following examples show how to use
react-native-reanimated#useDerivedValue.
You can vote up the ones you like or vote down the ones you don't like,
and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: AnimatedHelper.ts From curved-bottom-navigation-bar with MIT License | 6 votes |
withSharedTransition = (
value: Animated.SharedValue<boolean>,
config: WithTimingConfig = {
duration: 500,
easing: Easing.bezier(0, 0.55, 0.45, 1),
}
): Animated.SharedValue<number> => {
'worklet';
return useDerivedValue(() =>
value.value ? withTiming(1, config) : withTiming(0, config)
);
}
Example #2
Source File: usePrice.ts From react-native-wagmi-charts with MIT License | 6 votes |
export function useLineChartPrice({
format,
precision = 2,
}: { format?: TFormatterFn<string>; precision?: number } = {}) {
const { currentIndex, data } = useLineChart();
const float = useDerivedValue(() => {
if (typeof currentIndex.value === 'undefined' || currentIndex.value === -1)
return '';
let price = 0;
price = data[currentIndex.value].value;
return price.toFixed(precision).toString();
});
const formatted = useDerivedValue(() => {
let value = float.value || '';
const formattedPrice = value ? formatPrice({ value }) : '';
return format
? format({ value, formatted: formattedPrice })
: formattedPrice;
});
return { value: float, formatted };
}
Example #3
Source File: useDatetime.ts From react-native-wagmi-charts with MIT License | 6 votes |
export function useLineChartDatetime({
format,
locale,
options,
}: {
format?: TFormatterFn<number>;
locale?: string;
options?: Intl.DateTimeFormatOptions;
} = {}) {
const { currentIndex, data } = useLineChart();
const timestamp = useDerivedValue(() => {
if (typeof currentIndex.value === 'undefined' || currentIndex.value === -1)
return '';
return data[currentIndex.value].timestamp;
});
const timestampString = useDerivedValue(() => {
if (timestamp.value === '') return '';
return timestamp.value.toString();
});
const formatted = useDerivedValue(() => {
const formattedDatetime = timestamp.value
? formatDatetime({
value: timestamp.value,
locale,
options,
})
: '';
return format
? format({ value: timestamp.value || -1, formatted: formattedDatetime })
: formattedDatetime;
});
return { value: timestampString, formatted };
}
Example #4
Source File: useCurrentY.ts From react-native-wagmi-charts with MIT License | 6 votes |
export function useCurrentY() {
const { path, width } = useContext(LineChartDimensionsContext);
const { currentX } = useContext(LineChartContext);
const parsedPath = useMemo(() => (path ? parse(path) : undefined), [path]);
const currentY = useDerivedValue(() => {
if (!parsedPath) {
return -1;
}
const boundedX = Math.min(width, currentX.value);
return getYForX(parsedPath, boundedX) || 0;
});
return currentY;
}
Example #5
Source File: usePrice.ts From react-native-wagmi-charts with MIT License | 6 votes |
export function useCandlestickChartPrice({
format,
precision = 2,
type = 'crosshair',
}: {
format?: TFormatterFn<string>;
precision?: number;
type?: TPriceType;
} = {}): {
value: Readonly<Animated.SharedValue<string>>;
formatted: Readonly<Animated.SharedValue<string>>;
} {
const { currentY, domain, height } = useCandlestickChart();
const candle = useCandleData();
const float = useDerivedValue(() => {
let price = 0;
if (type === 'crosshair') {
price = getPrice({
y: currentY.value,
domain: [Math.min(...domain), Math.max(...domain)],
maxHeight: height,
});
} else {
price = candle.value[type];
}
if (price === -1) return '';
return price.toFixed(precision).toString();
});
const formatted = useDerivedValue(() => {
if (!float.value) return '';
const formattedPrice = formatPrice({ value: float.value });
return format
? format({ value: float.value, formatted: formattedPrice })
: formattedPrice;
});
return { value: float, formatted };
}
Example #6
Source File: useDatetime.ts From react-native-wagmi-charts with MIT License | 6 votes |
export function useCandlestickChartDatetime({
format,
locale,
options,
}: {
format?: TFormatterFn<number>;
locale?: string;
options?: { [key: string]: string };
} = {}): {
value: Readonly<Animated.SharedValue<string>>;
formatted: Readonly<Animated.SharedValue<string>>;
} {
const candle = useCandleData();
const timestamp = useDerivedValue(() => {
return candle.value.timestamp;
});
const timestampString = useDerivedValue(() => {
if (timestamp.value === -1) return '';
return timestamp.value.toString();
});
const formatted = useDerivedValue(() => {
if (timestamp.value === -1) return '';
const formattedDatetime = formatDatetime({
value: timestamp.value,
locale,
options,
});
return format
? format({ value: timestamp.value, formatted: formattedDatetime })
: formattedDatetime;
});
return { value: timestampString, formatted };
}
Example #7
Source File: useCandleData.ts From react-native-wagmi-charts with MIT License | 6 votes |
export function useCandleData(): Readonly<Animated.SharedValue<TCandle>> {
const { currentX, data, step } = useCandlestickChart();
const candle = useDerivedValue(() => {
if (currentX.value === -1) {
return { timestamp: -1, low: -1, open: -1, high: -1, close: -1 };
}
return data[Math.floor(currentX.value / step)];
});
return candle;
}
Example #8
Source File: AnimatedHelper.ts From curved-bottom-navigation-bar with MIT License | 6 votes |
useSharedTransition = (
state: boolean,
config: WithTimingConfig = {
duration: 500,
easing: Easing.bezier(0.33, 0.01, 0, 1),
}
): Animated.SharedValue<number> => {
'worklet';
return useDerivedValue(() =>
state ? withTiming(1, config) : withTiming(0, config)
);
}
Example #9
Source File: CrosshairTooltip.tsx From react-native-wagmi-charts with MIT License | 5 votes |
export function CandlestickChartCrosshairTooltip({
children,
xGutter = 8,
yGutter = 8,
tooltipTextProps,
textStyle,
...props
}: CandlestickChartCrosshairTooltipProps) {
const { width, height } = React.useContext(CandlestickChartDimensionsContext);
const { currentY } = useCandlestickChart();
const { position } = React.useContext(
CandlestickChartCrosshairTooltipContext
);
const elementHeight = useSharedValue(0);
const elementWidth = useSharedValue(0);
const handleLayout = React.useCallback(
(event) => {
elementHeight.value = event.nativeEvent.layout.height;
elementWidth.value = event.nativeEvent.layout.width;
},
[elementHeight, elementWidth]
);
const topOffset = useDerivedValue(() => {
let offset = 0;
if (currentY.value < elementHeight.value / 2 + yGutter) {
offset = currentY.value - (elementHeight.value / 2 + yGutter);
} else if (currentY.value + elementHeight.value / 2 > height - yGutter) {
offset = currentY.value + elementHeight.value / 2 - height + yGutter;
}
return offset;
});
const tooltip = useAnimatedStyle(() => ({
backgroundColor: 'white',
position: 'absolute',
display: 'flex',
padding: 4,
}));
const leftTooltip = useAnimatedStyle(() => ({
left: xGutter,
top: -(elementHeight.value / 2) - topOffset.value,
opacity: position.value === 'left' ? 1 : 0,
}));
const rightTooltip = useAnimatedStyle(() => ({
left: width - elementWidth.value - xGutter,
top: -(elementHeight.value / 2) - topOffset.value,
opacity: position.value === 'right' ? 1 : 0,
}));
return (
<>
<Animated.View
onLayout={handleLayout}
{...props}
style={[tooltip, leftTooltip, props.style]}
>
{children || (
<CandlestickChartPriceText
{...tooltipTextProps}
style={[styles.text, tooltipTextProps?.style, textStyle]}
/>
)}
</Animated.View>
<Animated.View {...props} style={[tooltip, rightTooltip, props.style]}>
{children || (
<CandlestickChartPriceText
{...tooltipTextProps}
style={[styles.text, tooltipTextProps?.style, textStyle]}
/>
)}
</Animated.View>
</>
);
}
Example #10
Source File: HorizontalLine.tsx From react-native-wagmi-charts with MIT License | 5 votes |
export function LineChartHorizontalLine({
color = 'gray',
lineProps = {},
at = { index: 0 },
offsetY = 0,
}: HorizontalLineProps) {
const { width, path, height, gutter } = React.useContext(
LineChartDimensionsContext
);
const { data, yDomain } = useLineChart();
const parsedPath = React.useMemo(() => parse(path), [path]);
const pointWidth = React.useMemo(
() => width / data.length,
[data.length, width]
);
const y = useDerivedValue(() => {
if (typeof at === 'number' || at.index != null) {
const index = typeof at === 'number' ? at : at.index;
const yForX = getYForX(parsedPath!, pointWidth * index) || 0;
return withTiming(yForX + offsetY);
}
/**
* <gutter>
* | ---------- | <- yDomain.max |
* | | | offsetTop
* | | <- value |
* | |
* | | <- yDomain.min
* <gutter>
*/
const offsetTop = yDomain.max - at.value;
const percentageOffsetTop = offsetTop / (yDomain.max - yDomain.min);
const heightBetweenGutters = height - gutter * 2;
const offsetTopPixels = gutter + percentageOffsetTop * heightBetweenGutters;
return withTiming(offsetTopPixels + offsetY);
});
const lineAnimatedProps = useAnimatedProps(() => ({
x1: 0,
x2: width,
y1: y.value,
y2: y.value,
}));
return (
<AnimatedLine
animatedProps={lineAnimatedProps}
strokeWidth={2}
stroke={color}
strokeDasharray="3 3"
{...lineProps}
/>
);
}
Example #11
Source File: AnimatedHelper.ts From curved-bottom-navigation-bar with MIT License | 5 votes |
sharedEq = (
v1: Animated.SharedValue<number | string>,
v2: Animated.SharedValue<number | string>
) => {
'worklet';
return useDerivedValue(() => v1.value === v2.value);
}
Example #12
Source File: AnimatedHelper.ts From curved-bottom-navigation-bar with MIT License | 5 votes |
useInterpolate = (
progress: Animated.SharedValue<number>,
input: number[],
output: number[],
type?: Animated.Extrapolate
) => useDerivedValue(() => interpolate(progress.value, input, output, type))
Example #13
Source File: Pager.tsx From react-native-gallery-toolkit with MIT License | 5 votes |
Page = React.memo(
({
pagerRefs,
item,
onPageStateChange,
gutterWidth,
index,
length,
renderPage,
shouldRenderGutter,
getPageTranslate,
width,
currentIndex,
isPagerInProgress,
}: PageProps) => {
const isPageActive = useDerivedValue(() => {
return currentIndex.value === index;
}, []);
return (
<View
style={{
flex: 1,
position: 'absolute',
top: 0,
bottom: 0,
left: -getPageTranslate(index),
}}
>
<View
style={[
{
flex: 1,
width,
justifyContent: 'center',
alignItems: 'center',
},
]}
>
{renderPage(
{
index,
pagerRefs,
onPageStateChange,
item,
width,
isPageActive,
isPagerInProgress,
},
index,
)}
</View>
{index !== length - 1 && shouldRenderGutter && (
<Gutter width={gutterWidth} />
)}
</View>
);
},
)
Example #14
Source File: ButtonTabItem.tsx From curved-bottom-navigation-bar with MIT License | 4 votes |
ButtonTabItemComponent = (props: TabBarItemProps) => {
// props
const {
index,
selectedIndex,
countTab,
indexAnimated,
width,
icon,
renderTitle,
title,
titleShown,
focused,
} = props;
// reanimated
const { bottom } = useSafeAreaInsets();
const isActive = useDerivedValue(() => sharedRound(indexAnimated.value));
const progress = useSharedValue(0);
const opacity = useInterpolate(progress, [0, 0.8], [1, 0]);
const translateY = useInterpolate(progress, [0, 0.4], [0, 10]);
const scale = useInterpolate(progress, [0, 1], [1, 0.5]);
// func
const _onPress = useCallback(() => {
selectedIndex.value = index;
}, [index, selectedIndex]);
// effect
useAnimatedReaction(
() => isActive.value === index,
(result, prevValue) => {
if (result !== prevValue) {
progress.value = sharedTiming(result ? 1 : 0);
}
}
);
// reanimated style
const containerIconStyle = useAnimatedStyle(() => ({
opacity: opacity.value,
justifyContent: 'center',
alignItems: 'center',
transform: [
{
translateY: translateY.value,
},
],
}));
const titleStyle = useAnimatedStyle(() => ({
transform: [{ scale: scale.value }],
}));
const buttonTab = useMemo(
() => ({
width: width / countTab,
paddingBottom: bottom,
}),
[width, countTab, bottom]
);
// render
const renderIcon = useCallback(() => {
return icon({ progress, focused });
}, [focused, icon, progress]);
const _renderTitle = useCallback(() => {
return renderTitle?.({ progress, focused, title: title ?? '' });
}, [focused, progress, renderTitle, title]);
const showTitle = useCallback(() => {
if (typeof renderTitle === 'function') {
return _renderTitle();
}
return (
<Animated.Text
style={[styles.title, titleStyle]}
allowFontScaling={false}
numberOfLines={1}
>
{title ?? ''}
</Animated.Text>
);
}, [_renderTitle, renderTitle, title, titleStyle]);
// render
return (
<TouchableOpacity onPress={_onPress} activeOpacity={0.7}>
<View style={[styles.buttonTab, buttonTab]}>
<Animated.View style={[containerIconStyle]}>
{renderIcon()}
{titleShown ? showTitle() : null}
</Animated.View>
</View>
</TouchableOpacity>
);
}
Example #15
Source File: CurvedTabBar.tsx From curved-bottom-navigation-bar with MIT License | 4 votes |
CurvedTabBarComponent = (props: TabBarViewProps) => {
// props
const {
routes,
selectedIndex,
barWidth,
duration,
dotColor,
tabBarColor,
titleShown,
isRtl,
navigationIndex,
dotSize: SIZE_DOT,
barHeight = TAB_BAR_HEIGHT,
} = props;
// state
const { bottom } = useSafeAreaInsets();
const { width } = useSafeAreaFrame();
const actualBarWidth = useMemo<number>(
() => barWidth || width,
[barWidth, width]
);
const widthTab = useMemo(
() => actualBarWidth / routes.length,
[routes, actualBarWidth]
);
const inputRange = useMemo(
() =>
isRtl
? routes.map((_: any, index: number) => index).reverse()
: routes.map((_: any, index: number) => index),
[isRtl, routes]
);
const outputRange = useMemo(
() =>
routes.map(
(_: any, index: number) => (index / routes.length) * actualBarWidth
),
[routes, actualBarWidth]
);
const actualBarHeight = useMemo<number>(
() => barHeight + bottom,
[barHeight, bottom]
);
const indexAnimated = useDerivedValue(() =>
sharedTiming(selectedIndex.value, { duration })
);
// func
const renderButtonTab = useCallback(
({ key, title, ...configs }: TabRoute, index: number) => {
return (
<ButtonTab
focused={index === selectedIndex.value}
width={actualBarWidth}
key={key}
title={title}
titleShown={titleShown}
indexAnimated={indexAnimated}
countTab={routes.length}
selectedIndex={selectedIndex}
index={index}
{...configs}
/>
);
},
[indexAnimated, routes.length, selectedIndex, titleShown, actualBarWidth]
);
// reanimated
const progress = withSharedTransition(sharedEq(selectedIndex, indexAnimated));
const xPath = useInterpolate(indexAnimated, inputRange, outputRange);
// path
const pathProps = useAnimatedProps<PathProps>(() => {
const centerHoleX = xPath.value + widthTab / 2;
return {
d: `M0,0 L${centerHoleX - SIZE_DOT},0
C${centerHoleX - SIZE_DOT * 0.5},0 ${
centerHoleX - SIZE_DOT * 0.75
},${HEIGHT_HOLE} ${centerHoleX},${HEIGHT_HOLE}
C${centerHoleX + SIZE_DOT * 0.75},${HEIGHT_HOLE} ${
centerHoleX + SIZE_DOT * 0.5
},0 ${centerHoleX + SIZE_DOT} 0
L${actualBarWidth * 2},0 L ${
actualBarWidth * 2
},${actualBarHeight} L 0,${actualBarHeight} Z
`,
};
}, [actualBarWidth, widthTab, SIZE_DOT, actualBarHeight]);
// style
const containerStyle = useMemo<StyleProp<ViewStyle>>(
() => [
{
height: actualBarHeight,
width: actualBarWidth,
},
],
[actualBarHeight, actualBarWidth]
);
const rowTab = useMemo<StyleProp<ViewStyle>>(
() => [
{
width: actualBarWidth,
height: actualBarHeight,
},
],
[actualBarHeight, actualBarWidth]
);
return (
<>
<RNShadow style={[styles.container, containerStyle]}>
<Svg
width={actualBarWidth}
height={actualBarHeight}
style={[styles.svg]}
>
<AnimatedPath
animatedProps={pathProps}
translateY={3}
fill={tabBarColor}
stroke={'transparent'}
strokeWidth={0}
/>
</Svg>
</RNShadow>
<View style={[styles.rowTab, rowTab]}>
<Dot
navigationIndex={navigationIndex}
isRtl={isRtl}
dotColor={dotColor}
dotSize={SIZE_DOT}
barHeight={actualBarHeight}
width={actualBarWidth}
selectedIndex={indexAnimated}
routes={routes}
progress={progress}
/>
{routes.map(renderButtonTab)}
</View>
</>
);
}
Example #16
Source File: ButtonTabItem.tsx From curved-bottom-navigation-bar with MIT License | 4 votes |
ButtonTabItemComponent = (props: TabBarItemProps) => {
// props
const {
index,
selectedIndex,
countTab,
indexAnimated,
width,
icon,
renderTitle,
title,
titleShown,
focused,
} = props;
// reanimated
const {bottom} = useSafeAreaInsets();
const isActive = useDerivedValue(() => sharedRound(indexAnimated.value));
const progress = useSharedValue(0);
const opacity = useInterpolate(progress, [0, 0.8], [1, 0]);
const translateY = useInterpolate(progress, [0, 0.4], [0, 10]);
const scale = useInterpolate(progress, [0, 1], [1, 0.5]);
// func
const _onPress = useCallback(() => {
selectedIndex.value = index;
}, [index, selectedIndex]);
// effect
useAnimatedReaction(
() => isActive.value === index,
(result, prevValue) => {
if (result !== prevValue) {
progress.value = sharedTiming(result ? 1 : 0);
}
},
);
// reanimated style
const containerIconStyle = useAnimatedStyle(() => ({
opacity: opacity.value,
justifyContent: 'center',
alignItems: 'center',
transform: [
{
translateY: translateY.value,
},
],
}));
const titleStyle = useAnimatedStyle(() => ({
transform: [{scale: scale.value}],
}));
const buttonTab = useMemo(
() => ({
width: width / countTab,
paddingBottom: bottom,
}),
[width, countTab, bottom],
);
// render
const renderIcon = useCallback(() => {
return icon({progress, focused});
}, [focused, icon, progress]);
const _renderTitle = useCallback(() => {
return renderTitle?.({progress, focused, title: title ?? ''});
}, [focused, progress, renderTitle, title]);
const showTitle = useCallback(() => {
if (typeof renderTitle === 'function') {
return _renderTitle();
}
return (
<Animated.Text
style={[styles.title, titleStyle]}
allowFontScaling={false}
numberOfLines={1}>
{title ?? ''}
</Animated.Text>
);
}, [_renderTitle, renderTitle, title, titleStyle]);
// render
return (
<TouchableOpacity onPress={_onPress} activeOpacity={0.7}>
<View style={[styles.buttonTab, buttonTab]}>
<Animated.View style={[containerIconStyle]}>
{renderIcon()}
{titleShown ? showTitle() : null}
</Animated.View>
</View>
</TouchableOpacity>
);
}
Example #17
Source File: CurvedTabBar.tsx From curved-bottom-navigation-bar with MIT License | 4 votes |
CurvedTabBarComponent = (props: TabBarViewProps) => {
// props
const {
routes,
selectedIndex,
barWidth,
duration,
dotColor,
tabBarColor,
titleShown,
isRtl,
navigationIndex,
dotSize: SIZE_DOT,
barHeight = TAB_BAR_HEIGHT,
} = props;
// state
const {bottom} = useSafeAreaInsets();
const {width} = useSafeAreaFrame();
const actualBarWidth = useMemo<number>(
() => barWidth || width,
[barWidth, width],
);
const widthTab = useMemo(
() => actualBarWidth / routes.length,
[routes, actualBarWidth],
);
const inputRange = useMemo(
() =>
isRtl
? routes.map((_: any, index: number) => index).reverse()
: routes.map((_: any, index: number) => index),
[isRtl, routes],
);
const outputRange = useMemo(
() =>
routes.map(
(_: any, index: number) => (index / routes.length) * actualBarWidth,
),
[routes, actualBarWidth],
);
const actualBarHeight = useMemo<number>(
() => barHeight + bottom,
[barHeight, bottom],
);
const indexAnimated = useDerivedValue(() =>
sharedTiming(selectedIndex.value, {duration}),
);
// func
const renderButtonTab = useCallback(
({key, title, ...configs}: TabRoute, index: number) => {
return (
<ButtonTab
focused={index === selectedIndex.value}
width={actualBarWidth}
key={key}
title={title}
titleShown={titleShown}
indexAnimated={indexAnimated}
countTab={routes.length}
selectedIndex={selectedIndex}
index={index}
{...configs}
/>
);
},
[indexAnimated, routes.length, selectedIndex, titleShown, actualBarWidth],
);
// reanimated
const progress = withSharedTransition(sharedEq(selectedIndex, indexAnimated));
const xPath = useInterpolate(indexAnimated, inputRange, outputRange);
// path
const pathProps = useAnimatedProps<PathProps>(() => {
const centerHoleX = xPath.value + widthTab / 2;
return {
d: `M0,0 L${centerHoleX - SIZE_DOT},0
C${centerHoleX - SIZE_DOT * 0.5},0 ${
centerHoleX - SIZE_DOT * 0.75
},${HEIGHT_HOLE} ${centerHoleX},${HEIGHT_HOLE}
C${centerHoleX + SIZE_DOT * 0.75},${HEIGHT_HOLE} ${
centerHoleX + SIZE_DOT * 0.5
},0 ${centerHoleX + SIZE_DOT} 0
L${actualBarWidth * 2},0 L ${
actualBarWidth * 2
},${actualBarHeight} L 0,${actualBarHeight} Z
`,
};
}, [actualBarWidth, widthTab, SIZE_DOT, actualBarHeight]);
// style
const containerStyle = useMemo<StyleProp<ViewStyle>>(
() => [
{
height: actualBarHeight,
width: actualBarWidth,
},
],
[actualBarHeight, actualBarWidth],
);
const rowTab = useMemo<StyleProp<ViewStyle>>(
() => [
{
width: actualBarWidth,
height: actualBarHeight,
},
],
[actualBarHeight, actualBarWidth],
);
return (
<>
<RNShadow style={[styles.container, containerStyle]}>
<Svg
width={actualBarWidth}
height={actualBarHeight}
style={[styles.svg]}>
<AnimatedPath
animatedProps={pathProps}
translateY={3}
fill={tabBarColor}
stroke={'transparent'}
strokeWidth={0}
/>
</Svg>
</RNShadow>
<View style={[styles.rowTab, rowTab]}>
<Dot
navigationIndex={navigationIndex}
isRtl={isRtl}
dotColor={dotColor}
dotSize={SIZE_DOT}
barHeight={actualBarHeight}
width={actualBarWidth}
selectedIndex={indexAnimated}
routes={routes}
progress={progress}
/>
{routes.map(renderButtonTab)}
</View>
</>
);
}
Example #18
Source File: Dot.tsx From react-native-wagmi-charts with MIT License | 4 votes |
export function LineChartDot({
at,
color: defaultColor = 'black',
dotProps,
hasOuterDot: defaultHasOuterDot = false,
hasPulse = false,
inactiveColor,
outerDotProps,
pulseBehaviour = 'while-inactive',
pulseDurationMs = 800,
showInactiveColor = true,
size = 4,
outerSize = size * 4,
}: LineChartDotProps) {
const { data, isActive } = useLineChart();
const { path, pathWidth: width } = React.useContext(
LineChartDimensionsContext
);
////////////////////////////////////////////////////////////
const { isInactive: _isInactive } = React.useContext(LineChartPathContext);
const isInactive = showInactiveColor && _isInactive;
const color = isInactive ? inactiveColor || defaultColor : defaultColor;
const opacity = isInactive && !inactiveColor ? 0.5 : 1;
const hasOuterDot = defaultHasOuterDot || hasPulse;
////////////////////////////////////////////////////////////
const parsedPath = React.useMemo(() => parse(path), [path]);
////////////////////////////////////////////////////////////
const pointWidth = React.useMemo(
() => width / (data.length - 1),
[data.length, width]
);
////////////////////////////////////////////////////////////
const x = useDerivedValue(() => withTiming(pointWidth * at));
const y = useDerivedValue(() =>
withTiming(getYForX(parsedPath!, x.value) || 0)
);
////////////////////////////////////////////////////////////
const animatedDotProps = useAnimatedProps(() => ({
cx: x.value,
cy: y.value,
}));
const animatedOuterDotProps = useAnimatedProps(() => {
let defaultProps = {
cx: x.value,
cy: y.value,
opacity: 0.1,
r: outerSize,
};
if (!hasPulse) {
return defaultProps;
}
if (isActive.value && pulseBehaviour === 'while-inactive') {
return {
...defaultProps,
r: 0,
};
}
const easing = Easing.out(Easing.sin);
const animatedOpacity = withRepeat(
withSequence(
withTiming(0.8),
withTiming(0, {
duration: pulseDurationMs,
easing,
})
),
-1,
false
);
const scale = withRepeat(
withSequence(
withTiming(0),
withTiming(outerSize, {
duration: pulseDurationMs,
easing,
})
),
-1,
false
);
if (pulseBehaviour === 'while-inactive') {
return {
...defaultProps,
opacity: isActive.value ? withTiming(0) : animatedOpacity,
r: isActive.value ? withTiming(0) : scale,
};
}
return {
...defaultProps,
opacity: animatedOpacity,
r: scale,
};
}, [outerSize]);
////////////////////////////////////////////////////////////
return (
<>
<AnimatedCircle
animatedProps={animatedDotProps}
r={size}
fill={color}
opacity={opacity}
{...dotProps}
/>
{hasOuterDot && (
<AnimatedCircle
animatedProps={animatedOuterDotProps}
fill={color}
{...outerDotProps}
/>
)}
</>
);
}
Example #19
Source File: ProgressBar.tsx From jellyfin-audio-player with MIT License | 4 votes |
function ProgressBar() {
const { position, buffered, duration } = useProgress();
const width = useSharedValue(0);
const pos = useSharedValue(0);
const buf = useSharedValue(0);
const dur = useSharedValue(0);
const isDragging = useSharedValue(false);
const offset = useSharedValue(0);
const bufferAnimation = useDerivedValue(() => {
return calculateProgressTranslation(buf.value, dur.value, width.value);
}, [[dur, buf, width.value]]);
const progressAnimation = useDerivedValue(() => {
if (isDragging.value) {
return calculateProgressTranslation(offset.value, width.value, width.value);
} else {
return calculateProgressTranslation(pos.value, dur.value, width.value);
}
});
const timePassed = useDerivedValue(() => {
if (isDragging.value) {
const currentPosition = (offset.value - DRAG_HANDLE_SIZE / 2) / (width.value - DRAG_HANDLE_SIZE) * dur.value;
return getMinutes(currentPosition) + ':' + getSeconds(currentPosition);
} else {
return getMinutes(pos.value) + ':' + getSeconds(pos.value);
}
}, [pos]);
const timeRemaining = useDerivedValue(() => {
if (isDragging.value) {
const currentPosition = (offset.value - DRAG_HANDLE_SIZE / 2) / (width.value - DRAG_HANDLE_SIZE) * dur.value;
const remaining = (currentPosition - dur.value) * -1;
return `-${getMinutes(remaining)}:${getSeconds((remaining))}`;
} else {
const remaining = (pos.value - dur.value) * -1;
return `-${getMinutes(remaining)}:${getSeconds((remaining))}`;
}
}, [pos, dur]);
const pan = Gesture.Pan()
.minDistance(1)
.activeOffsetX(1)
.activeOffsetY(1)
.onBegin((e) => {
isDragging.value = true;
offset.value = Math.min(Math.max(DRAG_HANDLE_SIZE / 2, e.x), width.value - DRAG_HANDLE_SIZE / 2);
}).onUpdate((e) => {
offset.value = Math.min(Math.max(DRAG_HANDLE_SIZE / 2, e.x), width.value - DRAG_HANDLE_SIZE / 2);
}).onFinalize(() => {
pos.value = (offset.value - DRAG_HANDLE_SIZE / 2) / (width.value - DRAG_HANDLE_SIZE) * dur.value;
isDragging.value = false;
runOnJS(TrackPlayer.seekTo)(pos.value);
});
const tap = Gesture.Tap()
.onBegin((e) => {
isDragging.value = true;
offset.value = Math.min(Math.max(DRAG_HANDLE_SIZE / 2, e.x), width.value - DRAG_HANDLE_SIZE / 2);
}).onFinalize(() => {
pos.value = (offset.value - DRAG_HANDLE_SIZE / 2) / (width.value - DRAG_HANDLE_SIZE) * dur.value;
isDragging.value = false;
runOnJS(TrackPlayer.seekTo)(pos.value);
});
const gesture = Gesture.Exclusive(pan, tap);
useEffect(() => {
pos.value = position;
buf.value = buffered;
dur.value = duration;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [position, buffered, duration]);
const dragHandleStyles = useAnimatedStyle(() => {
return {
transform: [
{ translateX: offset.value },
{
scale: withTiming(isDragging.value ? 1 : 0, {
duration: 100,
easing: Easing.out(Easing.ease),
})
}
],
};
});
const bufferStyles = useAnimatedStyle(() => ({
transform: [
{ translateX: bufferAnimation.value }
]
}));
const progressStyles = useAnimatedStyle(() => {
return {
transform: [
{ translateX: progressAnimation.value }
]
};
});
const timePassedStyles = useAnimatedStyle(() => {
return {
transform: [
{ translateY: withTiming(isDragging.value && offset.value < 48 ? 12 : 0, {
duration: 145,
easing: Easing.ease
}) },
],
};
});
const timeRemainingStyles = useAnimatedStyle(() => {
return {
transform: [
{ translateY: withTiming(isDragging.value && offset.value > width.value - 48 ? 12 : 0, {
duration: 150,
easing: Easing.ease
}) },
],
};
});
return (
<GestureDetector gesture={gesture}>
<Container onLayout={(e) => { width.value = e.nativeEvent.layout.width; }}>
<ProgressTrackContainer>
<ProgressTrack
opacity={0.15}
/>
<ProgressTrack
style={bufferStyles}
opacity={0.15}
/>
<ProgressTrack
style={progressStyles}
/>
</ProgressTrackContainer>
<DragHandle style={dragHandleStyles} />
<NumberBar style={{ flex: 1 }}>
<Number text={timePassed} style={timePassedStyles} />
<Number text={timeRemaining} style={timeRemainingStyles} />
</NumberBar>
</Container>
</GestureDetector>
);
}
Example #20
Source File: ImageTransformer.tsx From react-native-gallery-toolkit with MIT License | 4 votes |
ImageTransformer = React.memo<ImageTransformerProps>(
({
outerGestureHandlerRefs = [],
source,
width,
height,
onStateChange = workletNoop,
renderImage,
windowDimensions = Dimensions.get('window'),
isActive,
outerGestureHandlerActive,
style,
onTap = workletNoop,
onDoubleTap = workletNoop,
onInteraction = workletNoop,
DOUBLE_TAP_SCALE = 3,
MAX_SCALE = 3,
MIN_SCALE = 0.7,
OVER_SCALE = 0.5,
timingConfig = defaultTimingConfig,
springConfig = defaultSpringConfig,
enabled = true,
ImageComponent = Image,
}) => {
// fixGestureHandler();
assertWorklet(onStateChange);
assertWorklet(onTap);
assertWorklet(onDoubleTap);
assertWorklet(onInteraction);
if (typeof source === 'undefined') {
throw new Error(
'ImageTransformer: either source or uri should be passed to display an image',
);
}
const imageSource = useMemo(
() =>
typeof source === 'string'
? {
uri: source,
}
: source,
[source],
);
const interactionsEnabled = useSharedValue(false);
const setInteractionsEnabled = useCallback((value: boolean) => {
interactionsEnabled.value = value;
}, []);
const onLoadImageSuccess = useCallback(() => {
setInteractionsEnabled(true);
}, []);
// HACK ALERT
// we disable pinch handler in order to trigger onFinish
// in case user releases one finger
const [pinchEnabled, setPinchEnabledState] = useState(true);
useEffect(() => {
if (!pinchEnabled) {
setPinchEnabledState(true);
}
}, [pinchEnabled]);
const disablePinch = useCallback(() => {
setPinchEnabledState(false);
}, []);
// HACK ALERT END
const pinchRef = useRef(null);
const panRef = useRef(null);
const tapRef = useRef(null);
const doubleTapRef = useRef(null);
const panState = useSharedValue<State>(State.UNDETERMINED);
const pinchState = useSharedValue<State>(State.UNDETERMINED);
const scale = useSharedValue(1);
const scaleOffset = useSharedValue(1);
const translation = vectors.useSharedVector(0, 0);
const panVelocity = vectors.useSharedVector(0, 0);
const scaleTranslation = vectors.useSharedVector(0, 0);
const offset = vectors.useSharedVector(0, 0);
const canvas = useMemo(
() =>
vectors.create(
windowDimensions.width,
windowDimensions.height,
),
[windowDimensions.width, windowDimensions.height],
);
const targetWidth = windowDimensions.width;
const scaleFactor = width / targetWidth;
const targetHeight = height / scaleFactor;
const image = useMemo(
() => vectors.create(targetWidth, targetHeight),
[targetHeight, targetWidth],
);
const canPanVertically = useDerivedValue(() => {
return windowDimensions.height < targetHeight * scale.value;
}, []);
const resetSharedState = useWorkletCallback(
(animated?: boolean) => {
'worklet';
if (animated) {
scale.value = withTiming(1, timingConfig);
scaleOffset.value = 1;
vectors.set(offset, () => withTiming(0, timingConfig));
} else {
scale.value = 1;
scaleOffset.value = 1;
vectors.set(translation, 0);
vectors.set(scaleTranslation, 0);
vectors.set(offset, 0);
}
},
[timingConfig],
);
const maybeRunOnEnd = useWorkletCallback(() => {
'worklet';
const target = vectors.create(0, 0);
const fixedScale = clamp(MIN_SCALE, scale.value, MAX_SCALE);
const scaledImage = image.y * fixedScale;
const rightBoundary = (canvas.x / 2) * (fixedScale - 1);
let topBoundary = 0;
if (canvas.y < scaledImage) {
topBoundary = Math.abs(scaledImage - canvas.y) / 2;
}
const maxVector = vectors.create(rightBoundary, topBoundary);
const minVector = vectors.invert(maxVector);
if (!canPanVertically.value) {
offset.y.value = withSpring(target.y, springConfig);
}
// we should handle this only if pan or pinch handlers has been used already
if (
(checkIsNotUsed(panState) || checkIsNotUsed(pinchState)) &&
pinchState.value !== State.CANCELLED
) {
return;
}
if (
vectors.eq(offset, 0) &&
vectors.eq(translation, 0) &&
vectors.eq(scaleTranslation, 0) &&
scale.value === 1
) {
// we don't need to run any animations
return;
}
if (scale.value <= 1) {
// just center it
vectors.set(offset, () => withTiming(0, timingConfig));
return;
}
vectors.set(
target,
vectors.clamp(offset, minVector, maxVector),
);
const deceleration = 0.9915;
const isInBoundaryX = target.x === offset.x.value;
const isInBoundaryY = target.y === offset.y.value;
if (isInBoundaryX) {
if (
Math.abs(panVelocity.x.value) > 0 &&
scale.value <= MAX_SCALE
) {
offset.x.value = withDecay({
velocity: panVelocity.x.value,
clamp: [minVector.x, maxVector.x],
deceleration,
});
}
} else {
offset.x.value = withSpring(target.x, springConfig);
}
if (isInBoundaryY) {
if (
Math.abs(panVelocity.y.value) > 0 &&
scale.value <= MAX_SCALE &&
offset.y.value !== minVector.y &&
offset.y.value !== maxVector.y
) {
offset.y.value = withDecay({
velocity: panVelocity.y.value,
clamp: [minVector.y, maxVector.y],
deceleration,
});
}
} else {
offset.y.value = withSpring(target.y, springConfig);
}
}, [
MIN_SCALE,
MAX_SCALE,
image,
canvas,
springConfig,
timingConfig,
]);
const onPanEvent = useAnimatedGestureHandler<
PanGestureHandlerGestureEvent,
{
panOffset: vectors.Vector<number>;
pan: vectors.Vector<number>;
}
>({
onInit: (_, ctx) => {
ctx.panOffset = vectors.create(0, 0);
},
shouldHandleEvent: () => {
return (
scale.value > 1 &&
(typeof outerGestureHandlerActive !== 'undefined'
? !outerGestureHandlerActive.value
: true) &&
interactionsEnabled.value
);
},
beforeEach: (evt, ctx) => {
ctx.pan = vectors.create(evt.translationX, evt.translationY);
const velocity = vectors.create(evt.velocityX, evt.velocityY);
vectors.set(panVelocity, velocity);
},
onStart: (_, ctx) => {
cancelAnimation(offset.x);
cancelAnimation(offset.y);
ctx.panOffset = vectors.create(0, 0);
onInteraction('pan');
},
onActive: (evt, ctx) => {
panState.value = evt.state;
if (scale.value > 1) {
if (evt.numberOfPointers > 1) {
// store pan offset during the pan with two fingers (during the pinch)
vectors.set(ctx.panOffset, ctx.pan);
} else {
// subtract the offset and assign fixed pan
const nextTranslate = vectors.add(
ctx.pan,
vectors.invert(ctx.panOffset),
);
translation.x.value = nextTranslate.x;
if (canPanVertically.value) {
translation.y.value = nextTranslate.y;
}
}
}
},
onEnd: (evt, ctx) => {
panState.value = evt.state;
vectors.set(ctx.panOffset, 0);
vectors.set(offset, vectors.add(offset, translation));
vectors.set(translation, 0);
maybeRunOnEnd();
vectors.set(panVelocity, 0);
},
});
useAnimatedReaction(
() => {
if (typeof isActive === 'undefined') {
return true;
}
return isActive.value;
},
(currentActive) => {
if (!currentActive) {
resetSharedState();
}
},
);
const onScaleEvent = useAnimatedGestureHandler<
PinchGestureHandlerGestureEvent,
{
origin: vectors.Vector<number>;
adjustFocal: vectors.Vector<number>;
gestureScale: number;
nextScale: number;
}
>({
onInit: (_, ctx) => {
ctx.origin = vectors.create(0, 0);
ctx.gestureScale = 1;
ctx.adjustFocal = vectors.create(0, 0);
},
shouldHandleEvent: (evt) => {
return (
evt.numberOfPointers === 2 &&
(typeof outerGestureHandlerActive !== 'undefined'
? !outerGestureHandlerActive.value
: true) &&
interactionsEnabled.value
);
},
beforeEach: (evt, ctx) => {
// calculate the overall scale value
// also limits this.event.scale
ctx.nextScale = clamp(
evt.scale * scaleOffset.value,
MIN_SCALE,
MAX_SCALE + OVER_SCALE,
);
if (
ctx.nextScale > MIN_SCALE &&
ctx.nextScale < MAX_SCALE + OVER_SCALE
) {
ctx.gestureScale = evt.scale;
}
// this is just to be able to use with vectors
const focal = vectors.create(evt.focalX, evt.focalY);
const CENTER = vectors.divide(canvas, 2);
// since it works even when you release one finger
if (evt.numberOfPointers === 2) {
// focal with translate offset
// it alow us to scale into different point even then we pan the image
ctx.adjustFocal = vectors.sub(
focal,
vectors.add(CENTER, offset),
);
} else if (
evt.state === State.ACTIVE &&
evt.numberOfPointers !== 2
) {
runOnJS(disablePinch)();
}
},
afterEach: (evt, ctx) => {
if (
evt.state === State.END ||
evt.state === State.CANCELLED
) {
return;
}
scale.value = ctx.nextScale;
},
onStart: (_, ctx) => {
onInteraction('scale');
cancelAnimation(offset.x);
cancelAnimation(offset.y);
vectors.set(ctx.origin, ctx.adjustFocal);
},
onActive: (evt, ctx) => {
pinchState.value = evt.state;
const pinch = vectors.sub(ctx.adjustFocal, ctx.origin);
const nextTranslation = vectors.add(
pinch,
ctx.origin,
vectors.multiply(-1, ctx.gestureScale, ctx.origin),
);
vectors.set(scaleTranslation, nextTranslation);
},
onFinish: (evt, ctx) => {
// reset gestureScale value
ctx.gestureScale = 1;
pinchState.value = evt.state;
// store scale value
scaleOffset.value = scale.value;
vectors.set(offset, vectors.add(offset, scaleTranslation));
vectors.set(scaleTranslation, 0);
if (scaleOffset.value < 1) {
// make sure we don't add stuff below the 1
scaleOffset.value = 1;
// this runs the timing animation
scale.value = withTiming(1, timingConfig);
} else if (scaleOffset.value > MAX_SCALE) {
scaleOffset.value = MAX_SCALE;
scale.value = withTiming(MAX_SCALE, timingConfig);
}
maybeRunOnEnd();
},
});
const onTapEvent = useAnimatedGestureHandler({
shouldHandleEvent: (evt) => {
return (
evt.numberOfPointers === 1 &&
(typeof outerGestureHandlerActive !== 'undefined'
? !outerGestureHandlerActive.value
: true) &&
interactionsEnabled.value
);
},
onStart: () => {
cancelAnimation(offset.x);
cancelAnimation(offset.y);
},
onActive: () => {
onTap(scale.value > 1);
},
onEnd: () => {
maybeRunOnEnd();
},
});
const handleScaleTo = useWorkletCallback(
(x: number, y: number) => {
'worklet';
scale.value = withTiming(DOUBLE_TAP_SCALE, timingConfig);
scaleOffset.value = DOUBLE_TAP_SCALE;
const targetImageSize = vectors.multiply(
image,
DOUBLE_TAP_SCALE,
);
const CENTER = vectors.divide(canvas, 2);
const imageCenter = vectors.divide(image, 2);
const focal = vectors.create(x, y);
const origin = vectors.multiply(
-1,
vectors.sub(vectors.divide(targetImageSize, 2), CENTER),
);
const koef = vectors.sub(
vectors.multiply(vectors.divide(1, imageCenter), focal),
1,
);
const target = vectors.multiply(origin, koef);
if (targetImageSize.y < canvas.y) {
target.y = 0;
}
offset.x.value = withTiming(target.x, timingConfig);
offset.y.value = withTiming(target.y, timingConfig);
},
[DOUBLE_TAP_SCALE, timingConfig, image, canvas],
);
const onDoubleTapEvent = useAnimatedGestureHandler<
TapGestureHandlerGestureEvent,
{}
>({
shouldHandleEvent: (evt) => {
return (
evt.numberOfPointers === 1 &&
(typeof outerGestureHandlerActive !== 'undefined'
? !outerGestureHandlerActive.value
: true) &&
interactionsEnabled.value
);
},
onActive: ({ x, y }) => {
onDoubleTap(scale.value > 1);
if (scale.value > 1) {
resetSharedState(true);
} else {
handleScaleTo(x, y);
}
},
});
const animatedStyles = useAnimatedStyle<ViewStyle>(() => {
const noOffset = offset.x.value === 0 && offset.y.value === 0;
const noTranslation =
translation.x.value === 0 && translation.y.value === 0;
const noScaleTranslation =
scaleTranslation.x.value === 0 &&
scaleTranslation.y.value === 0;
const isInactive =
scale.value === 1 &&
noOffset &&
noTranslation &&
noScaleTranslation;
onStateChange(isInactive);
return {
transform: [
{
translateX:
scaleTranslation.x.value +
translation.x.value +
offset.x.value,
},
{
translateY:
scaleTranslation.y.value +
translation.y.value +
offset.y.value,
},
{ scale: scale.value },
],
};
}, []);
return (
<Animated.View
style={[styles.container, { width: targetWidth }, style]}
>
<PinchGestureHandler
enabled={enabled && pinchEnabled}
ref={pinchRef}
onGestureEvent={onScaleEvent}
simultaneousHandlers={[
panRef,
tapRef,
...outerGestureHandlerRefs,
]}
>
<Animated.View style={styles.fill}>
<PanGestureHandler
enabled={enabled}
ref={panRef}
minDist={4}
avgTouches
simultaneousHandlers={[
pinchRef,
tapRef,
...outerGestureHandlerRefs,
]}
onGestureEvent={onPanEvent}
>
<Animated.View style={styles.fill}>
<TapGestureHandler
enabled={enabled}
ref={tapRef}
numberOfTaps={1}
maxDeltaX={8}
maxDeltaY={8}
simultaneousHandlers={[
pinchRef,
panRef,
...outerGestureHandlerRefs,
]}
waitFor={doubleTapRef}
onGestureEvent={onTapEvent}
>
<Animated.View style={[styles.fill]}>
<Animated.View style={styles.fill}>
<Animated.View style={styles.wrapper}>
<TapGestureHandler
enabled={enabled}
ref={doubleTapRef}
numberOfTaps={2}
maxDelayMs={140}
maxDeltaX={16}
maxDeltaY={16}
simultaneousHandlers={[
pinchRef,
panRef,
...outerGestureHandlerRefs,
]}
onGestureEvent={onDoubleTapEvent}
>
<Animated.View style={animatedStyles}>
{typeof renderImage === 'function' ? (
renderImage({
source: imageSource,
width: targetWidth,
height: targetHeight,
onLoad: onLoadImageSuccess,
})
) : (
<ImageComponent
onLoad={onLoadImageSuccess}
source={imageSource}
style={{
width: targetWidth,
height: targetHeight,
}}
/>
)}
</Animated.View>
</TapGestureHandler>
</Animated.View>
</Animated.View>
</Animated.View>
</TapGestureHandler>
</Animated.View>
</PanGestureHandler>
</Animated.View>
</PinchGestureHandler>
</Animated.View>
);
},
)
Example #21
Source File: Pager.tsx From react-native-gallery-toolkit with MIT License | 4 votes |
Pager = typedMemo(function Pager<
TPages,
ItemT = UnpackItemT<TPages>,
>({
pages,
initialIndex,
totalCount,
numToRender = 2,
onIndexChange = workletNoop,
renderPage,
width = dimensions.width,
gutterWidth = GUTTER_WIDTH,
shouldRenderGutter = true,
keyExtractor,
pagerWrapperStyles = {},
getItem,
springConfig,
onPagerTranslateChange = workletNoop,
onGesture = workletNoop,
onEnabledGesture = workletNoop,
shouldHandleGestureEvent = workletNoopTrue,
initialDiffValue = 0,
shouldUseInteractionManager = true,
outerGestureHandlerRefs = [],
verticallyEnabled = true,
}: PagerProps<TPages, ItemT>) {
assertWorklet(onIndexChange);
assertWorklet(onPagerTranslateChange);
assertWorklet(onGesture);
assertWorklet(onEnabledGesture);
assertWorklet(shouldHandleGestureEvent);
// make sure to not calculate translate with gutter
// if we don't want to render it
if (!shouldRenderGutter) {
gutterWidth = 0;
}
const getPageTranslate = useWorkletCallback(
(i: number) => {
const t = i * width;
const g = gutterWidth * i;
return -(t + g);
},
[gutterWidth, width],
);
const pagerRef = useRef(null);
const tapRef = useRef(null);
const isActive = useSharedValue(true);
const onPageStateChange = useWorkletCallback((value: boolean) => {
isActive.value = value;
}, []);
const velocity = useSharedValue(0);
const isMounted = useRef(false);
const [diffValue, setDiffValue] = useState(initialDiffValue);
useEffect(() => {
if (shouldUseInteractionManager && !isMounted.current) {
InteractionManager.runAfterInteractions(() => {
setDiffValue(numToRender);
console.log('numToRender in interaction');
});
isMounted.current = true;
} else {
setDiffValue(numToRender);
}
}, [numToRender]);
// S2: Pager related stuff
const [activeIndex, setActiveIndex] = useState(initialIndex);
const index = useSharedValue(initialIndex);
const length = useSharedValue(totalCount);
const pagerX = useSharedValue(0);
const toValueAnimation = useSharedValue(
getPageTranslate(initialIndex),
);
const offsetX = useSharedValue(getPageTranslate(initialIndex));
const totalWidth = useDerivedValue(() => {
return length.value * width + gutterWidth * length.value - 2;
}, []);
const onIndexChangeCb = useWorkletCallback((nextIndex: number) => {
onIndexChange(nextIndex);
runOnJS(setActiveIndex)(nextIndex);
}, []);
const onIndexChangeWorklet = useWorkletCallback((i: number) => {
offsetX.value = getPageTranslate(i);
index.value = i;
onIndexChangeCb(i);
}, []);
useEffect(() => {
runOnUI(onIndexChangeWorklet)(initialIndex);
}, [initialIndex]);
const getSpringConfig = useWorkletCallback(
(noVelocity?: boolean) => {
const ratio = 1.1;
const mass = 0.4;
const stiffness = IS_ANDROID ? 200.0 : 100.0;
const damping = ratio * 2.0 * Math.sqrt(mass * stiffness);
const configToUse =
typeof springConfig !== 'undefined'
? springConfig
: {
stiffness,
mass,
damping,
restDisplacementThreshold: 1,
restSpeedThreshold: 5,
};
// @ts-ignore
// cannot use merge and spread here :(
configToUse.velocity = noVelocity ? 0 : velocity.value;
return configToUse;
},
[springConfig],
);
const onChangePageAnimation = useWorkletCallback(
(noVelocity?: boolean) => {
const config = getSpringConfig(noVelocity);
if (offsetX.value === toValueAnimation.value) {
return;
}
offsetX.value = withSpring(
toValueAnimation.value,
config,
(isCanceled) => {
if (!isCanceled) {
velocity.value = 0;
}
},
);
},
[getSpringConfig],
);
// S3 Pager
const getCanSwipe = useWorkletCallback(
(currentTranslate: number = 0) => {
const nextTranslate = offsetX.value + currentTranslate;
if (nextTranslate > 0) {
return false;
}
const totalTranslate =
width * (length.value - 1) + gutterWidth * (length.value - 1);
if (Math.abs(nextTranslate) >= totalTranslate) {
return false;
}
return true;
},
[width, gutterWidth],
);
const getNextIndex = useWorkletCallback(
(v: number) => {
const currentTranslate = Math.abs(
getPageTranslate(index.value),
);
const currentIndex = index.value;
const currentOffset = Math.abs(offsetX.value);
const nextIndex = v < 0 ? currentIndex + 1 : currentIndex - 1;
if (
nextIndex < currentIndex &&
currentOffset > currentTranslate
) {
return currentIndex;
}
if (
nextIndex > currentIndex &&
currentOffset < currentTranslate
) {
return currentIndex;
}
if (nextIndex > length.value - 1 || nextIndex < 0) {
return currentIndex;
}
return nextIndex;
},
[getPageTranslate],
);
const isPagerInProgress = useDerivedValue(() => {
return (
Math.floor(Math.abs(getPageTranslate(index.value))) !==
Math.floor(Math.abs(offsetX.value + pagerX.value))
);
}, [getPageTranslate]);
const onPan = useAnimatedGestureHandler<
PanGestureHandlerGestureEvent,
{
pagerActive: boolean;
offsetX: null | number;
}
>({
onGesture: (evt) => {
onGesture(evt, isActive);
if (isActive.value && !isPagerInProgress.value) {
onEnabledGesture(evt);
}
},
onInit: (_, ctx) => {
ctx.offsetX = null;
},
shouldHandleEvent: (evt) => {
return (
(evt.numberOfPointers === 1 &&
isActive.value &&
Math.abs(evt.velocityX) > Math.abs(evt.velocityY) &&
shouldHandleGestureEvent(evt)) ||
isPagerInProgress.value
);
},
onEvent: (evt) => {
velocity.value = clampVelocity(
evt.velocityX,
MIN_VELOCITY,
MAX_VELOCITY,
);
},
onStart: (_, ctx) => {
ctx.offsetX = null;
},
onActive: (evt, ctx) => {
// workaround alert
// the event triggers with a delay and first frame value jumps
// we capture that value and subtract from the actual one
// so the translate happens on a second frame
if (ctx.offsetX === null) {
ctx.offsetX =
evt.translationX < 0 ? evt.translationX : -evt.translationX;
}
const val = evt.translationX - ctx.offsetX;
const canSwipe = getCanSwipe(val);
pagerX.value = canSwipe ? val : friction(val);
},
onEnd: (evt, ctx) => {
const val = evt.translationX - ctx.offsetX!;
const canSwipe = getCanSwipe(val);
offsetX.value += pagerX.value;
pagerX.value = 0;
const nextIndex = getNextIndex(evt.velocityX);
const vx = Math.abs(evt.velocityX);
const translation = Math.abs(val);
const isHalf = width / 2 < translation;
const shouldMoveToNextPage = (vx > 10 || isHalf) && canSwipe;
// we invert the value since the translationY is left to right
toValueAnimation.value = -(shouldMoveToNextPage
? -getPageTranslate(nextIndex)
: -getPageTranslate(index.value));
onChangePageAnimation(!shouldMoveToNextPage);
if (shouldMoveToNextPage) {
index.value = nextIndex;
onIndexChangeCb(nextIndex);
}
},
});
const onTap = useAnimatedGestureHandler({
shouldHandleEvent: (evt) => {
return evt.numberOfPointers === 1 && isActive.value;
},
onStart: () => {
cancelAnimation(offsetX);
},
onEnd: () => {
onChangePageAnimation(true);
},
});
const pagerStyles = useAnimatedStyle<ViewStyle>(() => {
const translateX = pagerX.value + offsetX.value;
onPagerTranslateChange(translateX);
return {
width: totalWidth.value,
transform: [
{
translateX,
},
],
};
}, []);
const pagerRefs = useMemo<PageRefs>(() => [pagerRef, tapRef], []);
const pagesToRender = useMemo(() => {
const temp = [];
for (let i = 0; i < totalCount; i += 1) {
let itemToUse;
if (typeof getItem === 'function') {
itemToUse = getItem(pages, i);
} else if (Array.isArray(pages)) {
itemToUse = pages[i];
} else {
throw new Error(
'Pager: items either should be an array of getItem should be defined',
);
}
const shouldRender = getShouldRender(i, activeIndex, diffValue);
if (!shouldRender) {
temp.push(null);
} else {
temp.push(
<Page
key={keyExtractor(itemToUse, i)}
item={itemToUse}
currentIndex={index}
pagerRefs={pagerRefs}
onPageStateChange={onPageStateChange}
index={i}
length={totalCount}
gutterWidth={gutterWidth}
renderPage={renderPage}
getPageTranslate={getPageTranslate}
width={width}
isPagerInProgress={isPagerInProgress}
shouldRenderGutter={shouldRenderGutter}
/>,
);
}
}
return temp;
}, [
activeIndex,
diffValue,
keyExtractor,
getItem,
totalCount,
pages,
getShouldRender,
index,
pagerRefs,
onPageStateChange,
gutterWidth,
renderPage,
getPageTranslate,
width,
isPagerInProgress,
shouldRenderGutter,
]);
return (
<View style={StyleSheet.absoluteFillObject}>
<Animated.View style={[StyleSheet.absoluteFill]}>
<PanGestureHandler
ref={pagerRef}
minDist={0.1}
minVelocityX={0.1}
activeOffsetX={[-4, 4]}
activeOffsetY={verticallyEnabled ? [-4, 4] : undefined}
simultaneousHandlers={[tapRef, ...outerGestureHandlerRefs]}
onGestureEvent={onPan}
>
<Animated.View style={StyleSheet.absoluteFill}>
<TapGestureHandler
ref={tapRef}
maxDeltaX={10}
maxDeltaY={10}
simultaneousHandlers={pagerRef}
onGestureEvent={onTap}
>
<Animated.View
style={[StyleSheet.absoluteFill, pagerWrapperStyles]}
>
<Animated.View style={StyleSheet.absoluteFill}>
<Animated.View style={[styles.pager, pagerStyles]}>
{pagesToRender}
</Animated.View>
</Animated.View>
</Animated.View>
</TapGestureHandler>
</Animated.View>
</PanGestureHandler>
</Animated.View>
</View>
);
})