Closed dan-fein closed 3 years ago
Any thoughts on this? To be honest I fear it makes the header practically unusable if the two options are touchable or scrollable and they're mutually exclusive.
As long as the buttons are small enough, it can be scrolled from regions around them.
You need to experiment with pointerEvents
box-none
: https://reactnative.dev/docs/view#pointerevents
Scrolling from touchable regions I don't think is possible. FWIW the Reddit app on iOS has a similar collapsible header and initiating a scroll from the buttons in the header is impossible there either.
Did you try gesture handler buttons?
I just took the same header and same tab data and popped it out of the RN Collapsible Tab View component to see if ScrollView scrolling might work, and it does. I think this might be more of an implementation thing than something that has to do with the ability to scroll on a touchable component. Will keep exploring options and the gesture handler buttons.
If you put the header within the scrollview it will work, you can touch things just like you can in regular scrollview content.
However the header needs to be floating over the scrollview in order for anything to work (sticky tab bar, sticky header on scroll, and the fact that the header stays pinned and doesn't swipe with the pane left/right).
Without those features there's not much point to this library.
Right, the point there is that in general an element being both touchable & scrollable simultaneously, so the question is just, why within this library are those mutually exclusive? Can that be achieved, where buttons are both usable and on touch can be scrollable? Seems it should be achievable since it's proven to work elsewhere
The header floats over the scrollview. The scrollview can only scroll if the touch is within it. The header can either catch the touch or become transparent to it via pointerEvents
so that the scrollview underneath catches it.
In a purely native app this could be handled, via some effort, but with react native I don't know of a way to do it.
That makes sense, I'll see what can be done, I guess at this point it's my app that'll need reworking as I think this component is great and would like to continue to use it, thanks a lot!
@danielfein did you find any solution? We have the same issue within our app and didn't manage to solve it yet.
Same problem
I'm having the same issue, but I also want to be able to have a photo carousel as well on top, so seems like I have to choose between the two. Anyone know how IG does it? You can press on the buttons at the top and scroll the entire container along with having an infinite scroll list in each container.
Any updates? It would be the last feature missing for emulating apps like Instagram or Twitter 🙏
I have the solution!
On your header place: pointerEvents="box-none" instead of pointerEvents="none"
Then use BaseButton from react-native-gesture-handler instead of normal button.
My header Code
import {BaseButton} from 'react-native-gesture-handler';
<View style={{backgroundColor: 'white'}} pointerEvents="box-none">
<Text>COLLAPSIBLE</Text>
<BaseButton onPress={()=> {'your code'}} rippleColor='transparent'>
<Text style={{fontSize: 40}}>Test Button</Text>
</BaseButton>
</View>
Now you can scroll and use the buttons in the header.
This worked for me.
I have the solution!
On your header place: pointerEvents="box-none" instead of pointerEvents="none"
Then use BaseButton from react-native-gesture-handler instead of normal button.
My header Code
import {BaseButton} from 'react-native-gesture-handler';
<View style={{backgroundColor: 'white'}} pointerEvents="box-none"> <Text>COLLAPSIBLE</Text> <BaseButton onPress={()=> {'your code'}} rippleColor='transparent'> <Text style={{fontSize: 40}}>Test Button</Text> </BaseButton> </View>
Now you can scroll and use the buttons in the header.
This worked for me.
Hello, are you sure if you touch up inside the BaseButton
area you can also scroll the View
by holding down your finger?
I have the solution! On your header place: pointerEvents="box-none" instead of pointerEvents="none" Then use BaseButton from react-native-gesture-handler instead of normal button. My header Code import {BaseButton} from 'react-native-gesture-handler';
<View style={{backgroundColor: 'white'}} pointerEvents="box-none"> <Text>COLLAPSIBLE</Text> <BaseButton onPress={()=> {'your code'}} rippleColor='transparent'> <Text style={{fontSize: 40}}>Test Button</Text> </BaseButton> </View>
Now you can scroll and use the buttons in the header. This worked for me.
Hello, are you sure if you touch up inside the
BaseButton
area you can also scroll theView
by holding down your finger?
For me it worked try it yourself with the code I gave. The code is my collapsible header.
@TimDR-1356 How would you do other sort of touchable components like a horizontal scrollview? This seems more of a hack then a real solution...
@TimDR-1356 How would you do other sort of touchable components like a horizontal scrollview? This seems more of a hack then a real solution...
Idk I tried it with scrollview but didn't work maybe flatlist?
I think its a deal breaker to use this package It makes this package totally useless as it doesn't support touchable elements at the header I just switched to another package
I think its a deal breaker to use this package It makes this package totally useless as it doesn't support touchable elements at the header I just switched to another package
Which package did you try?
I think its a deal breaker to use this package It makes this package totally useless as it doesn't support touchable elements at the header I just switched to another package
Which package did you try?
I tried react-native-tab-view again You can use get ride of nesting virtualizedList inside scrollView error by using this hack
import React from 'react';
import { FlatList } from 'react-native';
import { Props } from './model';
const ScrollViewComponent: Props = props => {
return (
<FlatList
{...props}
data={[]}
keyExtractor={(e, i) => 'dom' + i.toString()}
ListEmptyComponent={null}
renderItem={null}
ListHeaderComponent={() => (
<>{props.children}</>
)}
/>
);
};
export default ScrollViewComponent;
Then try using react-native-tab-view version 2.16.0 instead of the latest version because the latest version shrink the tab inside the scrollView
It will let you have a full functional header alongside tabs which contains Flatlist and other type of elements Its not 100% same as this package, but it works
I have the solution! On your header place: pointerEvents="box-none" instead of pointerEvents="none" Then use BaseButton from react-native-gesture-handler instead of normal button. My header Code import {BaseButton} from 'react-native-gesture-handler';
<View style={{backgroundColor: 'white'}} pointerEvents="box-none"> <Text>COLLAPSIBLE</Text> <BaseButton onPress={()=> {'your code'}} rippleColor='transparent'> <Text style={{fontSize: 40}}>Test Button</Text> </BaseButton> </View>
Now you can scroll and use the buttons in the header. This worked for me.
Hello, are you sure if you touch up inside the
BaseButton
area you can also scroll theView
by holding down your finger?For me it worked try it yourself with the code I gave. The code is my collapsible header.
Yes, I also tried your code, but:
Text
component is scrollable if I add pointerEvents="none"
and it's okBaseButton
component is not scrollable because the touch event prevails and does not allow to scroll the component
I don't understand how it works for you 🤷I have the solution! On your header place: pointerEvents="box-none" instead of pointerEvents="none" Then use BaseButton from react-native-gesture-handler instead of normal button. My header Code import {BaseButton} from 'react-native-gesture-handler';
<View style={{backgroundColor: 'white'}} pointerEvents="box-none"> <Text>COLLAPSIBLE</Text> <BaseButton onPress={()=> {'your code'}} rippleColor='transparent'> <Text style={{fontSize: 40}}>Test Button</Text> </BaseButton> </View>
Now you can scroll and use the buttons in the header. This worked for me.
Hello, are you sure if you touch up inside the
BaseButton
area you can also scroll theView
by holding down your finger?For me it worked try it yourself with the code I gave. The code is my collapsible header.
Yes, I also tried your code, but:
Text
component is scrollable if I addpointerEvents="none"
and it's okBaseButton
component is not scrollable because the touch event prevails and does not allow to scroll the component I don't understand how it works for you 🤷
I am using V2 because I use react navigation, maybe that is the difference?
has anyone found a solution to this yet? 👀
any update on this? can't seem to scroll with the buttons in the header, and our buttons are pretty large.
I think its a deal breaker to use this package It makes this package totally useless as it doesn't support touchable elements at the header I just switched to another package
Which package did you try?
I tried react-native-tab-view again You can use get ride of nesting virtualizedList inside scrollView error by using this hack
import React from 'react'; import { FlatList } from 'react-native'; import { Props } from './model'; const ScrollViewComponent: Props = props => { return ( <FlatList {...props} data={[]} keyExtractor={(e, i) => 'dom' + i.toString()} ListEmptyComponent={null} renderItem={null} ListHeaderComponent={() => ( <>{props.children}</> )} /> ); }; export default ScrollViewComponent;
Then try using react-native-tab-view version 2.16.0 instead of the latest version because the latest version shrink the tab inside the scrollView
It will let you have a full functional header alongside tabs which contains Flatlist and other type of elements Its not 100% same as this package, but it works Adding on to this. It works with a simple scrollview. The only tradeoff is that the tab bar doesn't stick to the top. I'll take that over not being able to scroll or press things in the header. Hope I help save people some time and pain.
Use this:
yarn add react-native-tab-view@2.16.0
My code ended up looking super simple:
import { TabBar, TabView } from 'react-native-tab-view'
const MyComponent = ()=>{
const [index, setIndex] = useState(0)
return(
<View style={{ flex: 1 }}>
<MyPermanentTopHeader />
<ScrollView>
<MyCollapsibleHeader />
<TabView
navigationState={{ index, routes: [{ key: 'tab1', title: 'tab2' }, { key: 'tab1', title: 'tab2' }] }}
renderScene={({ route }) => {
switch (route.key) {
case 'tab1':
return <FlatList (with all my data) />
case 'tab2':
return <FlatList (with all my data ) />
default:
return null;
}
}}
onIndexChange={setIndex}
renderTabBar={props => <TabBar
{...props}
renderLabel={({ route }) => (
<Text style={{MyStyling}}>
{route.title}
</Text>
)}
indicatorStyle={{ backgroundColor: 'blue' }}
style={{ backgroundColor: 'white' }}
/>}
/>
</ScrollView>
</View>
)
in src/Container.tsx
patch
diff --git a/node_modules/react-native-collapsible-tab-view/src/Container.tsx b/node_modules/react-native-collapsible-tab-view/src/Container.tsx
index 1023290..403432e 100644
--- a/node_modules/react-native-collapsible-tab-view/src/Container.tsx
+++ b/node_modules/react-native-collapsible-tab-view/src/Container.tsx
@@ -5,16 +5,25 @@ import {
useWindowDimensions,
View,
} from 'react-native'
+import {
+ PanGestureHandler,
+ GestureHandlerRootView,
+ PanGestureHandlerGestureEvent,
+} from 'react-native-gesture-handler'
import PagerView from 'react-native-pager-view'
import Animated, {
runOnJS,
runOnUI,
+ Extrapolate,
+ interpolate,
useAnimatedReaction,
useAnimatedStyle,
useDerivedValue,
useSharedValue,
withDelay,
withTiming,
+ withDecay,
+ useAnimatedGestureHandler,
} from 'react-native-reanimated'
import { Context, TabNameContext } from './Context'
@@ -119,6 +128,7 @@ export const Container = React.memo(
const oldAccScrollY: ContextType['oldAccScrollY'] = useSharedValue(0)
const accDiffClamp: ContextType['accDiffClamp'] = useSharedValue(0)
const scrollYCurrent: ContextType['scrollYCurrent'] = useSharedValue(0)
+ const isTopContainerSynced = useSharedValue(true)
const scrollY: ContextType['scrollY'] = useSharedValue(
tabNamesArray.map(() => 0)
)
@@ -346,6 +356,63 @@ export const Container = React.memo(
[onTabPress]
)
+ const headerYCurrent = useSharedValue(0)
+
+ const gestureHandler = useAnimatedGestureHandler<
+ PanGestureHandlerGestureEvent,
+ { start: number }
+ >({
+ onStart: (_, ctx) => {
+ ctx.start = scrollY.value[index.value]
+ },
+ onActive: (event, ctx) => {
+ headerYCurrent.value = interpolate(
+ ctx.start - event.translationY,
+ [0, headerScrollDistance.value],
+ [0, headerScrollDistance.value],
+ Extrapolate.CLAMP
+ )
+ },
+ onEnd: (_) => {
+ headerYCurrent.value = withDecay(
+ {
+ velocity: -_.velocityY,
+ clamp: [0, headerScrollDistance.value],
+ deceleration: IS_IOS ? 0.998 : 0.99,
+ },
+ (finished) => {
+ isTopContainerSynced.value = finished || false
+ }
+ )
+ },
+ })
+
+ /* Syncs the scroll of the active tab once we complete the scroll gesture
+ on the header and the decay animation completes with success
+ */
+ useAnimatedReaction(
+ () => {
+ return isTopContainerSynced.value
+ },
+ (result) => {
+ if (!result) {
+ resyncTabScroll()
+ }
+ }
+ )
+
+ useAnimatedReaction(
+ () => headerYCurrent.value,
+ (y) => {
+ scrollY.value[index.value] = y
+ scrollYCurrent.value = y
+
+ for (const name of tabNamesArray) {
+ scrollToImpl(refMap[name], 0, y - contentInset.value, false)
+ }
+ }
+ )
+
return (
<Context.Provider
value={{
@@ -389,22 +456,29 @@ export const Container = React.memo(
!cancelTranslation && stylez,
]}
>
- <View
- style={[styles.container, styles.headerContainer]}
- onLayout={getHeaderHeight}
- pointerEvents="box-none"
- >
- {renderHeader &&
- renderHeader({
- containerRef,
- index,
- tabNames: tabNamesArray,
- focusedTab,
- indexDecimal,
- onTabPress,
- tabProps,
- })}
- </View>
+ <GestureHandlerRootView>
+ <PanGestureHandler
+ onGestureEvent={gestureHandler}
+ hitSlop={{ left: -20 }}
+ >
+ <Animated.View
+ style={[styles.container, styles.headerContainer]}
+ onLayout={getHeaderHeight}
+ pointerEvents="box-none"
+ >
+ {renderHeader &&
+ renderHeader({
+ containerRef,
+ index,
+ tabNames: tabNamesArray,
+ focusedTab,
+ indexDecimal,
+ onTabPress,
+ tabProps,
+ })}
+ </Animated.View>
+ </PanGestureHandler>
+ </GestureHandlerRootView>
<View
style={[styles.container, styles.tabBarContainer]}
onLayout={getTabBarHeight}
@see2ever it works to a certain extend, but it doesn't provides a snappy feels to it :(
@senghuotlay ends up with react-native-tab-view at v2.16.0 which can wrap a flatlist without warning
@see2ever could u share me your solution please
@senghuotlay fyi, https://github.com/see2ever/react-native-old-tab-view
just copy the src folder, and modify it
in
src/Container.tsx
patchdiff --git a/node_modules/react-native-collapsible-tab-view/src/Container.tsx b/node_modules/react-native-collapsible-tab-view/src/Container.tsx index 1023290..403432e 100644 --- a/node_modules/react-native-collapsible-tab-view/src/Container.tsx +++ b/node_modules/react-native-collapsible-tab-view/src/Container.tsx @@ -5,16 +5,25 @@ import { useWindowDimensions, View, } from 'react-native' +import { + PanGestureHandler, + GestureHandlerRootView, + PanGestureHandlerGestureEvent, +} from 'react-native-gesture-handler' import PagerView from 'react-native-pager-view' import Animated, { runOnJS, runOnUI, + Extrapolate, + interpolate, useAnimatedReaction, useAnimatedStyle, useDerivedValue, useSharedValue, withDelay, withTiming, + withDecay, + useAnimatedGestureHandler, } from 'react-native-reanimated' import { Context, TabNameContext } from './Context' @@ -119,6 +128,7 @@ export const Container = React.memo( const oldAccScrollY: ContextType['oldAccScrollY'] = useSharedValue(0) const accDiffClamp: ContextType['accDiffClamp'] = useSharedValue(0) const scrollYCurrent: ContextType['scrollYCurrent'] = useSharedValue(0) + const isTopContainerSynced = useSharedValue(true) const scrollY: ContextType['scrollY'] = useSharedValue( tabNamesArray.map(() => 0) ) @@ -346,6 +356,63 @@ export const Container = React.memo( [onTabPress] ) + const headerYCurrent = useSharedValue(0) + + const gestureHandler = useAnimatedGestureHandler< + PanGestureHandlerGestureEvent, + { start: number } + >({ + onStart: (_, ctx) => { + ctx.start = scrollY.value[index.value] + }, + onActive: (event, ctx) => { + headerYCurrent.value = interpolate( + ctx.start - event.translationY, + [0, headerScrollDistance.value], + [0, headerScrollDistance.value], + Extrapolate.CLAMP + ) + }, + onEnd: (_) => { + headerYCurrent.value = withDecay( + { + velocity: -_.velocityY, + clamp: [0, headerScrollDistance.value], + deceleration: IS_IOS ? 0.998 : 0.99, + }, + (finished) => { + isTopContainerSynced.value = finished || false + } + ) + }, + }) + + /* Syncs the scroll of the active tab once we complete the scroll gesture + on the header and the decay animation completes with success + */ + useAnimatedReaction( + () => { + return isTopContainerSynced.value + }, + (result) => { + if (!result) { + resyncTabScroll() + } + } + ) + + useAnimatedReaction( + () => headerYCurrent.value, + (y) => { + scrollY.value[index.value] = y + scrollYCurrent.value = y + + for (const name of tabNamesArray) { + scrollToImpl(refMap[name], 0, y - contentInset.value, false) + } + } + ) + return ( <Context.Provider value={{ @@ -389,22 +456,29 @@ export const Container = React.memo( !cancelTranslation && stylez, ]} > - <View - style={[styles.container, styles.headerContainer]} - onLayout={getHeaderHeight} - pointerEvents="box-none" - > - {renderHeader && - renderHeader({ - containerRef, - index, - tabNames: tabNamesArray, - focusedTab, - indexDecimal, - onTabPress, - tabProps, - })} - </View> + <GestureHandlerRootView> + <PanGestureHandler + onGestureEvent={gestureHandler} + hitSlop={{ left: -20 }} + > + <Animated.View + style={[styles.container, styles.headerContainer]} + onLayout={getHeaderHeight} + pointerEvents="box-none" + > + {renderHeader && + renderHeader({ + containerRef, + index, + tabNames: tabNamesArray, + focusedTab, + indexDecimal, + onTabPress, + tabProps, + })} + </Animated.View> + </PanGestureHandler> + </GestureHandlerRootView> <View style={[styles.container, styles.tabBarContainer]} onLayout={getTabBarHeight}
you saved my ass bro. thanks a lot 👍
in
src/Container.tsx
patchdiff --git a/node_modules/react-native-collapsible-tab-view/src/Container.tsx b/node_modules/react-native-collapsible-tab-view/src/Container.tsx index 1023290..403432e 100644 --- a/node_modules/react-native-collapsible-tab-view/src/Container.tsx +++ b/node_modules/react-native-collapsible-tab-view/src/Container.tsx @@ -5,16 +5,25 @@ import { useWindowDimensions, View, } from 'react-native' +import { + PanGestureHandler, + GestureHandlerRootView, + PanGestureHandlerGestureEvent, +} from 'react-native-gesture-handler' import PagerView from 'react-native-pager-view' import Animated, { runOnJS, runOnUI, + Extrapolate, + interpolate, useAnimatedReaction, useAnimatedStyle, useDerivedValue, useSharedValue, withDelay, withTiming, + withDecay, + useAnimatedGestureHandler, } from 'react-native-reanimated' import { Context, TabNameContext } from './Context' @@ -119,6 +128,7 @@ export const Container = React.memo( const oldAccScrollY: ContextType['oldAccScrollY'] = useSharedValue(0) const accDiffClamp: ContextType['accDiffClamp'] = useSharedValue(0) const scrollYCurrent: ContextType['scrollYCurrent'] = useSharedValue(0) + const isTopContainerSynced = useSharedValue(true) const scrollY: ContextType['scrollY'] = useSharedValue( tabNamesArray.map(() => 0) ) @@ -346,6 +356,63 @@ export const Container = React.memo( [onTabPress] ) + const headerYCurrent = useSharedValue(0) + + const gestureHandler = useAnimatedGestureHandler< + PanGestureHandlerGestureEvent, + { start: number } + >({ + onStart: (_, ctx) => { + ctx.start = scrollY.value[index.value] + }, + onActive: (event, ctx) => { + headerYCurrent.value = interpolate( + ctx.start - event.translationY, + [0, headerScrollDistance.value], + [0, headerScrollDistance.value], + Extrapolate.CLAMP + ) + }, + onEnd: (_) => { + headerYCurrent.value = withDecay( + { + velocity: -_.velocityY, + clamp: [0, headerScrollDistance.value], + deceleration: IS_IOS ? 0.998 : 0.99, + }, + (finished) => { + isTopContainerSynced.value = finished || false + } + ) + }, + }) + + /* Syncs the scroll of the active tab once we complete the scroll gesture + on the header and the decay animation completes with success + */ + useAnimatedReaction( + () => { + return isTopContainerSynced.value + }, + (result) => { + if (!result) { + resyncTabScroll() + } + } + ) + + useAnimatedReaction( + () => headerYCurrent.value, + (y) => { + scrollY.value[index.value] = y + scrollYCurrent.value = y + + for (const name of tabNamesArray) { + scrollToImpl(refMap[name], 0, y - contentInset.value, false) + } + } + ) + return ( <Context.Provider value={{ @@ -389,22 +456,29 @@ export const Container = React.memo( !cancelTranslation && stylez, ]} > - <View - style={[styles.container, styles.headerContainer]} - onLayout={getHeaderHeight} - pointerEvents="box-none" - > - {renderHeader && - renderHeader({ - containerRef, - index, - tabNames: tabNamesArray, - focusedTab, - indexDecimal, - onTabPress, - tabProps, - })} - </View> + <GestureHandlerRootView> + <PanGestureHandler + onGestureEvent={gestureHandler} + hitSlop={{ left: -20 }} + > + <Animated.View + style={[styles.container, styles.headerContainer]} + onLayout={getHeaderHeight} + pointerEvents="box-none" + > + {renderHeader && + renderHeader({ + containerRef, + index, + tabNames: tabNamesArray, + focusedTab, + indexDecimal, + onTabPress, + tabProps, + })} + </Animated.View> + </PanGestureHandler> + </GestureHandlerRootView> <View style={[styles.container, styles.tabBarContainer]} onLayout={getTabBarHeight}
That is a pretty good solution, but it kind of disables the pull-to-refresh 😕
in
src/Container.tsx
patchdiff --git a/node_modules/react-native-collapsible-tab-view/src/Container.tsx b/node_modules/react-native-collapsible-tab-view/src/Container.tsx index 1023290..403432e 100644 --- a/node_modules/react-native-collapsible-tab-view/src/Container.tsx +++ b/node_modules/react-native-collapsible-tab-view/src/Container.tsx @@ -5,16 +5,25 @@ import { useWindowDimensions, View, } from 'react-native' +import { + PanGestureHandler, + GestureHandlerRootView, + PanGestureHandlerGestureEvent, +} from 'react-native-gesture-handler' import PagerView from 'react-native-pager-view' import Animated, { runOnJS, runOnUI, + Extrapolate, + interpolate, useAnimatedReaction, useAnimatedStyle, useDerivedValue, useSharedValue, withDelay, withTiming, + withDecay, + useAnimatedGestureHandler, } from 'react-native-reanimated' import { Context, TabNameContext } from './Context' @@ -119,6 +128,7 @@ export const Container = React.memo( const oldAccScrollY: ContextType['oldAccScrollY'] = useSharedValue(0) const accDiffClamp: ContextType['accDiffClamp'] = useSharedValue(0) const scrollYCurrent: ContextType['scrollYCurrent'] = useSharedValue(0) + const isTopContainerSynced = useSharedValue(true) const scrollY: ContextType['scrollY'] = useSharedValue( tabNamesArray.map(() => 0) ) @@ -346,6 +356,63 @@ export const Container = React.memo( [onTabPress] ) + const headerYCurrent = useSharedValue(0) + + const gestureHandler = useAnimatedGestureHandler< + PanGestureHandlerGestureEvent, + { start: number } + >({ + onStart: (_, ctx) => { + ctx.start = scrollY.value[index.value] + }, + onActive: (event, ctx) => { + headerYCurrent.value = interpolate( + ctx.start - event.translationY, + [0, headerScrollDistance.value], + [0, headerScrollDistance.value], + Extrapolate.CLAMP + ) + }, + onEnd: (_) => { + headerYCurrent.value = withDecay( + { + velocity: -_.velocityY, + clamp: [0, headerScrollDistance.value], + deceleration: IS_IOS ? 0.998 : 0.99, + }, + (finished) => { + isTopContainerSynced.value = finished || false + } + ) + }, + }) + + /* Syncs the scroll of the active tab once we complete the scroll gesture + on the header and the decay animation completes with success + */ + useAnimatedReaction( + () => { + return isTopContainerSynced.value + }, + (result) => { + if (!result) { + resyncTabScroll() + } + } + ) + + useAnimatedReaction( + () => headerYCurrent.value, + (y) => { + scrollY.value[index.value] = y + scrollYCurrent.value = y + + for (const name of tabNamesArray) { + scrollToImpl(refMap[name], 0, y - contentInset.value, false) + } + } + ) + return ( <Context.Provider value={{ @@ -389,22 +456,29 @@ export const Container = React.memo( !cancelTranslation && stylez, ]} > - <View - style={[styles.container, styles.headerContainer]} - onLayout={getHeaderHeight} - pointerEvents="box-none" - > - {renderHeader && - renderHeader({ - containerRef, - index, - tabNames: tabNamesArray, - focusedTab, - indexDecimal, - onTabPress, - tabProps, - })} - </View> + <GestureHandlerRootView> + <PanGestureHandler + onGestureEvent={gestureHandler} + hitSlop={{ left: -20 }} + > + <Animated.View + style={[styles.container, styles.headerContainer]} + onLayout={getHeaderHeight} + pointerEvents="box-none" + > + {renderHeader && + renderHeader({ + containerRef, + index, + tabNames: tabNamesArray, + focusedTab, + indexDecimal, + onTabPress, + tabProps, + })} + </Animated.View> + </PanGestureHandler> + </GestureHandlerRootView> <View style={[styles.container, styles.tabBarContainer]} onLayout={getTabBarHeight}
Pretty awesome, definitely works but like others have mentioned it isn't the smoothest experience. I'm surprised there isn't more focus on this issue as it seems to be a deal breaker for many. I also didn't really like react-native-tab-view
because you're forced to use an older version and the content sizes across the different tabs are also a bit janky.
Anyone else have an implementation that worked well for them?
@antochan In my case, I had this issue solved with an old version of react-native-tab-views. But after migrating to the new React Native arch and upgrading reanimated, due to incompatibilities, I ended up trying this lib.
The error you mention is called 'flexbox equal heights'. To solve it, just apply a display: none
to the inactive tab scenes.
Btw, this library needs a way to handle this problem. It sucks.
+1
I have the solution!
On your header place: pointerEvents="box-none" instead of pointerEvents="none"
Then use BaseButton from react-native-gesture-handler instead of normal button.
My header Code
import {BaseButton} from 'react-native-gesture-handler';
<View style={{backgroundColor: 'white'}} pointerEvents="box-none"> <Text>COLLAPSIBLE</Text> <BaseButton onPress={()=> {'your code'}} rippleColor='transparent'> <Text style={{fontSize: 40}}>Test Button</Text> </BaseButton> </View>
Now you can scroll and use the buttons in the header.
This worked for me.
This thing works for me I put pointeEvent="box-none"
to every child View
and replaced TouchableOpacity
with BaseButton
. now it works fine. I thought it works just by wrapping the Parent View
only. :grin:
in
src/Container.tsx
patchdiff --git a/node_modules/react-native-collapsible-tab-view/src/Container.tsx b/node_modules/react-native-collapsible-tab-view/src/Container.tsx index 1023290..403432e 100644 --- a/node_modules/react-native-collapsible-tab-view/src/Container.tsx +++ b/node_modules/react-native-collapsible-tab-view/src/Container.tsx @@ -5,16 +5,25 @@ import { useWindowDimensions, View, } from 'react-native' +import { + PanGestureHandler, + GestureHandlerRootView, + PanGestureHandlerGestureEvent, +} from 'react-native-gesture-handler' import PagerView from 'react-native-pager-view' import Animated, { runOnJS, runOnUI, + Extrapolate, + interpolate, useAnimatedReaction, useAnimatedStyle, useDerivedValue, useSharedValue, withDelay, withTiming, + withDecay, + useAnimatedGestureHandler, } from 'react-native-reanimated' import { Context, TabNameContext } from './Context' @@ -119,6 +128,7 @@ export const Container = React.memo( const oldAccScrollY: ContextType['oldAccScrollY'] = useSharedValue(0) const accDiffClamp: ContextType['accDiffClamp'] = useSharedValue(0) const scrollYCurrent: ContextType['scrollYCurrent'] = useSharedValue(0) + const isTopContainerSynced = useSharedValue(true) const scrollY: ContextType['scrollY'] = useSharedValue( tabNamesArray.map(() => 0) ) @@ -346,6 +356,63 @@ export const Container = React.memo( [onTabPress] ) + const headerYCurrent = useSharedValue(0) + + const gestureHandler = useAnimatedGestureHandler< + PanGestureHandlerGestureEvent, + { start: number } + >({ + onStart: (_, ctx) => { + ctx.start = scrollY.value[index.value] + }, + onActive: (event, ctx) => { + headerYCurrent.value = interpolate( + ctx.start - event.translationY, + [0, headerScrollDistance.value], + [0, headerScrollDistance.value], + Extrapolate.CLAMP + ) + }, + onEnd: (_) => { + headerYCurrent.value = withDecay( + { + velocity: -_.velocityY, + clamp: [0, headerScrollDistance.value], + deceleration: IS_IOS ? 0.998 : 0.99, + }, + (finished) => { + isTopContainerSynced.value = finished || false + } + ) + }, + }) + + /* Syncs the scroll of the active tab once we complete the scroll gesture + on the header and the decay animation completes with success + */ + useAnimatedReaction( + () => { + return isTopContainerSynced.value + }, + (result) => { + if (!result) { + resyncTabScroll() + } + } + ) + + useAnimatedReaction( + () => headerYCurrent.value, + (y) => { + scrollY.value[index.value] = y + scrollYCurrent.value = y + + for (const name of tabNamesArray) { + scrollToImpl(refMap[name], 0, y - contentInset.value, false) + } + } + ) + return ( <Context.Provider value={{ @@ -389,22 +456,29 @@ export const Container = React.memo( !cancelTranslation && stylez, ]} > - <View - style={[styles.container, styles.headerContainer]} - onLayout={getHeaderHeight} - pointerEvents="box-none" - > - {renderHeader && - renderHeader({ - containerRef, - index, - tabNames: tabNamesArray, - focusedTab, - indexDecimal, - onTabPress, - tabProps, - })} - </View> + <GestureHandlerRootView> + <PanGestureHandler + onGestureEvent={gestureHandler} + hitSlop={{ left: -20 }} + > + <Animated.View + style={[styles.container, styles.headerContainer]} + onLayout={getHeaderHeight} + pointerEvents="box-none" + > + {renderHeader && + renderHeader({ + containerRef, + index, + tabNames: tabNamesArray, + focusedTab, + indexDecimal, + onTabPress, + tabProps, + })} + </Animated.View> + </PanGestureHandler> + </GestureHandlerRootView> <View style={[styles.container, styles.tabBarContainer]} onLayout={getTabBarHeight}
Works fine! You should use it with version 6.1.4
in
src/Container.tsx
patchdiff --git a/node_modules/react-native-collapsible-tab-view/src/Container.tsx b/node_modules/react-native-collapsible-tab-view/src/Container.tsx index 1023290..403432e 100644 --- a/node_modules/react-native-collapsible-tab-view/src/Container.tsx +++ b/node_modules/react-native-collapsible-tab-view/src/Container.tsx @@ -5,16 +5,25 @@ import { useWindowDimensions, View, } from 'react-native' +import { + PanGestureHandler, + GestureHandlerRootView, + PanGestureHandlerGestureEvent, +} from 'react-native-gesture-handler' import PagerView from 'react-native-pager-view' import Animated, { runOnJS, runOnUI, + Extrapolate, + interpolate, useAnimatedReaction, useAnimatedStyle, useDerivedValue, useSharedValue, withDelay, withTiming, + withDecay, + useAnimatedGestureHandler, } from 'react-native-reanimated' import { Context, TabNameContext } from './Context' @@ -119,6 +128,7 @@ export const Container = React.memo( const oldAccScrollY: ContextType['oldAccScrollY'] = useSharedValue(0) const accDiffClamp: ContextType['accDiffClamp'] = useSharedValue(0) const scrollYCurrent: ContextType['scrollYCurrent'] = useSharedValue(0) + const isTopContainerSynced = useSharedValue(true) const scrollY: ContextType['scrollY'] = useSharedValue( tabNamesArray.map(() => 0) ) @@ -346,6 +356,63 @@ export const Container = React.memo( [onTabPress] ) + const headerYCurrent = useSharedValue(0) + + const gestureHandler = useAnimatedGestureHandler< + PanGestureHandlerGestureEvent, + { start: number } + >({ + onStart: (_, ctx) => { + ctx.start = scrollY.value[index.value] + }, + onActive: (event, ctx) => { + headerYCurrent.value = interpolate( + ctx.start - event.translationY, + [0, headerScrollDistance.value], + [0, headerScrollDistance.value], + Extrapolate.CLAMP + ) + }, + onEnd: (_) => { + headerYCurrent.value = withDecay( + { + velocity: -_.velocityY, + clamp: [0, headerScrollDistance.value], + deceleration: IS_IOS ? 0.998 : 0.99, + }, + (finished) => { + isTopContainerSynced.value = finished || false + } + ) + }, + }) + + /* Syncs the scroll of the active tab once we complete the scroll gesture + on the header and the decay animation completes with success + */ + useAnimatedReaction( + () => { + return isTopContainerSynced.value + }, + (result) => { + if (!result) { + resyncTabScroll() + } + } + ) + + useAnimatedReaction( + () => headerYCurrent.value, + (y) => { + scrollY.value[index.value] = y + scrollYCurrent.value = y + + for (const name of tabNamesArray) { + scrollToImpl(refMap[name], 0, y - contentInset.value, false) + } + } + ) + return ( <Context.Provider value={{ @@ -389,22 +456,29 @@ export const Container = React.memo( !cancelTranslation && stylez, ]} > - <View - style={[styles.container, styles.headerContainer]} - onLayout={getHeaderHeight} - pointerEvents="box-none" - > - {renderHeader && - renderHeader({ - containerRef, - index, - tabNames: tabNamesArray, - focusedTab, - indexDecimal, - onTabPress, - tabProps, - })} - </View> + <GestureHandlerRootView> + <PanGestureHandler + onGestureEvent={gestureHandler} + hitSlop={{ left: -20 }} + > + <Animated.View + style={[styles.container, styles.headerContainer]} + onLayout={getHeaderHeight} + pointerEvents="box-none" + > + {renderHeader && + renderHeader({ + containerRef, + index, + tabNames: tabNamesArray, + focusedTab, + indexDecimal, + onTabPress, + tabProps, + })} + </Animated.View> + </PanGestureHandler> + </GestureHandlerRootView> <View style={[styles.container, styles.tabBarContainer]} onLayout={getTabBarHeight}
Hey thanks for the patch. It is not that smooth. Any of you have any working code example in which it work good ? @nihilenz @VovaParamonov
Hi is it possible to set (considering the pointerEvents we need) a touchable opacity that could exist in the header as both a scrollable area (hitting it when attempting to scroll) as well as being an active button?
I can't seem to get buttons to work in my header. They exist in either active button states or scrollable, but not bother at the same time.