The code to render a TabList:
import React, { Children, useEffect } from 'react';
import { LayoutChangeEvent, View } from 'react-native';
import {
ScrollView,
TouchableWithoutFeedback,
} from 'react-native-gesture-handler';
import Animated, {
Easing,
useAnimatedStyle,
useSharedValue,
withTiming,
} from 'react-native-reanimated';
import { isValidChild } from '@utils';
import { useTabIndex } from '../tab-context';
import { useStyle } from './tab-list.styles';
import { TabListProps } from './tab-list.type';
const animConfig = {
duration: 200,
easing: Easing.bezier(0.25, 0.1, 0.25, 1),
};
const TabList: React.FC<TabListProps> = props => {
const styles = useStyle();
const { children, onChange } = props;
const selectedTabIndex = useTabIndex();
const animatedTabIndicatorPosition = useSharedValue(0);
// Save layout of the container
const [containerLayout, setContainerLayout] = React.useState({
x: 0,
y: 0,
width: 0,
height: 0,
});
const onContainerLayout = (event: LayoutChangeEvent) => {
const { x, y, width, height } = event.nativeEvent.layout;
setContainerLayout({ x, y, width, height });
};
// get children length
const childrenLength = Children.count(children);
const tabWidth =
childrenLength > 3
? containerLayout.width / 3
: containerLayout.width / childrenLength;
const renderChildren = () => {
// Render only children of component type TabList
return Children.map(children, child => {
// Check if child is a valid React element and has type TabList
if (isValidChild(child, 'Tab')) {
return (
<TouchableWithoutFeedback
containerStyle={{ width: tabWidth }}
onPress={() => onChange((child as JSX.Element)?.props.tabIndex)}
>
{child}
</TouchableWithoutFeedback>
);
}
// Throw error if child is not a TabList
throw new Error('TabList component can only have children of type Tab');
});
};
useEffect(() => {
animatedTabIndicatorPosition.value = selectedTabIndex * tabWidth;
}, [selectedTabIndex]);
const indicatorAnimatedStyle = useAnimatedStyle(() => {
return {
width: tabWidth,
transform: [
{
translateX: withTiming(
animatedTabIndicatorPosition.value,
animConfig,
),
},
],
};
}, []);
return (
<View onLayout={onContainerLayout} style={styles.container}>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
testID="TestID__component-TabList"
>
<Animated.View
style={[styles.indicatorContainer, indicatorAnimatedStyle]}
>
<View
style={[
styles.indicator,
{
width: tabWidth - 4,
},
]}
/>
</Animated.View>
{renderChildren()}
</ScrollView>
</View>
);
};
export default TabList;
The styles for the component elements:
import { createUseStyle } from '@theme';
// createUseStyle basically returns (fn) => useStyle(fn)
export const useStyle = createUseStyle(theme => ({
container: {
position: 'relative',
flexGrow: 1,
backgroundColor: theme.palette.accents.color8,
height: 32,
borderRadius: theme.shape.borderRadius(4.5),
},
indicatorContainer: {
position: 'absolute',
height: 32,
justifyContent: 'center',
alignItems: 'center',
},
indicator: {
height: 28,
backgroundColor: theme.palette.background.main,
borderRadius: theme.shape.borderRadius(4),
},
}));
I am using react-native-reanimated to animate the tab indicator. What I noticed is, on app reload, the initial tab indicator position keeps on changing as seen in the GIF I have attached. At times, it is positioned where it should be and at times, half the box is hidden behind the scrollview container. When I remove the alignItems: center
from the Animated.View
, things work as expected.
I am perplexed as to why the position keeps changing because of align-items
?
CodePudding user response:
Edit: disregard the below, the behavior is not due to hot reloading! I'll leave this up in case anyone else has the same misconception
Hot reloading is not reliable with Reanimated - there are values on native threads that won't get refreshed. This has no impact on the final app.
To test whether it's really working, simply shake the device/sim and hit Reload after you make changes. This is enough to clear any sticky values. If your component still isn't doing what you want, you can then have the confidence to edit it and be sure it looks right.
CodePudding user response:
The issue was that the child indicator component wasn't wrapped within the boundary of the indicator container. I resolved this by adding flexWrap: 'wrap'
to the parent indicator container.
So, the new style looks like this:
import { createUseStyle } from '@theme';
// createUseStyle basically returns (fn) => useStyle(fn)
export const useStyle = createUseStyle(theme => ({
container: {
position: 'relative',
flexGrow: 1,
backgroundColor: theme.palette.accents.color8,
height: 32,
borderRadius: theme.shape.borderRadius(4.5),
},
indicatorContainer: {
position: 'absolute',
height: 32,
justifyContent: 'center',
alignItems: 'center',
flexWrap: 'wrap'
},
indicator: {
height: 28,
backgroundColor: theme.palette.background.main,
borderRadius: theme.shape.borderRadius(4),
},
}));