react-native-pages-flipper
Version:
387 lines (386 loc) • 12.7 kB
JavaScript
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { Pressable, StyleSheet, View } from 'react-native';
import { PanGestureHandler } from 'react-native-gesture-handler';
import Animated, {
Easing,
Extrapolation,
interpolate,
runOnJS,
useAnimatedStyle,
useDerivedValue,
useSharedValue,
withTiming,
} from 'react-native-reanimated';
import BackShadow from '../BookPage/BackShadow';
import FrontShadow from '../BookPage/FrontShadow';
import PageShadow from '../BookPage/PageShadow';
import { clamp, snapPoint } from '../utils/utils';
const timingConfig = {
duration: 800,
easing: Easing.inOut(Easing.cubic),
};
const BookPagePortrait = React.forwardRef(
(
{
current,
prev,
onPageFlip,
containerSize,
enabled,
isPressable,
setIsAnimating,
getPageStyle,
isAnimating,
isAnimatingRef,
next,
onFlipStart,
onPageDrag,
onPageDragEnd,
onPageDragStart,
renderPage,
},
ref
) => {
const containerWidth = containerSize.width;
const pSnapPoints = !prev
? [-containerSize.width, 0]
: [-containerSize.width, 0, containerSize.width];
const x = useSharedValue(0);
const isMounted = useRef(false);
const rotateYAsDeg = useSharedValue(0);
// might not need this
// useEffect(() => {
// if (!enabled) {
// setIsDragging(false);
// }
// }, [enabled]);
const turnPage = useCallback(
(id) => {
setIsAnimating(true);
if (onFlipStart && typeof onFlipStart === 'function') {
onFlipStart(id);
}
rotateYAsDeg.value = withTiming(
id < 0 ? -180 : 180,
timingConfig,
() => {
runOnJS(onPageFlip)(id, false);
}
);
},
[onFlipStart, onPageFlip, rotateYAsDeg, setIsAnimating]
);
React.useImperativeHandle(
ref,
() => ({
turnPage,
}),
[turnPage]
);
useEffect(() => {
isMounted.current = true;
return () => {
isMounted.current = false;
};
}, []);
const getDegreesForX = (x) => {
'worklet';
const val = interpolate(
x,
[-containerSize.width, 0, containerSize.width],
[180, 0, -180],
Extrapolation.CLAMP
);
return val;
};
const containerStyle = useAnimatedStyle(() => {
return {
flex: 1,
};
});
const onPanGestureHandler = useAnimatedGestureHandler({
// @ts-ignore
onStart: (event, ctx) => {
ctx.x = x.value;
if (onPageDragStart && typeof onPageDragStart === 'function') {
runOnJS(onPageDragStart)();
}
},
onActive: (event, ctx) => {
const newX = ctx.x + event.translationX;
const degrees = getDegreesForX(newX);
x.value = newX;
rotateYAsDeg.value = degrees;
if (onPageDrag && typeof onPageDrag === 'function') {
runOnJS(onPageDrag)();
}
},
onEnd: (event) => {
if (onPageDragEnd && typeof onPageDragEnd === 'function') {
runOnJS(onPageDragEnd)();
}
const snapTo = snapPoint(x.value, event.velocityX, pSnapPoints);
const id = snapTo > 0 ? -1 : snapTo < 0 ? 1 : 0;
if (!next && id > 0) {
// reset
x.value = withTiming(0);
rotateYAsDeg.value = withTiming(0);
// runOnJS(onDrag)(false);
return;
}
const degrees = getDegreesForX(snapTo);
x.value = snapTo;
if (rotateYAsDeg.value === degrees) {
// already same value
// debugValue('already there');
runOnJS(onPageFlip)(id, false);
} else {
runOnJS(setIsAnimating)(true);
const progress =
Math.abs(rotateYAsDeg.value - degrees) / 100;
const duration = clamp(
800 * progress - Math.abs(0.1 * event.velocityX),
350,
1000
);
rotateYAsDeg.value = withTiming(
degrees,
{
...timingConfig,
duration: duration,
},
() => {
if (snapTo === 0) {
//
}
runOnJS(onPageFlip)(id, false);
}
);
}
},
});
const gesturesEnabled = enabled && !isAnimating;
const iPageProps = {
containerSize,
containerWidth,
getPageStyle,
rotateYAsDeg,
renderPage,
};
return React.createElement(
Animated.View,
{ style: containerStyle },
React.createElement(
PanGestureHandler,
{
onGestureEvent: onPanGestureHandler,
enabled: gesturesEnabled,
},
React.createElement(
Animated.View,
{ style: containerStyle },
isPressable &&
prev &&
React.createElement(Pressable, {
disabled: isAnimating,
onPress: () => {
if (!isAnimatingRef.current) turnPage(-1);
},
style: {
position: 'absolute',
height: '100%',
width: '25%',
zIndex: 10000,
left: 0,
// backgroundColor: 'red',
// opacity: 0.2,
},
}),
isPressable &&
next &&
React.createElement(Pressable, {
disabled: isAnimating,
onPress: () => {
if (!isAnimatingRef.current) turnPage(1);
},
style: {
position: 'absolute',
height: '100%',
width: '30%',
zIndex: 10000,
right: 0,
// backgroundColor: 'blue',
// opacity: 0.2,
},
}),
current && next
? React.createElement(IPage, {
page: current,
right: true,
...iPageProps,
})
: React.createElement(
View,
{ style: { height: '100%', width: '100%' } },
renderPage &&
React.createElement(
View,
{ style: getPageStyle(true, true) },
renderPage(current.right)
)
),
prev &&
React.createElement(IPage, {
page: prev,
right: false,
...iPageProps,
})
)
)
);
}
);
const IPage = ({
right,
page,
rotateYAsDeg,
containerWidth,
containerSize,
getPageStyle,
renderPage,
}) => {
const [loaded, setLoaded] = useState(right);
useEffect(() => {
// hack fix
setTimeout(() => {
setLoaded(true);
}, 50);
}, []);
const rotationVal = useDerivedValue(() => {
const val = right
? rotateYAsDeg.value
: interpolate(rotateYAsDeg.value, [-180, 0], [0, 180]);
return val;
});
const portraitBackStyle = useAnimatedStyle(() => {
const x = interpolate(
rotationVal.value,
[0, 180],
[containerWidth, -containerWidth / 2]
);
const w = interpolate(
rotationVal.value,
[0, 180],
[0, containerWidth / 2]
);
return {
width: Math.ceil(w),
zIndex: 2,
opacity: 1,
transform: [{ translateX: x }],
};
});
const portraitFrontStyle = useAnimatedStyle(() => {
const w = interpolate(
rotationVal.value,
[0, 160],
[containerWidth, -20],
Extrapolation.CLAMP
);
const style = {
zIndex: 1,
width: Math.floor(w),
};
if (!right) {
style['left'] = 0;
} else {
// style['right'] = 0;
}
return style;
});
const frontPageStyle = getPageStyle(right, true);
const backPageStyle = getPageStyle(right, false);
if (!loaded) {
// hack fix
return null;
}
const shadowProps = {
right: true,
degrees: rotationVal,
width: containerSize.width,
viewHeight: containerSize.height,
};
return React.createElement(
View,
{
style: {
...StyleSheet.absoluteFillObject,
zIndex: !right ? 5 : 0,
},
},
React.createElement(
Animated.View,
{
style: [
styles.pageContainer,
portraitBackStyle,
{ overflow: 'visible' },
],
},
React.createElement(
View,
{ style: styles.pageContainer },
renderPage &&
React.createElement(
Animated.View,
{
style: [
backPageStyle,
{
opacity: 0.2,
transform: [
{ rotateX: '180deg' },
{ rotateZ: '180deg' },
],
},
],
},
renderPage(page.left)
)
),
React.createElement(BackShadow, {
degrees: rotationVal,
right: true,
}),
React.createElement(FrontShadow, { ...shadowProps }),
React.createElement(PageShadow, {
...shadowProps,
containerSize: containerSize,
})
),
React.createElement(
Animated.View,
{ style: [styles.pageContainer, portraitFrontStyle] },
renderPage &&
React.createElement(
Animated.View,
{ style: [frontPageStyle] },
renderPage(page.left)
)
)
);
};
export { BookPagePortrait };
const styles = StyleSheet.create({
container: {
flex: 1,
},
pageContainer: {
height: '100%',
width: '100%',
position: 'absolute',
backfaceVisibility: 'hidden',
overflow: 'hidden',
backgroundColor: 'white',
},
});