react-native-pages-flipper
Version:
390 lines (389 loc) • 14.1 kB
JavaScript
import usePrevious from './hooks/usePrevious';
import useSetState from './hooks/useSetState';
import React, { useEffect, useState, useCallback, useMemo } from 'react';
import { useRef } from 'react';
import { StyleSheet, View } from 'react-native';
import { BookPage } from './BookPage';
import { BookPagePortrait, } from './portrait/BookPagePortrait';
import { createPages } from './utils/utils';
import { BookPageBackground } from './BookPageBackground';
const debug = true;
const logger = (msg) => {
if (debug) {
console.log(msg);
}
};
const PageFlipper = React.forwardRef(({ data, enabled = true, pressable = true, singleImageMode = true, renderLastPage, portrait = false, onFlippedEnd, onFlipStart, onPageDrag, onPageDragEnd, onPageDragStart, onEndReached, onInitialized, renderContainer, renderPage, pageSize = { height: 600, width: 400 }, contentContainerStyle, }, ref) => {
const [{ width, height }, setLayout] = useState({
height: 0,
width: 0,
});
const [state, setState] = useSetState({
pageIndex: 0,
pages: [],
isAnimating: false,
initialized: false,
// pageSize: { width: 0, height: 0 },
prev: { left: '', right: '' },
current: { left: '', right: '' },
next: { left: '', right: '' },
nextPageIndex: undefined,
isPortrait: portrait,
});
const isAnimatingRef = useRef(false);
const prevBookPage = useRef(null);
const nextBookPage = useRef(null);
const portraitBookPage = useRef(null);
const previousPortrait = usePrevious(portrait);
const containerSize = useMemo(() => {
if (!state.initialized) {
return {
width: 0,
height: 0,
};
}
let size = {
height: pageSize.height,
width: singleImageMode && !state.isPortrait
? pageSize.width * 2
: pageSize.width,
};
if (!singleImageMode && state.isPortrait) {
size = {
height: pageSize.height,
width: pageSize.width / 2,
};
}
let finalSize;
// corrections
if (size.height > size.width) {
const ratio = size.height / size.width;
finalSize = {
height: width * ratio,
width,
};
if (finalSize.height > height) {
const diff = finalSize.height / height;
finalSize.height = height;
finalSize.width = finalSize.width / diff;
}
}
else {
const ratio = size.width / size.height;
finalSize = {
height,
width: height * ratio,
};
if (finalSize.width > width) {
const diff = finalSize.width / width;
finalSize.width = width;
finalSize.height = finalSize.height / diff;
}
}
return finalSize;
}, [
height,
singleImageMode,
width,
state.initialized,
state.isPortrait,
pageSize.height,
pageSize.width,
]);
useEffect(() => {
const initialize = async () => {
try {
const allPages = createPages({
portrait,
singleImageMode,
data,
});
let adjustedIndex = getAdjustedIndex(allPages);
setState({
initialized: true,
pages: allPages,
prev: allPages[adjustedIndex - 1],
current: allPages[adjustedIndex],
next: allPages[adjustedIndex + 1],
pageIndex: adjustedIndex,
isPortrait: portrait,
});
if (onInitialized) {
onInitialized({
pages: allPages,
index: adjustedIndex,
});
}
}
catch (error) {
console.error('error', error);
}
};
initialize();
// eslint-disable-next-line
}, [data, portrait, singleImageMode]);
useEffect(() => {
if (state.nextPageIndex !== undefined) {
if (!state.isPortrait) {
if (state.nextPageIndex > state.pageIndex) {
nextBookPage.current?.turnPage();
}
else {
prevBookPage.current?.turnPage();
}
}
else {
portraitBookPage.current?.turnPage(state.nextPageIndex > state.pageIndex ? 1 : -1);
}
}
// eslint-disable-next-line
}, [state.nextPageIndex]);
const goToPage = useCallback((index) => {
if (index === undefined || index === null) {
logger('index cannot be undefined or null');
return;
}
if (typeof index !== 'number' || isNaN(index)) {
logger('index must be a number');
return;
}
if (index < 0 || index > state.pages.length - 1) {
logger('invalid page');
return;
}
if (isAnimatingRef.current) {
logger('is already animating');
return;
}
if (index === state.pageIndex) {
logger('same page');
return;
}
if (index > state.pageIndex) {
setState({
next: state.pages[index],
nextPageIndex: index,
});
}
else {
setState({
prev: state.pages[index],
nextPageIndex: index,
});
}
}, [setState, state.pageIndex, state.pages]);
const previousPage = useCallback(() => {
const newIndex = state.pageIndex - 1;
goToPage(newIndex);
}, [goToPage, state.pageIndex]);
const nextPage = useCallback(() => {
const newIndex = state.pageIndex + 1;
goToPage(newIndex);
}, [goToPage, state.pageIndex]);
React.useImperativeHandle(ref, () => ({
goToPage,
nextPage,
previousPage,
}), [goToPage, nextPage, previousPage]);
const getAdjustedIndex = (allPages) => {
// THIS NEEDS REWORKING
let adjustedIndex = state.pageIndex;
if (previousPortrait !== undefined &&
previousPortrait !== portrait &&
singleImageMode) {
if (portrait) {
adjustedIndex *= 2;
}
else {
adjustedIndex = Math.floor(adjustedIndex % 2 === 0
? adjustedIndex / 2
: (adjustedIndex - 1) / 2);
}
}
if (adjustedIndex < 0 || adjustedIndex > allPages.length - 1) {
// invalid index, reset to 0
adjustedIndex = 0;
}
return adjustedIndex;
};
const onLayout = (e) => {
setLayout({
height: e.nativeEvent.layout.height,
width: e.nativeEvent.layout.width,
});
};
const onPageFlipped = useCallback((index) => {
const newIndex = state.nextPageIndex !== undefined
? state.nextPageIndex
: state.pageIndex + index;
if (newIndex < 0 || newIndex > state.pages.length - 1) {
// this if condition theoretically should never occur in the first place, so it could be removed but it's here just in case
logger('invalid page');
setState({
isAnimating: false,
nextPageIndex: undefined,
});
isAnimatingRef.current = false;
return;
}
const prev = state.pages[newIndex - 1];
const current = state.pages[newIndex];
const next = state.pages[newIndex + 1];
setState({
pageIndex: newIndex,
isAnimating: false,
prev,
current,
next,
nextPageIndex: undefined,
});
isAnimatingRef.current = false;
if (onFlippedEnd && typeof onFlippedEnd === 'function') {
onFlippedEnd(newIndex);
}
if (newIndex === state.pages.length - 1 && onEndReached) {
onEndReached();
}
}, [
onEndReached,
onFlippedEnd,
setState,
state.nextPageIndex,
state.pageIndex,
state.pages,
]);
const setIsAnimating = useCallback((val) => {
setState({
isAnimating: val,
});
isAnimatingRef.current = val;
}, [setState]);
const getPageStyle = (right, front) => {
if (!singleImageMode && isPortrait) {
const pageStyle = {
height: containerSize.height,
width: containerSize.width * 2,
position: 'absolute',
};
const isEvenPage = pageIndex % 2 === 0;
if (front) {
if (isEvenPage) {
pageStyle.left = right ? 0 : -containerSize.width;
}
else {
pageStyle.left = right ? -containerSize.width : 0;
}
}
else {
if (isEvenPage) {
pageStyle.left = right ? -containerSize.width : 0;
}
else {
pageStyle.left = right ? 0 : -containerSize.width;
}
}
return pageStyle;
}
const pageStyle = {
height: containerSize.height,
width: singleImageMode && !isPortrait
? containerSize.width / 2
: containerSize.width,
position: 'absolute',
};
const offset = singleImageMode ? 0 : -containerSize.width / 2;
if (isPortrait && front) {
pageStyle.left = 0;
}
else if ((front && right) || (!front && right)) {
// front right or back right
pageStyle.left = offset;
}
else if (front && !right) {
// front left
pageStyle.right = offset;
}
return pageStyle;
};
if (!state.initialized) {
return null;
}
const { current, pageIndex, pages, next, prev, isPortrait, isAnimating, } = state;
const isFirstPage = pageIndex === 0;
const isLastPage = pageIndex === pages.length - 1;
const isSecondToLastPage = pageIndex === pages.length - 2;
const shouldRenderLastPage = (isSecondToLastPage || isLastPage) &&
singleImageMode &&
data.length % 2 !== 0;
const bookPageProps = {
containerSize: containerSize,
isAnimating: isAnimating,
enabled,
setIsAnimating: setIsAnimating,
isAnimatingRef: isAnimatingRef,
onPageFlip: onPageFlipped,
getPageStyle,
single: singleImageMode,
onFlipStart,
onPageDrag,
onPageDragEnd,
onPageDragStart,
isPressable: pressable,
renderPage,
};
const ContentWrapper = renderContainer ? renderContainer : Wrapper;
return (React.createElement(View, { style: styles.container, onLayout: onLayout },
React.createElement(View, { style: [
styles.contentContainer,
{
height: containerSize.height,
width: containerSize.width,
},
contentContainerStyle,
] },
React.createElement(ContentWrapper, null, !isPortrait ? (React.createElement(View, { style: styles.content },
!prev ? (React.createElement(Empty, null)) : (React.createElement(BookPage, { ref: prevBookPage, right: false, front: current, back: prev, key: `left${pageIndex}`, ...bookPageProps })),
!next ? (React.createElement(Empty, null)) : (React.createElement(BookPage, { ref: nextBookPage, right: true, front: current, back: next, key: `right${pageIndex}`, ...bookPageProps })),
React.createElement(BookPageBackground, { left: !prev ? current.left : prev.left, right: !next ? current.right : next.right, isFirstPage: isFirstPage, isLastPage: isLastPage, getPageStyle: getPageStyle, containerSize: containerSize, renderPage: renderPage, renderLastPage: renderLastPage, shouldRenderLastPage: shouldRenderLastPage }))) : (React.createElement(View, { style: styles.portraitContent },
React.createElement(View, { style: { ...StyleSheet.absoluteFillObject } },
React.createElement(BookPagePortrait, { ...bookPageProps, current: current, prev: prev, next: next, onPageFlip: onPageFlipped, key: `right${pageIndex}`, ref: portraitBookPage })),
next && (React.createElement(View, { style: {
...StyleSheet.absoluteFillObject,
zIndex: -5,
overflow: 'hidden',
} }, renderPage && (React.createElement(View, { style: getPageStyle(true, false) }, renderPage(next.right)))))))))));
});
export default React.memo(PageFlipper);
const Wrapper = (props) => React.createElement(View, { style: styles.wrap, ...props });
const Empty = () => React.createElement(View, { style: styles.container, pointerEvents: "none" });
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
wrap: {
flex: 1,
},
contentContainer: {
flexDirection: 'row',
// shadowColor: '#000',
// shadowOffset: {
// width: 0,
// height: 2,
// },
// shadowOpacity: 0.25,
// shadowRadius: 3.84,
// elevation: 5,
backgroundColor: 'white',
},
content: {
flex: 1,
flexDirection: 'row',
overflow: 'hidden',
},
portraitContent: {
flex: 1,
overflow: 'hidden',
},
});