Код для отображения 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;
Стили для элементов компонента:
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),
},
}));
Я использую react-native-reanimated для анимации индикатора вкладки. Что я заметил, так это то, что при перезагрузке приложения начальная позиция индикатора вкладки продолжает меняться, как видно из прикрепленного мной GIF-файла. Иногда он расположен там, где должен быть, а иногда половина окна скрыта за контейнером прокрутки. Когда я удаляю alignItems: center
из Animated.View
, все работает, как и ожидалось.
Я недоумеваю, почему позиция постоянно меняется из-за align-items
?
Редактировать: не обращайте внимания на приведенное ниже, поведение не связано с горячей перезагрузкой! Я оставлю это, если у кого-то еще есть такое же заблуждение
Горячая перезагрузка ненадежна с Reanimated — в собственных потоках есть значения, которые не будут обновляться. Это не влияет на окончательное приложение.
Чтобы проверить, действительно ли он работает, просто встряхните устройство/сим-карту и нажмите «Перезагрузить» после внесения изменений. Этого достаточно, чтобы очистить все залипшие значения. Если ваш компонент по-прежнему не делает то, что вы хотите, вы можете с уверенностью отредактировать его и убедиться, что он выглядит правильно.
Это быстрая перезагрузка! Виноват
Проблема заключалась в том, что дочерний компонент индикатора не был заключен в границы контейнера индикатора. Я решил эту проблему, добавив flexWrap: 'wrap'
в контейнер родительского индикатора.
Итак, новый стиль выглядит так:
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),
},
}));
Может быть, GIF создает впечатление, что приложение перезагружается в горячем режиме. Но я нажимаю «R», чтобы полностью перезагрузить.