react-native-reanimated#withTiming TypeScript Examples
The following examples show how to use
react-native-reanimated#withTiming.
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: AnimatedHelper.ts From curved-bottom-navigation-bar with MIT License | 6 votes |
sharedTiming = (
toValue: number,
config?: WithTimingConfig,
callBack?: AnimationCallback
) => {
'worklet';
return withTiming(
toValue,
Object.assign(
{
duration: 500,
easing: Easing.bezier(0.22, 1, 0.36, 1),
},
config
),
callBack
);
}
Example #3
Source File: useInit.ts From react-native-gallery-toolkit with MIT License | 6 votes |
usedWorklets = {
withTiming,
withSpring,
bezier: Easing.bezier,
interpolate,
withDecay,
useAnimatedGestureHandler,
...usedVectors,
} as { [key: string]: any }
Example #4
Source File: Picker.tsx From swiftui-react-native with MIT License | 6 votes |
Divider = ({
color,
index,
selection,
childCount,
}: {
color: string;
index: number;
selection: number;
childCount: number;
}) => {
const animatedDividerStyle = useAnimatedStyle(() => {
return {
opacity:
index === selection ||
index === selection - 1 ||
index === childCount - 1
? withTiming(0)
: withTiming(1),
};
}, [index, selection, childCount]);
return (
<Animated.View
style={[
styles.divider,
animatedDividerStyle,
{
borderRightColor: color,
},
]}
/>
);
}
Example #5
Source File: utils.ts From swiftui-react-native with MIT License | 6 votes |
snapToClosest = (position: number, points: number[]) => {
'worklet';
const deltas = points.map((p) => Math.abs(position - p));
const minDelta = Math.min.apply(null, deltas);
return withTiming(
points.filter((p) => Math.abs(position - p) === minDelta)[0]
);
}
Example #6
Source File: useControls.tsx From react-native-gallery-toolkit with MIT License | 6 votes |
export function useControls() {
const controlsHidden = useSharedValue(false);
const translateYConfig = {
duration: 400,
easing: Easing.bezier(0.33, 0.01, 0, 1),
};
const controlsStyles = useAnimatedStyle(() => {
return {
opacity: controlsHidden.value ? withTiming(0) : withTiming(1),
transform: [
{
translateY: controlsHidden.value
? withTiming(-100, translateYConfig)
: withTiming(0, translateYConfig),
},
],
position: 'absolute',
top: 0,
width: '100%',
zIndex: 1,
};
});
const setControlsHidden = useWorkletCallback((hidden: boolean) => {
if (controlsHidden.value === hidden) {
return;
}
controlsHidden.value = hidden;
}, []);
return {
controlsHidden,
controlsStyles,
setControlsHidden,
};
}
Example #7
Source File: FullFeatured.tsx From react-native-gallery-toolkit with MIT License | 6 votes |
export function useToggleOpacity(
prop: Animated.SharedValue<boolean>,
) {
const translateY = useSharedValue(1);
const styles = useAnimatedStyle(() => {
if (prop.value) {
return {
opacity: withTiming(1),
transform: [{ translateY: 0 }],
};
}
return {
opacity: withTiming(0, undefined, () => {
translateY.value = -99999;
}),
transform: [{ translateY: translateY.value }],
};
}, []);
return styles;
}
Example #8
Source File: useAnimatedPath.ts From react-native-wagmi-charts with MIT License | 6 votes |
export default function useAnimatedPath({
enabled = true,
path,
}: {
enabled?: boolean;
path: string;
}) {
const transition = useSharedValue(0);
const previousPath = usePrevious(path);
useAnimatedReaction(
() => {
return path;
},
(_, previous) => {
if (previous) {
transition.value = 0;
transition.value = withTiming(1);
}
},
[path]
);
const animatedProps = useAnimatedProps(() => {
let d = path || '';
if (previousPath && enabled) {
const pathInterpolator = interpolatePath(previousPath, path, null);
d = pathInterpolator(transition.value);
}
return {
d,
};
});
return { animatedProps };
}
Example #9
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 #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: index.tsx From react-native-checkbox-reanimated with MIT License | 5 votes |
AnimatedCheckbox = (props: Props) => {
const { checked, checkmarkColor, highlightColor, boxOutlineColor } = props
const progress = useSharedValue(0)
useEffect(() => {
progress.value = withTiming(checked ? 1 : 0, {
duration: checked ? 300 : 100,
easing: Easing.linear
})
}, [checked])
const animatedBoxProps = useAnimatedProps(
() => ({
stroke: interpolateColor(
Easing.bezierFn(0.16, 1, 0.3, 1)(progress.value),
[0, 1],
[boxOutlineColor, highlightColor],
'RGB'
),
fill: interpolateColor(
Easing.bezierFn(0.16, 1, 0.3, 1)(progress.value),
[0, 1],
['#00000000', highlightColor],
'RGB'
)
}),
[highlightColor, boxOutlineColor]
)
return (
<Svg
viewBox={[-MARGIN, -MARGIN, vWidth + MARGIN, vHeight + MARGIN].join(' ')}
>
<Defs>
<ClipPath id="clipPath">
<Path
fill="white"
stroke="gray"
strokeLinejoin="round"
strokeLinecap="round"
d={outlineBoxPath}
/>
</ClipPath>
</Defs>
<AnimatedStroke
progress={progress}
d={checkMarkPath}
stroke={highlightColor}
strokeWidth={10}
strokeLinejoin="round"
strokeLinecap="round"
strokeOpacity={checked || false ? 1 : 0}
/>
<AnimatedPath
d={outlineBoxPath}
strokeWidth={7}
strokeLinejoin="round"
strokeLinecap="round"
animatedProps={animatedBoxProps}
/>
<G clipPath="url(#clipPath)">
<AnimatedStroke
progress={progress}
d={checkMarkPath}
stroke={checkmarkColor}
strokeWidth={10}
strokeLinejoin="round"
strokeLinecap="round"
strokeOpacity={checked || false ? 1 : 0}
/>
</G>
</Svg>
)
}
Example #12
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 #13
Source File: ChartPath.tsx From react-native-wagmi-charts with MIT License | 4 votes |
export function LineChartPathWrapper({
animationDuration = 300,
animationProps = {},
children,
color = 'black',
inactiveColor,
width: strokeWidth = 3,
widthOffset = 20,
pathProps = {},
showInactivePath = true,
animateOnMount,
mountAnimationDuration = animationDuration,
mountAnimationProps = animationProps,
}: LineChartPathWrapperProps) {
const { height, pathWidth, width } = React.useContext(
LineChartDimensionsContext
);
const { currentX, isActive } = useLineChart();
const isMounted = useSharedValue(false);
const hasMountedAnimation = useSharedValue(false);
React.useEffect(() => {
isMounted.value = true;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
////////////////////////////////////////////////
const svgProps = useAnimatedProps(() => {
const shouldAnimateOnMount = animateOnMount === 'foreground';
const inactiveWidth =
!isMounted.value && shouldAnimateOnMount ? 0 : pathWidth;
let duration =
shouldAnimateOnMount && !hasMountedAnimation.value
? mountAnimationDuration
: animationDuration;
const props =
shouldAnimateOnMount && !hasMountedAnimation.value
? mountAnimationProps
: animationProps;
if (isActive.value) {
duration = 0;
}
return {
width: withTiming(
isActive.value
? // on Web, <svg /> elements don't support negative widths
// https://github.com/coinjar/react-native-wagmi-charts/issues/24#issuecomment-955789904
Math.max(currentX.value, 0)
: inactiveWidth + widthOffset,
Object.assign({ duration }, props),
() => {
hasMountedAnimation.value = true;
}
),
};
});
const viewSize = React.useMemo(() => ({ width, height }), [width, height]);
////////////////////////////////////////////////
let backgroundChildren;
let foregroundChildren;
if (children) {
const iterableChildren = flattenChildren(children);
backgroundChildren = iterableChildren.filter((child) =>
// @ts-ignore
BACKGROUND_COMPONENTS.includes(child?.type?.displayName)
);
foregroundChildren = iterableChildren.filter((child) =>
// @ts-ignore
FOREGROUND_COMPONENTS.includes(child?.type?.displayName)
);
}
////////////////////////////////////////////////
return (
<>
<LineChartPathContext.Provider
value={{
color,
isInactive: showInactivePath,
isTransitionEnabled: pathProps.isTransitionEnabled ?? true,
}}
>
<View style={viewSize}>
<Svg width={width} height={height}>
<LineChartPath
color={color}
inactiveColor={inactiveColor}
width={strokeWidth}
{...pathProps}
/>
{backgroundChildren}
</Svg>
</View>
</LineChartPathContext.Provider>
<LineChartPathContext.Provider
value={{
color,
isInactive: false,
isTransitionEnabled: pathProps.isTransitionEnabled ?? true,
}}
>
<View style={StyleSheet.absoluteFill}>
<AnimatedSVG animatedProps={svgProps} height={height}>
<LineChartPath color={color} width={strokeWidth} {...pathProps} />
{foregroundChildren}
</AnimatedSVG>
</View>
</LineChartPathContext.Provider>
</>
);
}
Example #14
Source File: Candle.tsx From react-native-wagmi-charts with MIT License | 4 votes |
CandlestickChartCandle = ({
candle,
maxHeight,
domain,
margin = 2,
positiveColor = '#10b981',
negativeColor = '#ef4444',
rectProps: overrideRectProps,
lineProps: overrideLineProps,
index,
width,
useAnimations = true,
renderLine = (props) =>
props.useAnimations ? <AnimatedLine {...props} /> : <Line {...props} />,
renderRect = (props) =>
props.useAnimations ? <AnimatedRect {...props} /> : <Rect {...props} />,
}: CandlestickChartCandleProps) => {
const { close, open, high, low } = candle;
const isPositive = close > open;
const fill = isPositive ? positiveColor : negativeColor;
const x = index * width;
const max = Math.max(open, close);
const min = Math.min(open, close);
const lineProps = React.useMemo(
() => ({
stroke: fill,
strokeWidth: 1,
direction: isPositive ? 'positive' : 'negative',
x1: x + width / 2,
y1: getY({ maxHeight, value: low, domain }),
x2: x + width / 2,
y2: getY({ maxHeight, value: high, domain }),
...overrideLineProps,
}),
[
domain,
fill,
high,
isPositive,
low,
maxHeight,
overrideLineProps,
width,
x,
]
);
const animatedLineProps = useAnimatedProps(() => ({
x1: withTiming(x + width / 2),
y1: withTiming(getY({ maxHeight, value: low, domain })),
x2: withTiming(x + width / 2),
y2: withTiming(getY({ maxHeight, value: high, domain })),
}));
const rectProps = React.useMemo(
() => ({
width: width - margin * 2,
fill: fill,
direction: isPositive ? 'positive' : 'negative',
x: x + margin,
y: getY({ maxHeight, value: max, domain }),
height: getHeight({ maxHeight, value: max - min, domain }),
...overrideRectProps,
}),
[
domain,
fill,
isPositive,
margin,
max,
maxHeight,
min,
overrideRectProps,
width,
x,
]
);
const animatedRectProps = useAnimatedProps(() => ({
x: withTiming(x + margin),
y: withTiming(getY({ maxHeight, value: max, domain })),
height: withTiming(getHeight({ maxHeight, value: max - min, domain })),
}));
return (
<>
{renderLine({
...lineProps,
useAnimations,
...(useAnimations ? { animatedProps: animatedLineProps } : {}),
})}
{renderRect({
...rectProps,
useAnimations,
...(useAnimations ? { animatedProps: animatedRectProps } : {}),
})}
</>
);
}
Example #15
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 #16
Source File: InstagramFeed.tsx From react-native-gallery-toolkit with MIT License | 4 votes |
function RenderItem({
index: _index,
activeItemIndex,
item: { images, name },
setControlsHidden,
scrollViewRef,
}: RenderItemProps) {
const opacity = useSharedValue(0);
const backgroundScale = useSharedValue(0);
const activeIndexInPager = useSharedValue(0);
const normalizedImages = useMemo(
() =>
images.map((item) => {
const { targetWidth, targetHeight } = normalizeDimensions(
item,
);
return {
...item,
width: targetWidth,
height: targetHeight,
};
}),
[images],
);
const onScale = useWorkletCallback((scale: number) => {
opacity.value = interpolate(
scale,
[1, 2],
[0, 0.7],
Extrapolate.CLAMP,
);
backgroundScale.value = interpolate(
scale,
[1, 1.01, 2],
[0, 4, 5],
Extrapolate.CLAMP,
);
}, []);
const onGestureStart = useWorkletCallback(() => {
setControlsHidden(true);
runOnJS(StatusBar.setHidden)(true);
activeItemIndex.value = _index;
}, []);
const onGestureRelease = useWorkletCallback(() => {
//delay for smooth hiding background opacity
activeItemIndex.value = withDelay(200, withTiming(-1));
setControlsHidden(false);
runOnJS(StatusBar.setHidden)(false);
}, []);
const overlayStyles = useAnimatedStyle(() => {
return {
opacity: opacity.value,
transform: [
{
scale: backgroundScale.value,
},
],
};
});
const keyExtractor = useCallback(
({ id }: { id: string }) => id,
[],
);
const canvasHeight = useMemo(
() => Math.max(...normalizedImages.map((item) => item.height)),
[normalizedImages],
);
const renderPage = useCallback(({ item, pagerRefs }: RenderPageProps<SimpleGalleryItemType>) => {
return (
<ScalableImage
outerGestureHandlerRefs={[...pagerRefs, scrollViewRef]}
source={item.uri}
width={item.width}
height={item.height}
onScale={onScale}
onGestureStart={onGestureStart}
onGestureRelease={onGestureRelease}
/>
);
}, []);
const onIndexChangeWorklet = useWorkletCallback((nextIndex: number) => {
activeIndexInPager.value = nextIndex;
}, []);
const content = (() => {
if (images.length === 1) {
return (
<ScalableImage
source={images[0].uri}
width={images[0].width}
height={images[0].height}
onScale={onScale}
outerGestureHandlerRefs={[scrollViewRef]}
onGestureStart={onGestureStart}
onGestureRelease={onGestureRelease}
/>
);
} else {
return (
<>
<Pager
pages={images}
totalCount={images.length}
keyExtractor={keyExtractor}
initialIndex={0}
width={width}
gutterWidth={0}
outerGestureHandlerRefs={[scrollViewRef]}
verticallyEnabled={false}
// @ts-expect-error
renderPage={renderPage}
onIndexChange={onIndexChangeWorklet}
/>
<Pagination
length={images.length}
activeIndexInPager={activeIndexInPager}
/>
</>
);
}
})();
return (
<Animated.View style={s.itemContainer}>
<Header uri={images[0].uri} name={name} />
<Animated.View
pointerEvents="none"
style={[s.overlay, overlayStyles]}
/>
<View style={[s.itemPager, { height: canvasHeight }]}>
{content}
</View>
<Footer />
</Animated.View>
);
}
Example #17
Source File: Slider.tsx From swiftui-react-native with MIT License | 4 votes |
Slider: React.FC<SliderProps> = ({
tint,
trackTint,
thumbTint,
range = [0, 10],
step = 1,
value,
updateOnSlide = true,
frame,
backgroundColor,
style,
padding,
cornerRadius,
rotationEffect,
scaleEffect,
shadow,
border,
opacity,
zIndex,
alert,
onAppear,
onDisappear,
onChange,
}) => {
useAlert(alert);
useLifecycle(onAppear, onDisappear);
const colorScheme = useColorScheme();
const [sliderWidth, sliderHeight] = getSliderWidth(frame);
const [from, through] = range;
const midPoint = (through + from) / 2;
const slope = (midPoint - from) / (sliderWidth / 2);
const translateX = useSharedValue(
value2Position(value.value, midPoint, slope)
);
useEffect(() => {
const newPos = value2Position(value.value, midPoint, slope);
translateX.value = withTiming(newPos);
}, [value.value]);
const animatedCursorStyle = useAnimatedStyle(() => {
return {
transform: [
{
translateX: translateX.value,
},
],
};
});
const animatedFillStyle = useAnimatedStyle(() => {
return {
width: translateX.value + sliderWidth / 2,
};
});
const gestureHandler = useAnimatedGestureHandler<
PanGestureHandlerGestureEvent,
GestureHandlerContext
>({
onStart: (_, ctx) => {
ctx.offsetX = translateX.value;
},
onActive: (e, ctx) => {
const prevPos = translateX.value;
const newPos = e.translationX + ctx.offsetX;
if (newPos < sliderWidth / 2 && newPos > -sliderWidth / 2) {
translateX.value = newPos;
const prevVal = position2Value(prevPos, midPoint, slope, step);
const newVal = position2Value(newPos, midPoint, slope, step);
if (updateOnSlide && prevVal !== newVal) {
runOnJS(value.setValue)(newVal);
if (onChange) runOnJS(onChange)(newVal);
}
}
},
onEnd: () => {
if (!updateOnSlide) {
const newVal = position2Value(translateX.value, midPoint, slope, step);
runOnJS(value.setValue)(newVal);
if (onChange) runOnJS(onChange)(newVal);
}
},
});
return (
<View
style={[
{
opacity,
zIndex,
backgroundColor: getColor(backgroundColor, colorScheme),
...getCornerRadius(cornerRadius),
...getPadding(padding),
...getBorder(border),
...getShadow(shadow),
...getTransform(scaleEffect, rotationEffect),
},
style,
]}
>
<View
style={[
styles.slider,
{
width: sliderWidth,
height: sliderHeight,
marginTop: CIRCLE_WIDTH / 2,
marginBottom: CIRCLE_WIDTH / 2,
backgroundColor: getColor(trackTint, colorScheme, 'systemGray4'),
},
]}
>
<Animated.View
style={[
{
height: sliderHeight,
borderRadius: 10,
backgroundColor: getColor(tint, colorScheme, 'systemBlue'),
},
animatedFillStyle,
]}
/>
<PanGestureHandler onGestureEvent={gestureHandler}>
<Animated.View
style={[
styles.cursor,
{
left: sliderWidth / 2 - CIRCLE_WIDTH / 2,
top: -CIRCLE_WIDTH / 2,
height: CIRCLE_WIDTH,
width: CIRCLE_WIDTH,
backgroundColor: getColor(thumbTint, colorScheme, '#fff'),
},
animatedCursorStyle,
]}
/>
</PanGestureHandler>
</View>
</View>
);
}
Example #18
Source File: FullFeatured.tsx From react-native-gallery-toolkit with MIT License | 4 votes |
export default function FullFeatured() {
const nav = useNavigation();
const [index, setIndex] = useState(1);
const headerShown = useSharedValue(true);
const translateY = useSharedValue(0);
const bottomTranslateY = useSharedValue(0);
const galleryRef = useRef<SimpleGallery<GalleryItemType[]>>(
null,
);
const onIndexChange = useWorkletCallback((nextIndex: number) => {
runOnJS(setIndex)(nextIndex);
}, []);
function onNext() {
galleryRef.current!.goNext();
}
function onBack() {
galleryRef.current!.goBack();
}
function setHeaderShown(value: boolean) {
headerShown.value = value;
nav.setParams({ headerShown: value });
StatusBar.setHidden(!value);
}
function toggleHeaderShown() {
const nextValue = !headerShown.value;
setHeaderShown(nextValue);
}
function hide() {
setHeaderShown(false);
}
const opacityAnimatedStyles = useToggleOpacity(headerShown);
const translateYAnimatedStyles = useAnimatedStyle(() => {
return {
transform: [{ translateY: bottomTranslateY.value }],
};
}, []);
function handleClose() {
nav.goBack();
}
function shouldPagerHandleGestureEvent() {
'worklet';
return translateY.value === 0;
}
const handler = useCallback(
createAnimatedGestureHandler<PanGestureHandlerGestureEvent, {}>({
shouldHandleEvent: (evt) => {
'worklet';
return (
evt.numberOfPointers === 1 &&
Math.abs(evt.velocityX) < Math.abs(evt.velocityY)
);
},
onActive: (evt) => {
'worklet';
translateY.value = evt.translationY;
bottomTranslateY.value =
evt.translationY > 0 ? evt.translationY : 0;
},
onEnd: () => {
'worklet';
if (translateY.value > 80) {
translateY.value = withTiming(
-800,
undefined,
runOnJS(handleClose),
);
} else {
translateY.value = withTiming(0);
bottomTranslateY.value = withTiming(0);
}
},
}),
[],
);
const translateStyles = useAnimatedStyle(() => {
return {
transform: [
{
translateY: translateY.value,
},
],
};
}, []);
const onInteraction = useWorkletCallback(() => {
runOnJS(hide)();
}, []);
const onTap = useWorkletCallback(() => {
runOnJS(toggleHeaderShown)();
}, []);
const onDoubleTap = useWorkletCallback((isScaled: boolean) => {
if (!isScaled) {
runOnJS(hide);
}
}, []);
const insets = useSafeAreaInsets();
return (
<View style={{ flex: 1, backgroundColor: 'black' }}>
<CustomHeader
topInset={insets.top}
bottomTranslateY={bottomTranslateY}
headerShown={headerShown}
/>
<Animated.View
style={[translateStyles, StyleSheet.absoluteFill]}
>
<SimpleGallery
ref={galleryRef}
initialIndex={1}
items={images}
keyExtractor={(item) => item.id}
gutterWidth={56}
onIndexChange={onIndexChange}
renderImage={(props, _, index) => {
return <ImageRender index={index} {...props} />;
}}
renderPage={({ item, ...rest }) => {
if (item.type === 'image') {
return (
<SimpleGallery.ImageRenderer item={item} {...rest} />
);
}
// TODO: Figure out why Video component is not working
return (
<View>
<Text>I can be a video</Text>
</View>
);
}}
onInteraction={onInteraction}
onTap={onTap}
onDoubleTap={onDoubleTap}
numToRender={2}
shouldPagerHandleGestureEvent={
shouldPagerHandleGestureEvent
}
onPagerEnabledGesture={handler}
// onPagerTranslateChange={(translateX) => {
// console.log(translateX);
// }}
/>
</Animated.View>
<Animated.View
style={[
s.bottomBar,
opacityAnimatedStyles,
translateYAnimatedStyles,
]}
>
<Button onPress={onBack} text="Back" />
<Text>Index: {index}</Text>
<Button onPress={onNext} text="Next" />
</Animated.View>
</View>
);
}
Example #19
Source File: Linear.tsx From swiftui-react-native with MIT License | 4 votes |
Linear = ({
value,
total = 100,
backgroundColor,
opacity,
frame,
cornerRadius,
rotationEffect,
scaleEffect,
padding,
border,
shadow,
zIndex,
style,
tint,
alert,
onAppear,
onDisappear,
}: LinearProps) => {
useAlert(alert);
useLifecycle(onAppear, onDisappear);
const colorScheme = useColorScheme();
const [sliderWidth, sliderHeight] = getSliderWidth(frame);
const midPoint = total / 2;
const slope = midPoint / (sliderWidth / 2);
const progress = useSharedValue(value2Position(value, midPoint, slope));
useEffect(() => {
if (value > total) {
progress.value = withTiming(value2Position(total, midPoint, slope));
} else if (value < 0) {
progress.value = withTiming(0);
} else {
const newPos = value2Position(value, midPoint, slope);
progress.value = withTiming(newPos);
}
}, [value, total]);
const animatedFillStyle = useAnimatedStyle(() => {
return {
width: progress.value + sliderWidth / 2,
};
}, [progress.value]);
return (
<View
style={[
{
opacity,
zIndex,
backgroundColor: getColor(backgroundColor, colorScheme),
...getCornerRadius(cornerRadius),
...getPadding(padding),
...getBorder(border),
...getShadow(shadow),
...getTransform(scaleEffect, rotationEffect),
},
style,
]}
>
<View
style={[
styles.progressBar,
{
width: sliderWidth,
height: sliderHeight,
backgroundColor: getColor('systemGray4', colorScheme),
},
]}
>
<Animated.View
style={[
{
height: sliderHeight,
borderRadius: 10,
backgroundColor: getColor(tint, colorScheme, 'systemBlue'),
},
animatedFillStyle,
]}
/>
</View>
</View>
);
}
Example #20
Source File: Picker.tsx From swiftui-react-native with MIT License | 4 votes |
Picker = ({
children,
selection,
backgroundColor,
opacity,
frame,
cornerRadius,
scaleEffect,
rotationEffect,
padding,
border,
shadow,
zIndex,
style,
alert,
onChange,
onAppear,
onDisappear,
}: PickerProps) => {
useAlert(alert);
useLifecycle(onAppear, onDisappear);
const colorScheme = useColorScheme();
const childCount = Children.count(children);
const [optionDimensions, setOptionDimensions] = useState(null);
const tempSelection = useSharedValue(selection.value);
const width = optionDimensions ? optionDimensions.width : 0;
const slidePosition = useSharedValue(
selectedToPosition(selection.value, width, childCount)
);
const sliderHeight = optionDimensions ? optionDimensions.height - 5 : 0;
const sliderWidth = optionDimensions
? optionDimensions.width / childCount
: 0;
const animatedSliderStyle = useAnimatedStyle(() => {
return {
transform: [{ translateX: slidePosition.value }],
};
}, [slidePosition.value]);
const gestureHandler = useAnimatedGestureHandler<
PanGestureHandlerGestureEvent,
GestureHandlerContext
>({
onStart: (_, ctx) => {
ctx.offsetX = slidePosition.value;
},
onActive: (e, ctx) => {
const currentXPos = e.translationX + ctx.offsetX;
const optionWidth = width / childCount;
const slideTo = clamp(
Math.round((currentXPos - optionWidth / 2) / optionWidth) * optionWidth,
0,
(childCount - 1) * optionWidth
);
if (slideTo !== slidePosition.value) {
slidePosition.value = withTiming(slideTo);
tempSelection.value = positionToSelected(slideTo, width, childCount);
console.log(tempSelection.value);
}
},
onEnd: () => {
const newValue = positionToSelected(
slidePosition.value,
width,
childCount
);
runOnJS(selection.setValue)(newValue);
if (onChange) {
runOnJS(onChange)(newValue);
}
},
});
return (
<Fragment>
<View
onLayout={(e) => setOptionDimensions(e.nativeEvent.layout)}
style={[
styles.container,
{
opacity,
zIndex,
backgroundColor: getColor(
backgroundColor,
colorScheme,
'secondarySystemBackground'
),
...getCornerRadius(cornerRadius),
...getShadow(shadow),
...getPadding(padding),
...getFrame(frame),
...getBorder(border),
...getTransform(scaleEffect, rotationEffect),
},
style,
]}
>
<View style={styles.options}>
{React.Children.map(children as ReactElement<any>, (child, i) => {
const textChild =
child?.type === Text
? cloneElement(child, { fontSize: 12, ...child.props })
: child;
return (
<Fragment key={i}>
<TouchableOpacity
style={[styles.option, { flexBasis: `${100 / childCount}%` }]}
disabled={selection.value === i}
onPress={() => {
slidePosition.value = withTiming(
selectedToPosition(i, width, childCount)
);
tempSelection.value = i;
runOnJS(selection.setValue)(i);
if (onChange) {
runOnJS(onChange)(i);
}
}}
>
{textChild}
</TouchableOpacity>
<Divider
color={getColor('systemGray4', colorScheme)}
index={i}
selection={tempSelection.value}
childCount={childCount}
/>
</Fragment>
);
})}
</View>
<Animated.View
style={[
styles.slider,
animatedSliderStyle,
{
width: sliderWidth,
height: sliderHeight,
backgroundColor:
colorScheme === 'dark'
? getColor('secondarySystemBackground', 'dark')
: getColor('systemBackground', 'light'),
},
]}
/>
</View>
<PanGestureHandler onGestureEvent={gestureHandler}>
<Animated.View
style={[
styles.slider,
animatedSliderStyle,
{
width: sliderWidth,
height: sliderHeight,
zIndex: 0,
left: 10,
top: 12,
},
]}
/>
</PanGestureHandler>
</Fragment>
);
}
Example #21
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 #22
Source File: ScalableImage.tsx From react-native-gallery-toolkit with MIT License | 4 votes |
ScalableImage = React.memo<ScalableImageProps>(
({
outerGestureHandlerRefs = [],
source,
width,
height,
onStateChange = workletNoop,
renderImage,
canvasDimensions,
outerGestureHandlerActive,
style,
onScale = workletNoop,
onGestureRelease = workletNoop,
onGestureStart = workletNoop,
onEnd = workletNoop,
MAX_SCALE = 3,
MIN_SCALE = 1,
timingConfig = defaultTimingConfig,
enabled = true,
}) => {
fixGestureHandler();
if (typeof source === 'undefined') {
throw new Error(
'ScalableImage: either source or uri should be passed to display an image',
);
}
const imageSource =
typeof source === 'string'
? {
uri: source,
}
: source;
const interactionsEnabled = useSharedValue(false);
const setInteractionsEnabled = useCallback((value: boolean) => {
interactionsEnabled.value = value;
}, []);
const onLoadImageSuccess = useCallback(() => {
setInteractionsEnabled(true);
}, []);
const pinchRef = useRef(null);
const scale = useSharedValue(1);
const scaleOffset = useSharedValue(1);
const scaleTranslation = vectors.useSharedVector(0, 0);
const { targetWidth, targetHeight } = normalizeDimensions(
{
width,
height,
},
canvasDimensions?.width ?? Dimensions.get('window').width,
);
const canvas = vectors.create(
canvasDimensions?.width ?? targetWidth,
canvasDimensions?.height ?? targetHeight,
);
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;
},
shouldHandleEvent: (evt) => {
return (
evt.numberOfPointers === 2 &&
scale.value === 1 &&
interactionsEnabled.value &&
(typeof outerGestureHandlerActive !== 'undefined'
? !outerGestureHandlerActive.value
: true)
);
},
beforeEach: (evt, ctx) => {
// calculate the overall scale value
// also limits this.event.scale
ctx.nextScale = clamp(
evt.scale * scaleOffset.value,
MIN_SCALE,
MAX_SCALE,
);
if (ctx.nextScale > MIN_SCALE && ctx.nextScale < MAX_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);
// it alow us to scale into different point even then we pan the image
ctx.adjustFocal = vectors.sub(focal, CENTER);
},
afterEach: (evt, ctx) => {
if (evt.state === 5) {
return;
}
scale.value = ctx.nextScale;
},
onStart: (_, ctx) => {
vectors.set(ctx.origin, ctx.adjustFocal);
onGestureStart();
},
onActive: (_, ctx) => {
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);
},
onEnd: (_, ctx) => {
onGestureRelease();
// reset gestureScale value
ctx.gestureScale = 1;
// store scale value
scale.value = withTiming(1, timingConfig, () => {
'worklet';
onEnd();
});
vectors.set(scaleTranslation, () =>
withTiming(0, timingConfig),
);
},
});
const animatedStyles = useAnimatedStyle<ViewStyle>(() => {
const noScaleTranslation =
scaleTranslation.x.value === 0 &&
scaleTranslation.y.value === 0;
const isInactive = scale.value === 1 && noScaleTranslation;
onStateChange(isInactive);
onScale(scale.value);
return {
transform: [
{
translateX: scaleTranslation.x.value,
},
{
translateY: scaleTranslation.y.value,
},
{ scale: scale.value },
],
};
}, []);
return (
<Animated.View
style={[{ width: targetWidth, height: targetHeight }, style]}
>
<PinchGestureHandler
enabled={enabled}
ref={pinchRef}
onGestureEvent={onScaleEvent}
simultaneousHandlers={[...outerGestureHandlerRefs]}
>
<Animated.View style={StyleSheet.absoluteFillObject}>
<Animated.View style={animatedStyles}>
{typeof renderImage === 'function' ? (
renderImage({
source: imageSource,
width: targetWidth,
height: targetHeight,
onLoad: onLoadImageSuccess,
})
) : (
<AnimatedImageComponent
onLoad={onLoadImageSuccess}
source={imageSource}
style={{
width: targetWidth,
height: targetHeight,
}}
/>
)}
</Animated.View>
</Animated.View>
</PinchGestureHandler>
</Animated.View>
);
},
)