diff --git a/example/App.tsx b/example/App.tsx
index db427c3..36c9306 100644
--- a/example/App.tsx
+++ b/example/App.tsx
@@ -35,7 +35,7 @@ const App = () => {
-
+
diff --git a/mock.js b/mock.js
new file mode 100644
index 0000000..9c5f10b
--- /dev/null
+++ b/mock.js
@@ -0,0 +1,65 @@
+jest.mock('react-native-reanimated', () => {
+ const React = require('react');
+
+ const View = require('react-native/Libraries/Components/View/View');
+ const mock = require('react-native-reanimated/mock');
+
+ mock.default.Value = function() {
+ return {
+ setValue: function() {},
+ };
+ };
+
+ function MockView(props) {
+ React.useEffect(() => {
+ props.onLayout &&
+ props.onLayout({
+ nativeEvent: { layout: { width: 100, height: 100 } },
+ });
+ }, []);
+
+ return React.createElement(View, props, props.children);
+ }
+
+ mock.default.View = MockView;
+
+ mock.default.useCode = function() {};
+
+ return mock;
+});
+
+jest.mock('react-native-gesture-handler', () => {
+ const View = require('react-native/Libraries/Components/View/View');
+ const TouchableOpacity = require('react-native/Libraries/Components/Touchable/TouchableOpacity');
+ return {
+ Swipeable: View,
+ DrawerLayout: View,
+ State: {},
+ ScrollView: View,
+ Slider: View,
+ Switch: View,
+ TextInput: View,
+ ToolbarAndroid: View,
+ ViewPagerAndroid: View,
+ DrawerLayoutAndroid: View,
+ WebView: View,
+ NativeViewGestureHandler: View,
+ TapGestureHandler: View,
+ FlingGestureHandler: View,
+ ForceTouchGestureHandler: View,
+ LongPressGestureHandler: View,
+ PanGestureHandler: View,
+ PinchGestureHandler: View,
+ RotationGestureHandler: View,
+ /* Buttons */
+ RawButton: View,
+ BaseButton: View,
+ RectButton: View,
+ BorderlessButton: View,
+ TouchableOpacity: TouchableOpacity,
+ /* Other */
+ FlatList: View,
+ gestureHandlerRootHOC: jest.fn(),
+ Directions: {},
+ };
+});
diff --git a/package.json b/package.json
index a9e6e28..9849e7c 100644
--- a/package.json
+++ b/package.json
@@ -6,7 +6,8 @@
"typings": "dist/index.d.ts",
"author": "andrew smith",
"files": [
- "dist"
+ "dist",
+ "mock.js"
],
"repository": {
"type": "git",
diff --git a/src/pager.tsx b/src/pager.tsx
index 8d64ed2..f8ec235 100644
--- a/src/pager.tsx
+++ b/src/pager.tsx
@@ -4,7 +4,6 @@ import React, {
createContext,
useContext,
useEffect,
- memo,
} from 'react';
import { StyleSheet, LayoutChangeEvent, ViewStyle } from 'react-native';
import Animated from 'react-native-reanimated';
@@ -13,7 +12,6 @@ import {
State,
PanGestureHandlerProperties,
} from 'react-native-gesture-handler';
-import { memoize, interpolateWithConfig, runSpring } from './util';
export type SpringConfig = {
damping: Animated.Adaptable;
@@ -73,7 +71,6 @@ const {
cond,
eq,
add,
- or,
stopClock,
Clock,
set,
@@ -86,7 +83,12 @@ const {
greaterThan,
abs,
ceil,
- proc,
+ interpolate,
+ concat,
+ neq,
+ and,
+ startClock,
+ spring,
// @ts-ignore
debug,
} = Animated;
@@ -120,10 +122,6 @@ export interface iPager {
}
const REALLY_BIG_NUMBER = 1000000000;
-const minMax = proc((value, minimum, maximum) =>
- min(max(value, minimum), maximum)
-);
-
// at its core, this component converts an activeIndex integer value to an Animated.Value
// this animated value represents all intermediate values of a pager, e.g when a user is dragging, the index
// value might be anything between 1 -> 2 as they are moving. when a gesture is completed, it figures out
@@ -236,6 +234,8 @@ function Pager({
const targetTransform = type === 'vertical' ? 'translateY' : 'translateX';
const delta = type === 'vertical' ? dragY : dragX;
+ const layoutDimension = type === 'vertical' ? height : width;
+
// `totalDimension` on the container view is required for android layouts to work properly
// otherwise translations move the panHandler off of the screen
// set the total width of the container view to the sum width of all the screens
@@ -248,17 +248,6 @@ function Pager({
const TYPE = type === 'vertical' ? VERTICAL : HORIZONTAL;
- // it's important to use dimension as an animated value because the computations are memoized
- Animated.useCode(
- cond(
- // dimension already set to last layout
- or(eq(dimension, width), eq(dimension, height)),
- [],
- [cond(eq(TYPE, VERTICAL), set(dimension, height), set(dimension, width))]
- ),
- [width, height]
- );
-
// props that might change over time should be reactive:
const animatedThreshold = useAnimatedValue(threshold);
const clampDragPrev = useAnimatedValue(clampDrag.prev, REALLY_BIG_NUMBER);
@@ -288,7 +277,10 @@ function Pager({
// e.g prev => 0.5, next => 0.5 means change can only be between [-0.5, 0.5]
// minMax order is reversed because next is negative in translation values
const clampedDelta = memoize(
- minMax(divide(delta, dimension), multiply(clampDragNext, -1), clampDragPrev)
+ min(
+ max(divide(delta, dimension), multiply(clampDragNext, -1)),
+ clampDragPrev
+ )
);
const clock = memoize(new Clock());
@@ -326,14 +318,18 @@ function Pager({
nextIndex,
cond(
greaterThan(change, 0),
- minMax(
- sub(animatedActiveIndex, indexChange),
- animatedMinIndex,
+ min(
+ max(
+ sub(animatedActiveIndex, indexChange),
+ animatedMinIndex
+ ),
animatedMaxIndex
),
- minMax(
- add(animatedActiveIndex, indexChange),
- animatedMinIndex,
+ min(
+ max(
+ add(animatedActiveIndex, indexChange),
+ animatedMinIndex
+ ),
animatedMaxIndex
)
)
@@ -395,12 +391,69 @@ function Pager({
const defaultContainerStyle =
style && style.height ? { height: style.height } : undefined;
+ function renderChildren() {
+ // waiting for initial layout - except when testing
+ if (width === UNSET) {
+ return null;
+ }
+
+ return adjacentChildren.map((child: any, i) => {
+ // use map instead of React.Children because we want to track
+ // the keys of these children by there index
+ // React.Children shifts these key values intelligently, but it
+ // causes issues with the memoized values in components
+ let index = i;
+
+ if (adjacentChildOffset !== undefined) {
+ index =
+ activeIndex <= adjacentChildOffset
+ ? i
+ : activeIndex - adjacentChildOffset + i;
+ }
+
+ return (
+
+
+
+ {child}
+
+
+
+ );
+ });
+ }
+
// extra Animated.Views below may seem redundant but they preserve applied styles e.g padding and margin
// of the page views
return (
+
+
- {width === UNSET
- ? null
- : adjacentChildren.map((child: any, i) => {
- // use map instead of React.Children because we want to track
- // the keys of these children by there index
- // React.Children shifts these key values intelligently, but it
- // causes issues with the memoized values in components
- let index = i;
-
- if (adjacentChildOffset !== undefined) {
- index =
- activeIndex <= adjacentChildOffset
- ? i
- : activeIndex - adjacentChildOffset + i;
- }
-
- return (
-
-
-
- {child}
-
-
-
- );
- })}
+ {renderChildren()}
@@ -473,7 +492,7 @@ interface iPage {
animatedIndex: Animated.Value;
}
-function _Page({
+function Page({
children,
index,
minimum,
@@ -493,7 +512,7 @@ function _Page({
// min-max the position based on clamp values
// this means the will have a container that is always positioned
// in the same place, but the inner view can be translated within these bounds
- const translation = memoize(minMax(position, minimum, maximum));
+ const translation = memoize(min(max(position, minimum), maximum));
const defaultStyle = memoize({
// map to height / width value depending on vertical / horizontal configuration
@@ -542,15 +561,13 @@ function _Page({
);
}
-const Page = memo(_Page);
-
// utility to update animated values without changing their reference
// this is key for using memoized Animated.Values and prevents costly rerenders
function useAnimatedValue(
value?: number,
- defaultValue?: number
+ defaultValue = 0
): Animated.Value {
- const initialValue = value || defaultValue || 0;
+ const initialValue = value !== undefined ? value : defaultValue;
const animatedValue = memoize(new Value(initialValue));
useEffect(() => {
@@ -691,6 +708,105 @@ function useInterpolation(
return styles;
}
+function interpolateWithConfig(
+ offset: Animated.Node,
+ pageInterpolation?: iPageInterpolation
+): ViewStyle {
+ if (!pageInterpolation) {
+ return {};
+ }
+
+ return Object.keys(pageInterpolation).reduce((styles: any, key: any) => {
+ const currentStyle = pageInterpolation[key];
+
+ if (Array.isArray(currentStyle)) {
+ const _style = currentStyle.map((interpolationConfig: any) =>
+ interpolateWithConfig(offset, interpolationConfig)
+ );
+
+ styles[key] = _style;
+ return styles;
+ }
+
+ if (typeof currentStyle === 'object') {
+ let _style;
+ const { unit, ...rest } = currentStyle;
+ if (currentStyle.unit) {
+ _style = concat(interpolate(offset, rest), currentStyle.unit);
+ } else {
+ _style = interpolate(offset, currentStyle);
+ }
+
+ styles[key] = _style;
+ return styles;
+ }
+
+ if (typeof currentStyle === 'function') {
+ const _style = currentStyle(offset);
+ styles[key] = _style;
+ return styles;
+ }
+
+ return styles;
+ }, {});
+}
+
+function memoize(value: any): any {
+ const ref = React.useRef(value);
+ return ref.current;
+}
+
+const DEFAULT_SPRING_CONFIG = {
+ stiffness: 1000,
+ damping: 500,
+ mass: 3,
+ overshootClamping: false,
+ restDisplacementThreshold: 0.01,
+ restSpeedThreshold: 0.01,
+};
+
+function runSpring(
+ clock: Animated.Clock,
+ position: Animated.Value,
+ toValue: Animated.Node,
+ springConfig?: Partial
+) {
+ const state = {
+ finished: new Value(0),
+ velocity: new Value(0),
+ position: position,
+ time: new Value(0),
+ };
+
+ const config = {
+ ...DEFAULT_SPRING_CONFIG,
+ ...springConfig,
+ toValue: new Value(0),
+ };
+
+ return block([
+ cond(
+ clockRunning(clock),
+ [
+ cond(neq(config.toValue, toValue), [
+ set(state.finished, 0),
+ set(config.toValue, toValue),
+ ]),
+ ],
+ [
+ set(state.finished, 0),
+ set(state.time, 0),
+ set(state.velocity, 0),
+ set(config.toValue, toValue),
+ startClock(clock),
+ ]
+ ),
+ spring(clock, state, config),
+ cond(state.finished, [stopClock(clock), set(state.position, position)]),
+ state.position,
+ ]);
+}
+
export {
Pager,
PagerProvider,