Skip to content

Commit

Permalink
fix: prevent not syncing on tab item press and momentum scrolling
Browse files Browse the repository at this point in the history
  • Loading branch information
PedroBern committed Jan 26, 2021
1 parent 304e8cc commit 5c5ff49
Show file tree
Hide file tree
Showing 5 changed files with 143 additions and 88 deletions.
30 changes: 1 addition & 29 deletions src/MaterialTabBar/TabBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,15 @@ import { MaterialTabBarProps, ItemLayout } from './types'
export const TABBAR_HEIGHT = 48

const TabBar: React.FC<MaterialTabBarProps<any>> = ({
focusedTab,
refMap,
indexDecimal,
containerRef,
scrollEnabled = false,
indicatorStyle,
index,
TabItemComponent = TabItem,
tabItemProps = {},
getLabelText = (name) => name.toUpperCase(),
onTabPress,
}) => {
const tabBarRef = useAnimatedRef<Animated.ScrollView>()
const windowWidth = useWindowDimensions().width
Expand Down Expand Up @@ -63,33 +62,6 @@ const TabBar: React.FC<MaterialTabBarProps<any>> = ({
}
}, [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) {
Expand Down
2 changes: 1 addition & 1 deletion src/MaterialTabBar/TabItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ const TabItem: React.FC<MaterialTabItemProps<any>> = ({
styles.item,
style,
]}
onPress={() => onPress(index, name)}
onPress={() => onPress(name)}
android_ripple={{
borderless: true,
color: pressColor,
Expand Down
2 changes: 1 addition & 1 deletion src/MaterialTabBar/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export type MaterialTabItemProps<T extends string> = {
name: T
index: number
indexDecimal: Animated.SharedValue<number>
onPress: (index: number, name: T) => void
onPress: (name: T) => void
onLayout?: (event: LayoutChangeEvent) => void
scrollEnabled?: boolean
label: string
Expand Down
191 changes: 135 additions & 56 deletions src/createCollapsibleTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ const createCollapsibleTabs = <
const [headerHeight, setHeaderHeight] = React.useState<number | undefined>(
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))
Expand All @@ -85,12 +85,19 @@ const createCollapsibleTabs = <
const index = useSharedValue(0)
// @ts-ignore
const tabNames = useSharedValue<T[]>(Object.keys(refMap))
// @ts-ignore
const focusedTab = useSharedValue<T>(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<T>(() => {
return tabNames.value[index.value]
})

const isGliding = useSharedValue(false)

const getItemLayout = React.useCallback(
(_: unknown, index: number) => ({
Expand All @@ -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]
}
}
)
Expand All @@ -139,6 +150,12 @@ const createCollapsibleTabs = <
const { x } = event.contentOffset
scrollX.value = x
},
onBeginDrag: () => {
isSwiping.value = true
},
onMomentumEnd: () => {
isSwiping.value = false
},
},
[refMap]
)
Expand Down Expand Up @@ -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 (
<Context.Provider
value={{
Expand All @@ -269,6 +321,9 @@ const createCollapsibleTabs = <
containerHeight,
scrollX,
indexDecimal,
isGliding,
isSnapping,
snappingTo,
}}
>
<Animated.View
Expand Down Expand Up @@ -296,6 +351,7 @@ const createCollapsibleTabs = <
refMap={refMap}
focusedTab={focusedTab}
indexDecimal={indexDecimal}
onTabPress={onTabPress}
/>
)}
</View>
Expand All @@ -311,6 +367,7 @@ const createCollapsibleTabs = <
refMap={refMap}
focusedTab={focusedTab}
indexDecimal={indexDecimal}
onTabPress={onTabPress}
{...tabBarProps}
/>
)}
Expand Down Expand Up @@ -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) => {
Expand All @@ -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
}
Expand Down
6 changes: 5 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export type TabBarProps<T extends string> = {
refMap: Record<T, Ref>
index: Animated.SharedValue<number>
containerRef: React.RefObject<ContainerRef>
onTabPress: (name: T) => void
}

export type CollapsibleProps<
Expand Down Expand Up @@ -72,12 +73,15 @@ export type ContextType<T extends string> = {
oldAccScrollY: Animated.SharedValue<number>
accScrollY: Animated.SharedValue<number>
offset: Animated.SharedValue<number>
isScrolling: Animated.SharedValue<boolean | number>
isScrolling: Animated.SharedValue<boolean>
focusedTab: Animated.SharedValue<T>
accDiffClamp: Animated.SharedValue<number>
containerHeight?: number
scrollX: Animated.SharedValue<number>
indexDecimal: Animated.SharedValue<number>
isGliding: Animated.SharedValue<boolean>
isSnapping: Animated.SharedValue<boolean>
snappingTo: Animated.SharedValue<number>
}

export type TabProps<T extends string> = {
Expand Down

0 comments on commit 5c5ff49

Please sign in to comment.