diff --git a/src/MaterialTabBar/TabBar.tsx b/src/MaterialTabBar/TabBar.tsx index 1b007b3c..04e95b61 100644 --- a/src/MaterialTabBar/TabBar.tsx +++ b/src/MaterialTabBar/TabBar.tsx @@ -21,16 +21,15 @@ import { MaterialTabBarProps, ItemLayout } from './types' export const TABBAR_HEIGHT = 48 const TabBar: React.FC> = ({ - focusedTab, refMap, indexDecimal, - containerRef, scrollEnabled = false, indicatorStyle, index, TabItemComponent = TabItem, tabItemProps = {}, getLabelText = (name) => name.toUpperCase(), + onTabPress, }) => { const tabBarRef = useAnimatedRef() const windowWidth = useWindowDimensions().width @@ -63,33 +62,6 @@ const TabBar: React.FC> = ({ } }, [scrollEnabled, nTabs, refMap, windowWidth]) - const onTabPress = React.useCallback( - (i: number, name: keyof typeof refMap) => { - if (name === focusedTab.value) { - // @ts-ignore - if (refMap[name].current?.scrollTo) { - // @ts-ignore - refMap[name].current?.scrollTo({ - x: 0, - y: 0, - animated: true, - }) - // @ts-ignore - } else if (refMap[name].current?.scrollToOffset) { - // @ts-ignore - refMap[name].current?.scrollToOffset({ - offset: 0, - animated: true, - }) - } - } else { - containerRef.current?.scrollToIndex({ animated: true, index: i }) - } - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [containerRef, refMap] - ) - const onTabItemLayout = React.useCallback( (event: LayoutChangeEvent) => { if (scrollEnabled && itemsLayout.length < nTabs) { diff --git a/src/MaterialTabBar/TabItem.tsx b/src/MaterialTabBar/TabItem.tsx index 1e513365..ef81d415 100644 --- a/src/MaterialTabBar/TabItem.tsx +++ b/src/MaterialTabBar/TabItem.tsx @@ -44,7 +44,7 @@ const TabItem: React.FC> = ({ styles.item, style, ]} - onPress={() => onPress(index, name)} + onPress={() => onPress(name)} android_ripple={{ borderless: true, color: pressColor, diff --git a/src/MaterialTabBar/types.ts b/src/MaterialTabBar/types.ts index e8a17a47..d6d84127 100644 --- a/src/MaterialTabBar/types.ts +++ b/src/MaterialTabBar/types.ts @@ -16,7 +16,7 @@ export type MaterialTabItemProps = { name: T index: number indexDecimal: Animated.SharedValue - onPress: (index: number, name: T) => void + onPress: (name: T) => void onLayout?: (event: LayoutChangeEvent) => void scrollEnabled?: boolean label: string diff --git a/src/createCollapsibleTabs.tsx b/src/createCollapsibleTabs.tsx index 2af12010..e4a43f92 100644 --- a/src/createCollapsibleTabs.tsx +++ b/src/createCollapsibleTabs.tsx @@ -74,7 +74,7 @@ const createCollapsibleTabs = < const [headerHeight, setHeaderHeight] = React.useState( initialHeaderHeight ) - const isScrolling = useSharedValue(-1) + const isScrolling = useSharedValue(false) const scrollX = useSharedValue(0) const scrollYCurrent = useSharedValue(0) const scrollY = useSharedValue([...new Array(children.length)].map(() => 0)) @@ -85,12 +85,19 @@ const createCollapsibleTabs = < const index = useSharedValue(0) // @ts-ignore const tabNames = useSharedValue(Object.keys(refMap)) - // @ts-ignore - const focusedTab = useSharedValue(tabNames.value[index.value]) const pagerOpacity = useSharedValue( initialHeaderHeight === undefined ? 0 : 1 ) const canUpdatePagerOpacity = useSharedValue(false) + const isSwiping = useSharedValue(false) + const isSnapping = useSharedValue(false) + const snappingTo = useSharedValue(0) + + const focusedTab = useDerivedValue(() => { + return tabNames.value[index.value] + }) + + const isGliding = useSharedValue(false) const getItemLayout = React.useCallback( (_: unknown, index: number) => ({ @@ -116,19 +123,23 @@ const createCollapsibleTabs = < } }, [containerRef, index.value, windowWidth]) - // derived from scrollX, to calculate the next offset and index + // derived from scrollX + // calculate the next offset and index if swiping + // if scrollX changes from tab press, + // the same logic must be done, but knowing + // the next index in advance useAnimatedReaction( () => { - const nextIndex = Math.round(indexDecimal.value) + const nextIndex = isSwiping.value + ? Math.round(indexDecimal.value) + : null return nextIndex }, (nextIndex) => { - if (nextIndex !== index.value) { + if (nextIndex !== null && nextIndex !== index.value) { offset.value = scrollY.value[index.value] - scrollY.value[nextIndex] + offset.value index.value = nextIndex - // @ts-ignore - focusedTab.value = tabNames.value[nextIndex] } } ) @@ -139,6 +150,12 @@ const createCollapsibleTabs = < const { x } = event.contentOffset scrollX.value = x }, + onBeginDrag: () => { + isSwiping.value = true + }, + onMomentumEnd: () => { + isSwiping.value = false + }, }, [refMap] ) @@ -246,6 +263,41 @@ const createCollapsibleTabs = < } }, []) + const onTabPress = React.useCallback( + (name: T) => { + // simplify logic by preventing index change + // when is scrolling or gliding. + if (!isScrolling.value && !isGliding.value) { + const i = tabNames.value.findIndex((n) => n === name) + offset.value = + scrollY.value[index.value] - scrollY.value[i] + offset.value + index.value = i + if (name === focusedTab.value) { + // @ts-ignore + if (refMap[name].current?.scrollTo) { + // @ts-ignore + refMap[name].current?.scrollTo({ + x: 0, + y: 0, + animated: true, + }) + // @ts-ignore + } else if (refMap[name].current?.scrollToOffset) { + // @ts-ignore + refMap[name].current?.scrollToOffset({ + offset: 0, + animated: true, + }) + } + } else { + containerRef.current?.scrollToIndex({ animated: true, index: i }) + } + } + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [containerRef, refMap] + ) + return ( )} @@ -311,6 +367,7 @@ const createCollapsibleTabs = < refMap={refMap} focusedTab={focusedTab} indexDecimal={indexDecimal} + onTabPress={onTabPress} {...tabBarProps} /> )} @@ -415,8 +472,15 @@ const createCollapsibleTabs = < index, scrollYCurrent, headerHeight, + isGliding, + isSnapping, + snappingTo, } = useTabsContext() + const [tabIndex] = React.useState( + tabNames.value.findIndex((n) => n === name) + ) + const scrollHandler = useAnimatedScrollHandler( { onScroll: (event) => { @@ -431,83 +495,98 @@ const createCollapsibleTabs = < onBeginDrag: () => { isScrolling.value = true }, + onEndDrag: () => { + isGliding.value = true + isScrolling.value = false + }, onMomentumEnd: () => { if (snapEnabled) { - if (diffClampEnabled) { - if (accDiffClamp.value > 0) { - if (scrollYCurrent.value > headerHeight) { - if (accDiffClamp.value <= headerHeight * snapThreshold) { - // snap down - accDiffClamp.value = withTiming(0) - } else if (accDiffClamp.value < headerHeight) { - // snap up - accDiffClamp.value = withTiming(headerHeight) - } - } else { - accDiffClamp.value = withTiming(0) + if (diffClampEnabled && accDiffClamp.value > 0) { + if (scrollYCurrent.value > headerHeight) { + if (accDiffClamp.value <= headerHeight * snapThreshold) { + // snap down + isSnapping.value = true + accDiffClamp.value = withTiming(0, undefined, () => { + isSnapping.value = false + }) + } else if (accDiffClamp.value < headerHeight) { + // snap up + isSnapping.value = true + accDiffClamp.value = withTiming( + headerHeight, + undefined, + () => { + isSnapping.value = false + } + ) } + } else { + isSnapping.value = true + accDiffClamp.value = withTiming(0, undefined, () => { + isSnapping.value = false + }) } } else { if (scrollYCurrent.value <= headerHeight * snapThreshold) { // snap down + snappingTo.value = 0 // @ts-ignore scrollTo(refMap[name], 0, 0, true) } else if (scrollYCurrent.value <= headerHeight) { // snap up + snappingTo.value = headerHeight // @ts-ignore scrollTo(refMap[name], 0, headerHeight, true) } + isSnapping.value = false } } - isScrolling.value = false + isGliding.value = false }, }, [headerHeight, name, diffClampEnabled, snapEnabled] ) - // sync unfocused tabs - useDerivedValue(() => { - if (isScrolling.value === false) { - const tabIndex = tabNames.value.findIndex((n) => n === name) - const tabScrollY = scrollY.value[tabIndex] - if (focusedTab.value !== name) { + // sync unfocused scenes + useAnimatedReaction( + () => { + return !isSnapping.value && !isScrolling.value && !isGliding.value + }, + (sync) => { + if (sync && focusedTab.value !== name) { let nextPosition = null - if ( - !diffClampEnabled && - scrollYCurrent.value <= headerHeight && - tabScrollY > headerHeight - ) { - // sync up - nextPosition = scrollYCurrent.value - } else if ( - diffClampEnabled && - (accDiffClamp.value > tabScrollY || tabScrollY <= headerHeight) - ) { - // todo perf if snapEnabled - nextPosition = accDiffClamp.value - } else if ( - tabScrollY < scrollYCurrent.value && - tabScrollY < headerHeight - ) { - // sync down - nextPosition = Math.min(headerHeight, scrollYCurrent.value) - } else if ( - tabScrollY <= headerHeight && - tabScrollY > scrollYCurrent.value && - scrollYCurrent.value < headerHeight - ) { - // sync up - nextPosition = scrollYCurrent.value + const focusedScrollY = scrollY.value[index.value] + const tabScrollY = scrollY.value[tabIndex] + const areEqual = focusedScrollY === tabScrollY + + if (!areEqual) { + const currIsOnTop = tabScrollY <= headerHeight + 1 + const focusedIsOnTop = focusedScrollY <= headerHeight + 1 + if (diffClampEnabled) { + const hasGap = accDiffClamp.value > tabScrollY + if (hasGap || currIsOnTop) { + nextPosition = accDiffClamp.value + } + } else if (snapEnabled) { + if (focusedIsOnTop) { + nextPosition = snappingTo.value + } else if (currIsOnTop) { + nextPosition = headerHeight + } + } else if (currIsOnTop || focusedIsOnTop) { + nextPosition = Math.min(focusedScrollY, headerHeight) + } } + if (nextPosition !== null) { scrollY.value[tabIndex] = nextPosition // @ts-ignore scrollTo(refMap[name], 0, nextPosition, false) } } - } - return 0 - }) + }, + [diffClampEnabled, snapEnabled, headerHeight] + ) return scrollHandler } diff --git a/src/types.ts b/src/types.ts index 209641c5..cc03244f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -23,6 +23,7 @@ export type TabBarProps = { refMap: Record index: Animated.SharedValue containerRef: React.RefObject + onTabPress: (name: T) => void } export type CollapsibleProps< @@ -72,12 +73,15 @@ export type ContextType = { oldAccScrollY: Animated.SharedValue accScrollY: Animated.SharedValue offset: Animated.SharedValue - isScrolling: Animated.SharedValue + isScrolling: Animated.SharedValue focusedTab: Animated.SharedValue accDiffClamp: Animated.SharedValue containerHeight?: number scrollX: Animated.SharedValue indexDecimal: Animated.SharedValue + isGliding: Animated.SharedValue + isSnapping: Animated.SharedValue + snappingTo: Animated.SharedValue } export type TabProps = {