diff --git a/README.md b/README.md
index 5b5a468..31ebbee 100644
--- a/README.md
+++ b/README.md
@@ -60,7 +60,7 @@ From App.js in /example directory
import React, { useState } from 'react';
import { StyleSheet, View, Text, TouchableOpacity } from 'react-native';
-import { Pager } from '@crowdlinker/react-native-pager';
+import { Pager, useFocus } from '@crowdlinker/react-native-pager';
const children = Array.from({ length: 1000 }, (_, i) => (
@@ -99,6 +99,7 @@ const colors = [
];
function Slide({ i }: { i: number }) {
+ const focused = useFocus();
return (
{`Screen: ${i}`}
+ {`Focused: ${focused}`}
);
}
diff --git a/example/App.tsx b/example/App.tsx
index 9c30aa6..1990c09 100644
--- a/example/App.tsx
+++ b/example/App.tsx
@@ -18,7 +18,6 @@ import {SwipeCards} from './src/swipe-cards';
import {Stack} from './src/stack';
import {Tabs} from './src/tabs';
import {MyPager} from './src/basic-example';
-import {SingleStackExample} from './src/single-stack-example';
import {PagerProvider} from '@crowdlinker/react-native-pager';
const App = () => {
diff --git a/example/src/basic-example.tsx b/example/src/basic-example.tsx
index d4fab63..450363a 100644
--- a/example/src/basic-example.tsx
+++ b/example/src/basic-example.tsx
@@ -3,13 +3,21 @@ import {StyleSheet, View, Text, TouchableOpacity} from 'react-native';
import {Pager} from '@crowdlinker/react-native-pager';
import {Slide, NavigationButtons} from './shared-components';
-const children = Array.from({length: 1000}, (_, i) => );
+const children = Array.from({length: 10000}, (_, i) => );
function MyPager() {
- const [activeIndex, onChange] = useState(400);
+ const [activeIndex, onChange] = useState(5000);
return (
+
+ {`Number of screens: ${children.length}`}
+
+
{children}
+
);
diff --git a/example/src/shared-components.tsx b/example/src/shared-components.tsx
index b93e2b1..fd12d69 100644
--- a/example/src/shared-components.tsx
+++ b/example/src/shared-components.tsx
@@ -1,5 +1,13 @@
import React, {useState} from 'react';
-import {Text, View, TouchableOpacity, StyleSheet} from 'react-native';
+import {
+ Text,
+ View,
+ TouchableOpacity,
+ StyleSheet,
+ TextInput,
+ Button,
+} from 'react-native';
+import {useFocus} from '@crowdlinker/react-native-pager';
const colors = [
'aquamarine',
@@ -12,8 +20,10 @@ const colors = [
'salmon',
];
-function Slide({i, focused}: {i: number; focused?: boolean}) {
- const [count, setCount] = useState(0);
+function Slide({i}: {i: number}) {
+ // const [count, setCount] = useState(0);
+ const focused = useFocus();
+
return (
-
-
-
-
-
-
-
-
-
-
- );
-}
-
-export {SingleStackExample};
diff --git a/example/src/stack.tsx b/example/src/stack.tsx
index 460ab85..a5bc8ba 100644
--- a/example/src/stack.tsx
+++ b/example/src/stack.tsx
@@ -55,7 +55,7 @@ function Stack() {
justifyContent: 'center',
alignItems: 'center',
}}>
- Push
+ Push
diff --git a/src/index.tsx b/src/index.tsx
index 44af19a..591955b 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -1,3 +1,2 @@
export * from './pager';
export * from './pagination';
-export * from './single-stack';
diff --git a/src/pager.tsx b/src/pager.tsx
index 19e714b..e09d414 100644
--- a/src/pager.tsx
+++ b/src/pager.tsx
@@ -6,7 +6,7 @@ import React, {
useContext,
useEffect,
memo,
- cloneElement,
+ useRef,
} from 'react';
import { StyleSheet, LayoutChangeEvent, ViewStyle } from 'react-native';
import Animated from 'react-native-reanimated';
@@ -15,7 +15,7 @@ import {
State,
PanGestureHandlerProperties,
} from 'react-native-gesture-handler';
-import { memoize, mapConfigToStyle } from './util';
+import { memoize, mapConfigToStyle, safelyUpdateValues } from './util';
export type SpringConfig = {
damping: Animated.Adaptable;
@@ -98,7 +98,7 @@ const DEFAULT_SPRING_CONFIG = {
restSpeedThreshold: 0.01,
};
-export interface PagerProps {
+export interface iPager {
activeIndex?: number;
onChange?: (nextIndex: number) => void;
initialIndex?: number;
@@ -152,7 +152,7 @@ function Pager({
next: REALLY_BIG_NUMBER,
},
animatedIndex: parentAnimatedIndex,
-}: PagerProps) {
+}: iPager) {
const context = useContext(PagerContext);
// register these props if they exist -- they can be shared with other
@@ -209,8 +209,11 @@ function Pager({
// set this up on first render, and update when the value from above changes
const maxIndex = memoize(new Value(maxIndexValue));
+ const maxIndexUpdateReq = useRef(undefined);
useEffect(() => {
- maxIndex.setValue(maxIndexValue);
+ safelyUpdateValues(() => {
+ maxIndex.setValue(maxIndexValue);
+ }, maxIndexUpdateReq);
}, [maxIndexValue]);
const dragX = memoize(new Value(0));
@@ -252,15 +255,19 @@ function Pager({
const width = memoize(new Value(0));
const height = memoize(new Value(0));
+ const layoutRequest = useRef(undefined);
+
function handleLayout({ nativeEvent: { layout } }: LayoutChangeEvent) {
- width.setValue(layout.width as any);
- height.setValue(layout.height as any);
-
- // this sets the initial offset to the correct translation w/o animation
- // e.g an initial index of 4 will be centered when layout registers
- translationValue.setValue((activeIndex *
- layout[targetDimension] *
- -1) as any);
+ safelyUpdateValues(() => {
+ width.setValue(layout.width as any);
+ height.setValue(layout.height as any);
+
+ // this sets the initial offset to the correct translation w/o animation
+ // e.g an initial index of 4 will be centered when layout registers
+ translationValue.setValue((activeIndex *
+ layout[targetDimension] *
+ -1) as any);
+ }, layoutRequest);
}
// correctly assign variables based on vertical / horizontal configurations
@@ -343,9 +350,14 @@ function Pager({
// not sure if Animated.useCode is any better here, it seemed to fire much more
// frequently than activeIndex was actually changing.
+
+ const indexChangeRequest = useRef(undefined);
+
useEffect(() => {
if (activeIndex >= minIndex && activeIndex <= maxIndexValue) {
- nextPosition.setValue(activeIndex);
+ safelyUpdateValues(() => {
+ nextPosition.setValue(activeIndex);
+ }, indexChangeRequest);
}
}, [activeIndex, nextPosition]);
@@ -525,7 +537,9 @@ function Pager({
clampNext={clampNext}
pageInterpolation={pageInterpolation}
>
- {cloneElement(child, { focused: activeIndex === index })}
+
+ {child}
+
);
})}
@@ -679,4 +693,24 @@ function usePager(): iPagerContext {
return context;
}
-export { Pager, PagerProvider, usePager, PagerContext };
+// provide hook for child screens to access pager focus:
+const FocusContext = React.createContext(false);
+
+interface iFocusProvider {
+ children: React.ReactNode;
+ focused: boolean;
+}
+
+function FocusProvider({ focused, children }: iFocusProvider) {
+ return (
+ {children}
+ );
+}
+
+function useFocus() {
+ const focused = useContext(FocusContext);
+
+ return focused;
+}
+
+export { Pager, PagerProvider, usePager, PagerContext, useFocus };
diff --git a/src/pagination.tsx b/src/pagination.tsx
index d74bc43..5d2688c 100644
--- a/src/pagination.tsx
+++ b/src/pagination.tsx
@@ -1,8 +1,8 @@
-import React, { Children } from 'react';
+import React, { Children, useRef } from 'react';
import Animated from 'react-native-reanimated';
import { ViewStyle, LayoutChangeEvent } from 'react-native';
import { iPageInterpolation, usePager } from './pager';
-import { memoize, mapConfigToStyle } from './util';
+import { memoize, mapConfigToStyle, safelyUpdateValues } from './util';
const { sub, Value, divide, multiply, add } = Animated;
@@ -105,9 +105,12 @@ function Slider({
: new Value(0);
const width = memoize(new Value(0));
+ const request = useRef(undefined);
function handleLayout({ nativeEvent: { layout } }: LayoutChangeEvent) {
- width.setValue(layout.width as any);
+ safelyUpdateValues(() => {
+ width.setValue(layout.width as any);
+ }, request);
}
const sliderWidth = divide(width, numberOfScreens);
@@ -142,9 +145,12 @@ function Progress({
: new Value(0);
const width = memoize(new Value(0));
+ const request = useRef(undefined);
function handleLayout({ nativeEvent: { layout } }: LayoutChangeEvent) {
- width.setValue(layout.width as any);
+ safelyUpdateValues(() => {
+ width.setValue(layout.width as any);
+ }, request);
}
const sliderWidth = divide(
diff --git a/src/single-stack.tsx b/src/single-stack.tsx
deleted file mode 100644
index b1ea09d..0000000
--- a/src/single-stack.tsx
+++ /dev/null
@@ -1,442 +0,0 @@
-import React, { Children, useEffect, useContext, useState } from 'react';
-import { PanGestureHandler, State } from 'react-native-gesture-handler';
-import Animated from 'react-native-reanimated';
-import { LayoutChangeEvent, StyleSheet, ViewStyle } from 'react-native';
-import { mapConfigToStyle, memoize, runSpring } from './util';
-import { iPageInterpolation, SpringConfig, PagerContext } from './pager';
-
-// SingleStack... can't think of a better name for this as of yet, it's likely to change...
-
-// this component manages the translation of two container views
-// and translates them in and out of the active scope based on provided
-// rootIndex and activeIndex values
-
-// the root view always stays in the center, as non-root views should only appear on
-// top of the root view -- this component is mainly for navigation structures where
-// there are multiple different, branched pathways to go to from a root view
-// but the child views aren't related to eachother and so should appear to operate independently
-
-// the two containers with non-root views also manage their children views, springing the active
-// view in and out of the window. this likely will only provide a layer of consistency,
-// in the wild these situations should rarely occur, as those kinds of transitions indicate
-// the two views are somehow related, and a different navigation component should probably
-// be used
-
-const {
- event,
- block,
- Value,
- divide,
- cond,
- eq,
- add,
- Clock,
- set,
- neq,
- sub,
- call,
- max,
- min,
- greaterThan,
- abs,
- multiply,
- // @ts-ignore
- debug,
- lessThan,
- floor,
- and,
- greaterOrEq,
- lessOrEq,
- ceil,
- diff,
-} = Animated;
-
-interface iSingleStack {
- children: React.ReactNode[];
- activeIndex?: number;
- onChange?: (nextIndex: number) => void;
- rootIndex: number;
- style?: ViewStyle;
- rootInterpolation?: iPageInterpolation;
- pageInterpolation?: iPageInterpolation;
- springConfig?: Partial;
- threshold?: number;
- type?: 'vertical' | 'horizontal';
-}
-
-function SingleStack({
- children,
- activeIndex: parentActiveIndex,
- onChange: parentOnChange,
- rootIndex,
- style,
- rootInterpolation,
- pageInterpolation,
- springConfig,
- threshold = 0.2,
- type = 'horizontal',
-}: iSingleStack) {
- const context = useContext(PagerContext);
- const [_activeIndex, _onChange] = useState(parentActiveIndex || 0);
- // assign activeIndex and onChange correctly based on controlled / uncontrolled
- // configurations
-
- // prioritize direct prop over context, and context over internal state
- const isControlled = parentActiveIndex !== undefined;
-
- const activeIndex = isControlled
- ? parentActiveIndex
- : context
- ? context[0]
- : (_activeIndex as any);
-
- const onChange = isControlled
- ? parentOnChange
- : context
- ? context[1]
- : (_onChange as any);
-
- const width = memoize(new Value(0));
- const height = memoize(new Value(0));
-
- function handleLayout({ nativeEvent: { layout } }: LayoutChangeEvent) {
- width.setValue(layout.width);
- height.setValue(layout.height);
- }
-
- const dragX = memoize(new Value(0));
- const dragY = memoize(new Value(0));
- const gestureState = memoize(new Value(-1));
-
- const handleGesture = memoize(
- event(
- [
- {
- nativeEvent: {
- translationX: dragX,
- translationY: dragY,
- },
- },
- ],
- { useNativeDriver: true }
- )
- );
-
- const handleStateChange = memoize(
- event(
- [
- {
- nativeEvent: {
- state: gestureState,
- },
- },
- ],
- {
- useNativeDriver: true,
- }
- )
- );
-
- const dimension = type === 'vertical' ? height : width;
- const translateValue = type === 'vertical' ? 'translateY' : 'translateX';
- const dragValue = type === 'vertical' ? dragY : dragX;
-
- const animatedIndex = memoize(new Value(activeIndex));
-
- // memoize the animated index and next position to prevent extra rerenders
- // in child components, using an integer prop that changes rapidly causes rerenders that
- // can be prevented with no loss to the state of the component by updating Animated.Values here
- useEffect(() => {
- // nextPosition should always be a value between -1 and 1
- const offset = activeIndex - rootIndex;
- const nextOffset = Math.max(Math.min(offset, 1), -1);
-
- nextPosition.setValue(nextOffset);
- animatedIndex.setValue(activeIndex);
- }, [activeIndex]);
-
- const initialOffset = activeIndex - rootIndex;
- const initialPosition = memoize(Math.max(Math.min(initialOffset, 1), -1));
-
- // position is the drag + active value ranging from -1 to 1
- // every child container and view computes its offset from this value
-
- // tracking and updating a single position is necessary to get smooth springing between indices
- // otherwise there is a bit of a jank when updating an activeIndex value
- // as there will always be a slight frame delay between states
- // e.g passing a dragEnd value to a child view on drag end and runSpring in that view always means
- // there will be a slight jank, can't quite figure out a better way to achieve smooth transitions
- // without tracking and sharing one value with all views
- // it's likely there is a slight jank still, just not so noticeable
- const position = memoize(new Value(initialPosition));
- const nextPosition = memoize(new Value(initialPosition));
-
- const clock = memoize(new Clock());
-
- const swiping = memoize(new Value(0));
- const dragStart = memoize(new Value(0));
- const percentageDragged = memoize(new Value(0));
- const roundedPosition = memoize(
- cond(lessThan(position, 0), floor(position), ceil(position))
- );
-
- const animatedOffset = memoize(
- block([
- cond(
- eq(gestureState, State.ACTIVE),
- [
- cond(swiping, 0, [set(dragStart, position)]),
-
- set(swiping, 1),
-
- set(
- percentageDragged,
- divide(abs(add(dragValue, dragStart)), max(dimension, 1))
- ),
-
- set(
- nextPosition,
- cond(greaterThan(percentageDragged, threshold), [0], [nextPosition])
- ),
-
- set(position, sub(dragStart, divide(dragValue, dimension))),
- ],
- [
- cond(neq(roundedPosition, nextPosition), [
- cond(swiping, call([], ([]) => onChange(rootIndex))),
- ]),
-
- set(swiping, 0),
- set(percentageDragged, 0),
-
- runSpring(clock, position, nextPosition, springConfig),
- ]
- ),
-
- position,
- ])
- );
-
- const interpolatedStyle = memoize(
- mapConfigToStyle(animatedOffset, rootInterpolation)
- );
-
- // these are used to compute the offset of the two containers
- // e.g offset = -width or offset = width
- const leftPosition = memoize(new Value(-1));
- const rightPosition = memoize(new Value(1));
-
- // this crazy computation clamps the value of container translate offsets (-1 -> 0)
- // between a given range -> either -1 to 0 or 0 to 1
- const clampLeft = memoize(min(max(sub(leftPosition, animatedOffset), -1), 0));
- const clampRight = memoize(
- max(min(sub(rightPosition, animatedOffset), 1), 0)
- );
-
- // divide the children up into container views
- const childrenLeft = children.slice(0, rootIndex);
- const childrenRight = children.slice(rootIndex + 1);
- const childCenter = children[rootIndex];
-
- const numberOfScreens = Children.count(children);
-
- return (
-
-
-
-
-
- {childrenLeft}
-
-
-
- {childCenter}
-
-
-
- {childrenRight}
-
-
-
-
-
- );
-}
-
-interface iPageContainer {
- children: React.ReactNode;
- dimension: Animated.Node;
- offset: Animated.Node;
- activeIndex: Animated.Node;
- pageInterpolation?: iPageInterpolation;
- minIndex: number;
- position: number;
- range: [number, number];
- swiping: Animated.Node;
- springConfig?: Partial;
- translateValue: 'translateX' | 'translateY';
-}
-
-function PageContainer({
- children,
- dimension,
- offset,
- pageInterpolation,
- activeIndex,
- minIndex,
- position,
- swiping,
- range,
- springConfig,
- translateValue,
-}: iPageContainer) {
- const translation = memoize(multiply(dimension, offset));
- const interpolatedStyle = memoize(
- mapConfigToStyle(offset, pageInterpolation)
- );
-
- return (
-
-
-
- {Children.map(children, (child: any, index: number) => (
-
- {child}
-
- ))}
-
-
-
- );
-}
-
-interface iPage {
- children: React.ReactNode;
- dimension: Animated.Node;
- index: number;
- parentOffset: number;
- activeIndex: Animated.Node;
- range: [number, number];
- swiping: Animated.Node;
- springConfig?: Partial;
- translateValue: 'translateX' | 'translateY';
-}
-
-function Page({
- children,
- dimension,
- parentOffset,
- activeIndex,
- index,
- swiping,
- range,
- springConfig,
- translateValue,
-}: iPage) {
- const clock = memoize(new Clock());
- const position = memoize(new Value(0));
- const zIndex = memoize(new Value(0));
-
- const offset = memoize(multiply(dimension, parentOffset));
-
- const isActive = memoize(eq(activeIndex, index));
- // if dragging value or active value is within range of this container
- const containerIsActive = memoize(
- and(greaterOrEq(activeIndex, range[0]), lessOrEq(activeIndex, range[1]))
- );
-
- // these updates are primarily to keep continuity in the UI for child views
- // it will keep the last rendered view on top as it translates off screen,
- // and set all views to be collapsed into the container view with no offsets
- // when the container becomes inactive
- const nextPosition = memoize(
- block([
- // set initial position
- cond(greaterThan(diff(dimension), 0), [
- set(position, cond(isActive, 0, offset)),
- ]),
-
- cond(
- swiping,
- cond(
- isActive,
- [set(position, 0)],
- [set(zIndex, 0), set(position, offset)]
- )
- ),
-
- // if the view is active (e.g it matches activeIndex), center it back in
- // the container
- cond(
- isActive,
- [set(zIndex, 1), 0],
- [set(zIndex, 0), cond(containerIsActive, offset, set(position, 0))]
- ),
- ])
- );
-
- const translation = memoize(
- runSpring(clock, position, nextPosition, springConfig)
- );
-
- return (
-
- {children}
-
- );
-}
-
-export { SingleStack };
diff --git a/src/util.ts b/src/util.ts
index 54221bc..fb44a2b 100644
--- a/src/util.ts
+++ b/src/util.ts
@@ -1,4 +1,4 @@
-import { useRef } from 'react';
+import { useRef, MutableRefObject } from 'react';
import { ViewStyle } from 'react-native';
import Animated from 'react-native-reanimated';
import { iPageInterpolation, SpringConfig } from './pager';
@@ -116,4 +116,22 @@ function runSpring(
]);
}
-export { mapConfigToStyle, memoize, runSpring };
+// prevents layout bugs on multiply calls to an Animated.Value.setValue()
+// setValue() issue: https://github.com/kmagiera/react-native-reanimated/issues/216
+// it's not entirely foolproof, not exactly sure how this works to be honest,
+// but from the issue above it appears to happen only in debug mode anyways
+function safelyUpdateValues(
+ fn: Function,
+ ref: MutableRefObject
+) {
+ if (ref.current) {
+ cancelAnimationFrame(ref.current);
+ }
+
+ ref.current = requestAnimationFrame(() => {
+ fn();
+ ref.current = undefined;
+ });
+}
+
+export { mapConfigToStyle, memoize, runSpring, safelyUpdateValues };