Skip to content
This repository has been archived by the owner on Jan 6, 2023. It is now read-only.

Commit

Permalink
fix: layout jank
Browse files Browse the repository at this point in the history
  • Loading branch information
Andrew Smith authored and Andrew Smith committed Oct 2, 2019
1 parent c78f4d2 commit 470c172
Showing 7 changed files with 125 additions and 127 deletions.
11 changes: 6 additions & 5 deletions example/App.tsx
Original file line number Diff line number Diff line change
@@ -29,13 +29,14 @@ const App = () => {
}

return (
<View style={{flex: 1, backgroundColor: 'white'}}>
<SafeAreaView style={{flex: 1, justifyContent: 'center'}}>
<SafeAreaView style={{flex: 1}}>
<View
style={{flex: 1, backgroundColor: 'white', justifyContent: 'center'}}>
<PagerProvider activeIndex={activeIndex} onChange={onChange}>
<MyPager />
<SwipeCards />
</PagerProvider>
</SafeAreaView>
</View>
</View>
</SafeAreaView>
);
};

12 changes: 4 additions & 8 deletions example/src/basic-example.tsx
Original file line number Diff line number Diff line change
@@ -3,10 +3,10 @@ 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: 10000}, (_, i) => <Slide key={i} i={i} />);
const children = Array.from({length: 1000}, (_, i) => <Slide key={i} i={i} />);

function MyPager() {
const [activeIndex, onChange] = useState(5000);
const [activeIndex, onChange] = useState(500);

return (
<View>
@@ -19,13 +19,9 @@ function MyPager() {
</Text>

<Pager
style={{height: 200, width: 200, alignSelf: 'center'}}
activeIndex={activeIndex}
onChange={onChange}
style={{
height: 200,
width: 200,
alignSelf: 'center',
}}>
onChange={onChange}>
{children}
</Pager>

2 changes: 1 addition & 1 deletion example/src/stack.tsx
Original file line number Diff line number Diff line change
@@ -31,7 +31,7 @@ function Stack() {
enabled: activeIndex !== 0,
}}
onChange={onChange}
clamp={{prev: 0.3}}
clamp={{prev: 0.4}}
clampDrag={{next: 0}}
adjacentChildOffset={4}
style={{
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -49,7 +49,7 @@
"react-dom": "^16.9.0",
"react-native": "^0.60.5",
"react-native-gesture-handler": "^1.4.1",
"react-native-reanimated": "^1.2.0",
"react-native-reanimated": "^1.3.0",
"tsdx": "^0.9.1",
"tslib": "^1.10.0",
"typescript": "^3.6.2"
192 changes: 100 additions & 92 deletions src/pager.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
import React, {
useState,
Children,
useMemo,
createContext,
useContext,
useEffect,
memo,
useRef,
} from 'react';
import { StyleSheet, LayoutChangeEvent, ViewStyle } from 'react-native';
import Animated from 'react-native-reanimated';
import { StyleSheet, LayoutChangeEvent, ViewStyle, View } from 'react-native';
import Animated, { Easing } from 'react-native-reanimated';
import {
PanGestureHandler,
State,
@@ -61,6 +59,9 @@ export interface iPageInterpolation {
| iInterpolationFn;
}

const VERTICAL = 1;
const HORIZONTAL = 2;

const {
event,
block,
@@ -69,6 +70,7 @@ const {
cond,
eq,
add,
or,
stopClock,
Clock,
set,
@@ -85,10 +87,20 @@ const {
abs,
lessThan,
ceil,
proc,
timing,
and,
lessOrEq,
greaterOrEq,
// @ts-ignore
debug,
} = Animated;

const TIMING_CONFIG = {
duration: 200,
easing: Easing.out(Easing.cubic),
};

const DEFAULT_SPRING_CONFIG = {
stiffness: 1000,
damping: 500,
@@ -124,7 +136,6 @@ export interface iPager {
next?: number;
};
}

const REALLY_BIG_NUMBER = 1000000000;

function Pager({
@@ -139,8 +150,8 @@ function Pager({
minIndex = 0,
maxIndex: parentMax,
adjacentChildOffset = 10,
style,
animatedValue,
style,
type = 'horizontal',
pageInterpolation,
clamp = {
@@ -175,6 +186,7 @@ function Pager({
: context
? context[3]
: new Value(0);

const animatedIndex = memoize(_animatedIndex);

const [_activeIndex, _onChange] = useState(initialIndex);
@@ -206,14 +218,10 @@ function Pager({
? Math.ceil((numberOfScreens - 1) / pageSize)
: parentMax;

// set this up on first render, and update when the value from above changes
const maxIndex = memoize(new Value(maxIndexValue));

const maxIndexUpdateReq = useRef<undefined | number>(undefined);
useEffect(() => {
safelyUpdateValues(() => {
maxIndex.setValue(maxIndexValue);
}, maxIndexUpdateReq);
maxIndex.setValue(maxIndexValue);
}, [maxIndexValue]);

const dragX = memoize(new Value(0));
@@ -249,38 +257,37 @@ function Pager({
)
);

// these dimensions are used to compute the layout of child screens
// they are also used to determine the behaviour of the pager for things like
// drag threshold, and how large an individual page might be
const width = memoize(new Value(0));
const height = memoize(new Value(0));

const layoutRequest = useRef<undefined | number>(undefined);
const [width, setWidth] = useState(0);
const [height, setHeight] = useState(0);
const dimension = memoize(new Value(0));

function handleLayout({ nativeEvent: { layout } }: LayoutChangeEvent) {
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);
layout.width !== width && setWidth(layout.width);
layout.height !== height && setHeight(layout.height);
}

// correctly assign variables based on vertical / horizontal configurations
const TYPE = type === 'vertical' ? VERTICAL : HORIZONTAL;

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)),
set(translationValue, multiply(activeIndex, dimension, -1)),
]
),
[width, height]
);

// assign variables based on vertical / horizontal configurations
const targetDimension = type === 'vertical' ? 'height' : 'width';
const dimension = type === 'vertical' ? height : width;
const translateValue = type === 'vertical' ? 'translateY' : 'translateX';
const dragValue = type === 'vertical' ? dragY : dragX;

// compute the total size of a page to determine how far to snap
const page = useMemo(() => multiply(dimension, pageSize), [
dimension,
pageSize,
]);
const page = memoize(multiply(dimension, pageSize));

// only need one clock
const clock = memoize(new Clock());
@@ -325,8 +332,6 @@ function Pager({
),
spring(clock, state, config),
cond(state.finished, [stopClock(clock), set(state.time, 0)]),
// keep the animatedIndex value in sync with any changes to position
set(animatedIndex, divide(state.position, max(dimension, 1), -1)),
state.position,
]);
});
@@ -340,7 +345,7 @@ function Pager({

// position is a different name for the current index registered with the <Pager />
// it does not necessarily reflect the activeIndex in all states
const position = memoize(new Value(initialIndex));
const position = memoize(new Value(activeIndex));

// `nextPosition` is computed every frame (could be optimized probably)
// and the value is used on release to snap to a given index offset
@@ -350,16 +355,11 @@ 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 | number>(undefined);

useEffect(() => {
if (activeIndex >= minIndex && activeIndex <= maxIndexValue) {
safelyUpdateValues(() => {
nextPosition.setValue(activeIndex);
}, indexChangeRequest);
nextPosition.setValue(activeIndex);
}
}, [activeIndex, nextPosition]);
}, [activeIndex, maxIndexValue, minIndex]);

// compute the next snap point - it could be multiply screens depending
// on how far the user has dragged and what pageSize value is
@@ -443,6 +443,8 @@ function Pager({
]),
set(swiping, 0),
set(translationValue, runSpring(position)),
set(animatedIndex, divide(translationValue, max(dimension, 1), -1)),
translationValue,
]
),
])
@@ -498,54 +500,56 @@ function Pager({

return (
<Animated.View style={style || { flex: 1 }} onLayout={handleLayout}>
<PanGestureHandler
{...panProps}
onGestureEvent={handleGesture}
onHandlerStateChange={handleStateChange}
>
<Animated.View style={{ flex: 1 }}>
<Animated.View
style={{
...StyleSheet.absoluteFillObject,
[targetDimension]: totalDimension,
transform: [{ [translateValue]: translation }],
}}
>
{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 <Page /> components
let index = i;

if (adjacentChildOffset !== undefined) {
index =
activeIndex <= adjacentChildOffset
? i
: activeIndex - adjacentChildOffset + i;
}

return (
<Page
key={index}
index={index}
dimension={dimension}
translation={translation}
targetDimension={targetDimension}
translateValue={translateValue}
clampPrev={clampPrev}
clampNext={clampNext}
pageInterpolation={pageInterpolation}
>
<FocusProvider focused={index === activeIndex}>
{child}
</FocusProvider>
</Page>
);
})}
{width === 0 ? null : (
<PanGestureHandler
{...panProps}
onGestureEvent={handleGesture}
onHandlerStateChange={handleStateChange}
>
<Animated.View style={{ flex: 1 }}>
<Animated.View
style={{
...StyleSheet.absoluteFillObject,
[targetDimension]: totalDimension,
transform: [{ [translateValue]: translation }],
}}
>
{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 <Page /> components
let index = i;

if (adjacentChildOffset !== undefined) {
index =
activeIndex <= adjacentChildOffset
? i
: activeIndex - adjacentChildOffset + i;
}

return (
<Page
key={index}
index={index}
dimension={dimension}
translation={translation}
targetDimension={targetDimension}
translateValue={translateValue}
clampPrev={clampPrev}
clampNext={clampNext}
pageInterpolation={pageInterpolation}
>
<FocusProvider focused={index === activeIndex}>
{child}
</FocusProvider>
</Page>
);
})}
</Animated.View>
</Animated.View>
</Animated.View>
</PanGestureHandler>
</PanGestureHandler>
)}
</Animated.View>
);
}
@@ -668,7 +672,7 @@ function PagerProvider({
const onChange = isControlled ? parentOnChange : _setActiveIndex;

const animatedValue = memoize(new Value(0));
const animatedIndex = memoize(new Value(0));
const animatedIndex = memoize(new Value(activeIndex));

return (
<PagerContext.Provider
@@ -713,4 +717,8 @@ function useFocus() {
return focused;
}

const minMax = proc((value, minimum, maximum) =>
min(max(value, minimum), maximum)
);

export { Pager, PagerProvider, usePager, PagerContext, useFocus };
Loading

0 comments on commit 470c172

Please sign in to comment.