Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/header touchables and horizontal scroll #254

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
2 changes: 2 additions & 0 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import Ref from './Ref'
import RevealHeaderOnScroll from './RevealHeaderOnScroll'
import RevealHeaderOnScrollSnap from './RevealHeaderOnScrollSnap'
import ScrollOnHeader from './ScrollOnHeader'
import ScrollOnHeaderWithTouchables from './ScrollOnHeaderWithTouchables'
import ScrollableTabs from './ScrollableTabs'
import Snap from './Snap'
import StartOnSpecificTab from './StartOnSpecificTab'
Expand All @@ -51,6 +52,7 @@ const EXAMPLE_COMPONENTS: ExampleComponentType[] = [
AnimatedHeader,
AndroidSharedPullToRefresh,
HeaderOverscrollExample,
ScrollOnHeaderWithTouchables,
]

const ExampleList: React.FC<object> = () => {
Expand Down
90 changes: 90 additions & 0 deletions example/src/ScrollOnHeaderWithTouchables.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import React, { useMemo } from 'react'
import {
StyleSheet,
View,
FlatList,
Text,
Alert,
TouchableOpacity,
} from 'react-native'

import { TabBarProps } from '../../src/types'
import ExampleComponent from './Shared/ExampleComponent'
import { ExampleComponentType } from './types'

const title = 'Scroll On Header with Touchables'

const SLIDER_ITEM_SIZE = 200
const HEADER_HEIGHT = SLIDER_ITEM_SIZE * 2

const data = Array.from({ length: 15 }).map((_, i) => i.toString())

const styles = StyleSheet.create({
item: {
width: SLIDER_ITEM_SIZE,
height: SLIDER_ITEM_SIZE,
alignItems: 'center',
justifyContent: 'center',
},
itemButton: {
padding: 16,
backgroundColor: 'white',
},
itemName: {
fontSize: 48,
color: 'black',
},
itemSeparator: { width: 4 },
})

const Slider = ({ isReversed = false }) => {
const config = useMemo(
() => ({
data: isReversed ? [...data].reverse() : data,
backgroundColor: isReversed ? 'purple' : 'orangered',
}),
[isReversed]
)

return (
<FlatList
andreialecu marked this conversation as resolved.
Show resolved Hide resolved
data={config.data}
horizontal
keyExtractor={(item) => item}
showsHorizontalScrollIndicator={false}
ItemSeparatorComponent={() => <View style={styles.itemSeparator} />}
bounces={false}
renderItem={({ item }) => (
<View
style={[styles.item, { backgroundColor: config.backgroundColor }]}
>
<TouchableOpacity
style={styles.itemButton}
onPress={() => Alert.alert(`Touchable number ${item} pressed`)}
>
<Text style={styles.itemName}>{item}</Text>
</TouchableOpacity>
</View>
)}
/>
)
}

const NewHeader: React.FC<TabBarProps> = () => {
return (
<View>
<Slider />
<Slider isReversed />
</View>
)
}

const DefaultExample: ExampleComponentType = () => {
return (
<ExampleComponent renderHeader={NewHeader} headerHeight={HEADER_HEIGHT} />
)
}

DefaultExample.title = title

export default DefaultExample
124 changes: 32 additions & 92 deletions src/Container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import {
LayoutChangeEvent,
StyleSheet,
useWindowDimensions,
View,
} from 'react-native'
import Animated, {
runOnJS,
Expand All @@ -18,9 +17,12 @@ import Animated, {
} from 'react-native-reanimated'

import { Context, TabNameContext } from './Context'
import { HeaderContainer } from './HeaderContainer'
import { Lazy } from './Lazy'
import { MaterialTabBar, TABBAR_HEIGHT } from './MaterialTabBar'
import { Tab } from './Tab'
import { TabBarContainer } from './TabBarContainer'
import { TopContainer } from './TopContainer'
import {
AnimatedFlatList,
IS_IOS,
Expand Down Expand Up @@ -125,6 +127,7 @@ export const Container = React.memo(
const oldAccScrollY: ContextType['oldAccScrollY'] = useSharedValue(0)
const accDiffClamp: ContextType['accDiffClamp'] = useSharedValue(0)
const isScrolling: ContextType['isScrolling'] = useSharedValue(0)
const isSlidingTopContainer = useSharedValue(false)
const scrollYCurrent: ContextType['scrollYCurrent'] = useSharedValue(0)
const scrollY: ContextType['scrollY'] = useSharedValue(
tabNamesArray.map(() => 0)
Expand Down Expand Up @@ -325,34 +328,6 @@ export const Container = React.memo(
: -Math.min(scrollYCurrent.value, headerScrollDistance.value)
}, [revealHeaderOnScroll])

const stylez = useAnimatedStyle(() => {
return {
transform: [
{
translateY: headerTranslateY.value,
},
],
}
}, [revealHeaderOnScroll])

const getHeaderHeight = React.useCallback(
(event: LayoutChangeEvent) => {
const height = event.nativeEvent.layout.height
if (headerHeight.value !== height) {
headerHeight.value = height
}
},
[headerHeight]
)

const getTabBarHeight = React.useCallback(
(event: LayoutChangeEvent) => {
const height = event.nativeEvent.layout.height
if (tabBarHeight.value !== height) tabBarHeight.value = height
},
[tabBarHeight]
)

const onLayout = React.useCallback(
(event: LayoutChangeEvent) => {
const height = event.nativeEvent.layout.height
Expand Down Expand Up @@ -388,8 +363,12 @@ export const Container = React.memo(
const onTabPress = React.useCallback(
(name: TabName) => {
// simplify logic by preventing index change
// when is scrolling or gliding.
if (!isScrolling.value && !isGliding.value) {
// when is scrolling, gliding, or scrolling the top container
if (
!isScrolling.value &&
!isGliding.value &&
!isSlidingTopContainer.value
) {
const i = tabNames.value.findIndex((n) => n === name)

if (name === focusedTab.value) {
Expand Down Expand Up @@ -473,55 +452,36 @@ export const Container = React.memo(
headerTranslateY,
width,
allowHeaderOverscroll,
isSlidingTopContainer,
}}
>
<Animated.View
style={[styles.container, { width }, containerStyle]}
onLayout={onLayout}
pointerEvents="box-none"
>
<Animated.View
pointerEvents="box-none"
style={[
styles.topContainer,
headerContainerStyle,
!cancelTranslation && stylez,
]}
<TopContainer
cancelTranslation={cancelTranslation}
headerContainerStyle={headerContainerStyle}
>
<View
style={[styles.container, styles.headerContainer]}
onLayout={getHeaderHeight}
pointerEvents="box-none"
>
{renderHeader &&
renderHeader({
containerRef,
index,
tabNames: tabNamesArray,
focusedTab,
indexDecimal,
onTabPress,
tabProps,
})}
</View>
<View
style={[styles.container, styles.tabBarContainer]}
onLayout={getTabBarHeight}
pointerEvents="box-none"
>
{renderTabBar &&
renderTabBar({
containerRef,
index,
tabNames: tabNamesArray,
focusedTab,
indexDecimal,
width,
onTabPress,
tabProps,
})}
</View>
</Animated.View>
<HeaderContainer
containerRef={containerRef}
onTabPress={onTabPress}
tabNamesArray={tabNamesArray}
tabProps={tabProps}
renderHeader={renderHeader}
/>

<TabBarContainer
containerRef={containerRef}
onTabPress={onTabPress}
tabNamesArray={tabNamesArray}
tabProps={tabProps}
width={width}
renderTabBar={renderTabBar}
/>
</TopContainer>

{headerHeight !== undefined && (
<AnimatedFlatList
ref={containerRef}
Expand Down Expand Up @@ -551,24 +511,4 @@ const styles = StyleSheet.create({
container: {
flex: 1,
},
topContainer: {
position: 'absolute',
zIndex: 100,
width: '100%',
backgroundColor: 'white',
shadowColor: '#000000',
shadowOffset: {
width: 0,
height: 2,
},
shadowOpacity: 0.23,
shadowRadius: 2.62,
elevation: 4,
},
tabBarContainer: {
zIndex: 1,
},
headerContainer: {
zIndex: 2,
},
})
55 changes: 55 additions & 0 deletions src/HeaderContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import React from 'react'
import { LayoutChangeEvent, StyleSheet, View } from 'react-native'

import { useTabsContext } from './hooks'
import { CollapsibleProps, TabBarProps, TabName } from './types'

type HeaderContainerProps<T extends TabName = TabName> = Pick<
CollapsibleProps,
'renderHeader'
> &
Pick<TabBarProps<T>, 'containerRef' | 'onTabPress' | 'tabProps'> & {
tabNamesArray: TabName[]
}

export const HeaderContainer: React.FC<HeaderContainerProps> = ({
renderHeader,
containerRef,
tabNamesArray,
onTabPress,
tabProps,
}) => {
const { headerHeight, focusedTab, index, indexDecimal } = useTabsContext()

const getHeaderHeight = React.useCallback(
(event: LayoutChangeEvent) => {
const height = event.nativeEvent.layout.height
if (headerHeight.value !== height) {
headerHeight.value = height
}
},
[headerHeight]
)

return (
<View style={[styles.container]} onLayout={getHeaderHeight}>
{renderHeader &&
renderHeader({
containerRef,
index,
tabNames: tabNamesArray,
focusedTab,
indexDecimal,
onTabPress,
tabProps,
})}
</View>
)
}

const styles = StyleSheet.create({
container: {
zIndex: 2,
flex: 1,
},
})
52 changes: 52 additions & 0 deletions src/TabBarContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import React from 'react'
import { LayoutChangeEvent, StyleSheet, View } from 'react-native'

import { useTabsContext } from './hooks'
import { CollapsibleProps, TabBarProps, TabName } from './types'

type TabBarContainerProps<T extends TabName = TabName> = Pick<
CollapsibleProps,
'renderTabBar' | 'width'
> &
Pick<TabBarProps<T>, 'onTabPress' | 'tabProps' | 'containerRef'> & {
tabNamesArray: TabName[]
}

export const TabBarContainer: React.FC<TabBarContainerProps> = ({
renderTabBar,
onTabPress,
tabProps,
tabNamesArray,
containerRef,
width,
}) => {
const { tabBarHeight, focusedTab, index, indexDecimal } = useTabsContext()

const getTabBarHeight = React.useCallback(
(event: LayoutChangeEvent) => {
const height = event.nativeEvent.layout.height
if (tabBarHeight.value !== height) tabBarHeight.value = height
},
[tabBarHeight]
)

return (
<View style={[styles.container]} onLayout={getTabBarHeight}>
{renderTabBar &&
renderTabBar({
containerRef,
index,
tabNames: tabNamesArray,
focusedTab,
indexDecimal,
width,
onTabPress,
tabProps,
})}
</View>
)
}

const styles = StyleSheet.create({
container: { flex: 1, zIndex: 1 },
})
Loading