Skip to content

Commit

Permalink
feat: add snapTimeout
Browse files Browse the repository at this point in the history
  • Loading branch information
andreialecu committed Dec 3, 2020
1 parent e9905e1 commit 3c476ca
Show file tree
Hide file tree
Showing 3 changed files with 67 additions and 25 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ All props are optional, but if you are not rendering a header, you'd be probably
| `disableSnap?` | Disable the snap animation. | `false` |
| `renderTabBar?` | Same as [renderTabBar](https://github.com/satya164/react-native-tab-view#rendertabbar) of the original [TabView](https://github.com/satya164/react-native-tab-view#tabview), but with the additional `isGliding` property. | `undefined` |
| `snapThreshold?` | Percentage of header height to make the snap effect. A number between 0 and 1. | `0.5` |
| `snapTimeout?` | How long to wait before initiating the snap effect, in milliseconds. | `100` |
| `onHeaderHeightChange?` | Callback fired when the `headerHeight` state value inside `CollapsibleTabView` will be updated in the `onLayout` event from the tab/header container.<br/><br/> Useful to call layout animations. Example:<br/><br/><pre lang="js">() => {LayoutAnimation.configureNext(preset)};</pre> | `undefined` |
| `routeKeyProp?` | The property from the `routes` map to use for the active route key. | `key` |

Expand Down
90 changes: 65 additions & 25 deletions src/CollapsibleTabView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,11 @@ export type Props<T extends Route> = Partial<TabViewProps<T>> &
* 0 and 1. Default is 0.5.
*/
snapThreshold?: number;
/**
* How long to wait before initiating the snap effect, in milliseconds.
* Default is 100
*/
snapTimeout?: number;
/**
* The property from the `routes` map to use for the active route key
* Default is 'key'
Expand All @@ -106,44 +111,65 @@ const CollapsibleTabView = <T extends Route>({
renderTabBar: customRenderTabBar,
onHeaderHeightChange,
snapThreshold = 0.5,
snapTimeout = 100,
routeKeyProp = 'key',
...tabViewProps
}: React.PropsWithoutRef<Props<T>>): React.ReactElement => {
const [headerHeight, setHeaderHeight] = React.useState(initialHeaderHeight);
const scrollY = React.useRef<Animated.Value>(animatedValue).current;
const scrollY = React.useRef(animatedValue);
const listRefArr = React.useRef<{ key: T['key']; value?: ScrollRef }[]>([]);
const listOffset = React.useRef<{ [key: string]: number }>({});
const isGliding = React.useRef(false);
/** Used to keep track if the user is actively scrolling */
const isUserScrolling = React.useRef(false);

const [canSnap, setCanSnap] = React.useState(false);

const activateSnapDebounced = useDebouncedCallback(
() => {
if (!isUserScrolling.current) {
setCanSnap(true);
}
},
snapTimeout,
{ trailing: true, leading: false }
);

const [translateY, setTranslateY] = React.useState(
headerHeight === 0
? 0
: scrollY.interpolate({
: scrollY.current.interpolate({
inputRange: [0, Math.max(headerHeight, 0)],
outputRange: [0, -headerHeight],
extrapolateRight: 'clamp',
})
);

React.useEffect(() => {
scrollY.addListener(({ value }) => {
const currY = scrollY.current;
currY.addListener(({ value }) => {
const curRoute = routes[index][routeKeyProp as keyof Route] as string;
listOffset.current[curRoute] = value;
// ensure we activate the snapping when scrollY stops changing (handled by debouncer)
activateSnapDebounced.callback();
});
return () => {
scrollY.removeAllListeners();
currY.removeAllListeners();
};
}, [routes, index, scrollY, routeKeyProp]);
}, [routes, index, routeKeyProp, activateSnapDebounced]);

/**
* Sync the scroll of unfocused routes to the current focused route,
* the default behavior is to snap to 0 or the `headerHeight`, it
* can be disabled with `disableSnap` prop.
*/
const syncScrollOffset = React.useCallback(() => {
React.useEffect(() => {
const curRouteKey = routes[index][routeKeyProp as keyof Route] as string;
const offset = listOffset.current[curRouteKey];

if (canSnap !== false) {
setCanSnap(false);
}
const newOffset: number | null =
offset >= 0 && offset <= headerHeight
? disableSnap
Expand All @@ -156,21 +182,25 @@ const CollapsibleTabView = <T extends Route>({
: null;

listRefArr.current.forEach((item) => {
const isCurrentRoute = item.key === curRouteKey;
const itemOffset = listOffset.current[item.key];

if (newOffset !== null) {
if ((disableSnap && item.key !== curRouteKey) || !disableSnap) {
scrollScene({
ref: item.value,
offset: newOffset,
animated: item.key === curRouteKey,
});
if ((disableSnap && !isCurrentRoute) || !disableSnap) {
if (newOffset !== itemOffset) {
scrollScene({
ref: item.value,
offset: newOffset,
animated: isCurrentRoute,
});
}
}
if (item.key !== curRouteKey) {
if (!isCurrentRoute) {
listOffset.current[item.key] = newOffset;
}
} else if (
item.key !== curRouteKey &&
(listOffset.current[item.key] < headerHeight ||
!listOffset.current[item.key])
!isCurrentRoute &&
(itemOffset < headerHeight || !itemOffset)
) {
scrollScene({
ref: item.value,
Expand All @@ -179,22 +209,31 @@ const CollapsibleTabView = <T extends Route>({
});
}
});
}, [routes, index, routeKeyProp, headerHeight, disableSnap, snapThreshold]);

const syncScrollOffsetDebounced = useDebouncedCallback(syncScrollOffset, 16);

}, [
routes,
index,
routeKeyProp,
headerHeight,
disableSnap,
snapThreshold,
canSnap,
]);
const onMomentumScrollBegin = () => {
isGliding.current = true;
syncScrollOffsetDebounced.cancel();
};

const onMomentumScrollEnd = () => {
isGliding.current = false;
syncScrollOffsetDebounced.callback();
};

const onScrollBeginDrag = () => {
isUserScrolling.current = true;
};

const onScrollEndDrag = () => {
syncScrollOffsetDebounced.callback();
isUserScrolling.current = false;
// make sure we snap if the user keeps his finger in the same position for a while then lifts it
activateSnapDebounced.callback();
};

/**
Expand Down Expand Up @@ -235,7 +274,7 @@ const CollapsibleTabView = <T extends Route>({
setTranslateY(
headerHeight === 0
? 0
: scrollY.interpolate({
: scrollY.current.interpolate({
inputRange: [0, Math.max(value, 0)],
outputRange: [0, -value],
extrapolateRight: 'clamp',
Expand Down Expand Up @@ -299,11 +338,12 @@ const CollapsibleTabView = <T extends Route>({
<CollapsibleContextProvider
value={{
activeRouteKey: routes[index][routeKeyProp as keyof Route] as string,
scrollY,
scrollY: scrollY.current,
buildGetRef,
headerHeight,
tabBarHeight,
onMomentumScrollBegin,
onScrollBeginDrag,
onScrollEndDrag,
onMomentumScrollEnd,
}}
Expand Down
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export type CollapsibleContext = {
onMomentumScrollBegin: (
event: NativeSyntheticEvent<NativeScrollEvent>
) => void;
onScrollBeginDrag: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
onScrollEndDrag: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
onMomentumScrollEnd: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
};
Expand Down

0 comments on commit 3c476ca

Please sign in to comment.