react native顶部标签栏导航器:与文本匹配的指示器宽度

inkz8wg9  于 2023-04-12  发布在  React
关注(0)|答案(7)|浏览(226)

我有三个标签在顶部标签栏导航不同宽度的文本.是否有可能使指标宽度匹配的文本?在一个类似的注意,我如何才能使标签匹配的文本的宽度太,而不会使它显示奇怪.我已经尝试了宽度自动,但它不停留在中心.
这是它看起来与自动宽度:

<Tab.Navigator 
                initialRouteName="Open"
                tabBarOptions={{
                    style: { 
                      backgroundColor: "white", 
                      paddingTop: 20, 
                      paddingHorizontal: 25
                      },
                    indicatorStyle: {
                      borderBottomColor: colorScheme.teal,
                      borderBottomWidth: 2,
                      width: '30%',
                      left:"9%"
                    },
                    tabStyle : {
                      justifyContent: "center",
                      width: tabBarWidth/3,
                    }
                  }}
            >
                <Tab.Screen 
                    name="Open" 
                    component={WriterRequestScreen} 
                    initialParams={{ screen: 'Open' }} 
                    options={{ 
                      tabBarLabel: ({focused}) => <Text style = {{fontSize: 18, fontWeight: 'bold', color: focused? colorScheme.teal : colorScheme.grey}}> Open </Text>, 
                   }}
                    />
               <Tab.Screen 
                    name="In Progress" 
                    component={WriterRequestScreen} 
                    initialParams={{ screen: 'InProgress' }}
                    options={{ 
                      tabBarLabel: ({focused}) => <Text style = {{fontSize: 18, fontWeight: 'bold', color: focused? colorScheme.teal : colorScheme.grey}}> In Progress </Text>}}
                    />
               <Tab.Screen 
                name="Completed" 
                component={WriterRequestScreen} 
                initialParams={{ screen: 'Completed' }}
                options={{ tabBarLabel: ({focused}) => <Text style = {{fontSize: 18, fontWeight: 'bold', color: focused? colorScheme.teal : colorScheme.grey}}> Completed </Text>}}
                />
            </Tab.Navigator>
hrirmatl

hrirmatl1#

我还需要使指示器适合文本大小,标签的动态宽度,以及由于长标签而可滚动的顶部栏。结果如下所示:
tab bar with dynamic indicator width
如果您不关心适合标签的指示器宽度,您可以简单地将screenOptions.tabBarScrollEnabled: truescreenOptions.tabBarIndicatorStyle中的width: "auto"结合使用。
否则,您需要创建自己的标签栏组件,并将其传递给<Tab.Navigator>tabBar属性。我使用了ScrollView,但如果您只有几个带有短标签的标签,View会更简单。以下是此自定义TabBar组件的Typescript代码:

import { MaterialTopTabBarProps } from "@react-navigation/material-top-tabs";
import { useEffect, useRef, useState } from "react";
import {
  Animated,
  Dimensions,
  View,
  TouchableOpacity,
  StyleSheet,
  ScrollView,
  I18nManager,
  LayoutChangeEvent,
} from "react-native";

const screenWidth = Dimensions.get("window").width;

const DISTANCE_BETWEEN_TABS = 20;

const TabBar = ({
  state,
  descriptors,
  navigation,
  position,
}: MaterialTopTabBarProps): JSX.Element => {
  const [widths, setWidths] = useState<(number | undefined)[]>([]);
  const scrollViewRef = useRef<ScrollView>(null);
  const transform = [];
  const inputRange = state.routes.map((_, i) => i);

  // keep a ref to easily scroll the tab bar to the focused label
  const outputRangeRef = useRef<number[]>([]);

  const getTranslateX = (
    position: Animated.AnimatedInterpolation,
    routes: never[],
    widths: number[]
  ) => {
    const outputRange = routes.reduce((acc, _, i: number) => {
      if (i === 0) return [DISTANCE_BETWEEN_TABS / 2 + widths[0] / 2];
      return [
        ...acc,
        acc[i - 1] + widths[i - 1] / 2 + widths[i] / 2 + DISTANCE_BETWEEN_TABS,
      ];
    }, [] as number[]);
    outputRangeRef.current = outputRange;
    const translateX = position.interpolate({
      inputRange,
      outputRange,
      extrapolate: "clamp",
    });
    return Animated.multiply(translateX, I18nManager.isRTL ? -1 : 1);
  };

  // compute translateX and scaleX because we cannot animate width directly
  if (
    state.routes.length > 1 &&
    widths.length === state.routes.length &&
    !widths.includes(undefined)
  ) {
    const translateX = getTranslateX(
      position,
      state.routes as never[],
      widths as number[]
    );
    transform.push({
      translateX,
    });
    const outputRange = inputRange.map((_, i) => widths[i]) as number[];
    transform.push({
      scaleX:
        state.routes.length > 1
          ? position.interpolate({
              inputRange,
              outputRange,
              extrapolate: "clamp",
            })
          : outputRange[0],
    });
  }

  // scrolls to the active tab label when a new tab is focused
  useEffect(() => {
    if (
      state.routes.length > 1 &&
      widths.length === state.routes.length &&
      !widths.includes(undefined)
    ) {
      if (state.index === 0) {
        scrollViewRef.current?.scrollTo({
          x: 0,
        });
      } else {
        // keep the focused label at the center of the screen
        scrollViewRef.current?.scrollTo({
          x: (outputRangeRef.current[state.index] as number) - screenWidth / 2,
        });
      }
    }
  }, [state.index, state.routes.length, widths]);

  // get the label widths on mount
  const onLayout = (event: LayoutChangeEvent, index: number) => {
    const { width } = event.nativeEvent.layout;
    const newWidths = [...widths];
    newWidths[index] = width - DISTANCE_BETWEEN_TABS;
    setWidths(newWidths);
  };

  // basic labels as suggested by react navigation
  const labels = state.routes.map((route, index) => {
    const { options } = descriptors[route.key];
    const label = route.name;
    const isFocused = state.index === index;

    const onPress = () => {
      const event = navigation.emit({
        type: "tabPress",
        target: route.key,
        canPreventDefault: true,
      });

      if (!isFocused && !event.defaultPrevented) {
        // The `merge: true` option makes sure that the params inside the tab screen are preserved
        // eslint-disable-next-line
        // @ts-ignore
        navigation.navigate({ name: route.name, merge: true });
      }
    };
    const inputRange = state.routes.map((_, i) => i);
    const opacity = position.interpolate({
      inputRange,
      outputRange: inputRange.map((i) => (i === index ? 1 : 0.5)),
    });

    return (
      <TouchableOpacity
        key={route.key}
        accessibilityRole="button"
        accessibilityState={isFocused ? { selected: true } : {}}
        accessibilityLabel={options.tabBarAccessibilityLabel}
        onPress={onPress}
        style={styles.button}
      >
        <View
          onLayout={(event) => onLayout(event, index)}
          style={styles.buttonContainer}
        >
          <Animated.Text style={[styles.text, { opacity }]}>
            {label}
          </Animated.Text>
        </View>
      </TouchableOpacity>
    );
  });

  return (
    <View style={styles.contentContainer}>
      <Animated.ScrollView
        horizontal
        ref={scrollViewRef}
        showsHorizontalScrollIndicator={false}
        style={styles.container}
      >
        {labels}
        <Animated.View style={[styles.indicator, { transform }]} />
      </Animated.ScrollView>
    </View>
  );
};

const styles = StyleSheet.create({
  button: {
    alignItems: "center",
    justifyContent: "center",
  },
  buttonContainer: {
    paddingHorizontal: DISTANCE_BETWEEN_TABS / 2,
  },
  container: {
    backgroundColor: "black",
    flexDirection: "row",
    height: 34,
  },
  contentContainer: {
    height: 34,
    marginTop: 30,
  },
  indicator: {
    backgroundColor: "white",
    bottom: 0,
    height: 3,
    left: 0,
    position: "absolute",
    right: 0,
    // this must be 1 for the scaleX animation to work properly
    width: 1,
  },
  text: {
    color: "white",
    fontSize: 14,
    textAlign: "center",
  },
});

export default TabBar;

我设法使它的工作与混合:

请让我知道如果你找到一个更方便的解决方案。

iaqfqrcu

iaqfqrcu2#

screenOptions中,为
tabBarScrollEnabled: truetabBarItemStyle: {{width: "auto", minWidht: "100"}}
minWidth只是为了保持设计一致。
请注意,我使用的是react-navigation 6.x和Camille Hg,答案非常有用。

qv7cva1a

qv7cva1a3#

您必须将width:auto添加到tabStyle以使选项卡宽度灵活。
然后在每个tabBarLabel <Text>组件中添加样式textAlign: "center"width: YOUR_WIDTH
YOUR_WIDTH可以是不同的每个标签,可以是你的文本。长度 * 10(如果你想让它取决于你的文本长度)或从尺寸屏幕宽度,并将其除以任何其他数字,使其在屏幕上的宽度相等。示例:

const win = Dimensions.get('window');

...

bigTab: {
    fontFamily: "Mulish-Bold",
    fontSize: 11,
    color: "#fff",
    textAlign: "center",
    width: win.width/2 - 40
},
smallTab: {
    fontFamily: "Mulish-Bold",
    fontSize: 11,
    color: "#fff",
    textAlign: "center",
    width: win.width / 5 + 10
}
vcudknz3

vcudknz34#

从indicatorStyle中删除宽度并使用flex:1

indicatorStyle: {    borderBottomColor: colorScheme.teal,
                      borderBottomWidth: 2,
                      flex:1,
                      left:"9%"
                    },
t30tvxxf

t30tvxxf5#

我已经使用了一些关于onLayout的技巧来实现这一点,请注意,我是在假设两个标签的情况下实现的,并且第二个标签的宽度大于第一个标签的宽度。它可能需要调整其他用例。

import React, { useEffect, useState } from 'react'
import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs'
import { Animated, Text, TouchableOpacity, View } from 'react-native'

const Stack = createMaterialTopTabNavigator()

const DISTANCE_BETWEEN_TABS = 25

function MyTabBar ({ state, descriptors, navigation, position }) {
  const [widths, setWidths] = useState([])
  const [transform, setTransform] = useState([])

  const inputRange = state.routes.map((_, i) => i)

  useEffect(() => {
    if (widths.length === 2) {
      const [startingWidth, transitionWidth] = widths
      const translateX = position.interpolate({
        inputRange,
        outputRange: [0, startingWidth + DISTANCE_BETWEEN_TABS + (transitionWidth - startingWidth) / 2]
      })
      const scaleX = position.interpolate({
        inputRange,
        outputRange: [1, transitionWidth / startingWidth]
      })
      setTransform([{ translateX }, { scaleX }])
    }
  }, [widths])

  return (
    <View style={{ flexDirection: 'row' }}>
      {state.routes.map((route, index) => {
        const { options } = descriptors[route.key]
        const label =
          options.tabBarLabel !== undefined
            ? options.tabBarLabel
            : options.title !== undefined
              ? options.title
              : route.name

        const isFocused = state.index === index

        const onPress = () => {
          const event = navigation.emit({
            type: 'tabPress',
            target: route.key,
            canPreventDefault: true
          })

          if (!isFocused && !event.defaultPrevented) {
            // The `merge: true` option makes sure that the params inside the tab screen are preserved
            navigation.navigate({ name: route.name, merge: true })
          }
        }

        const onLayout = event => {
          const { width } = event.nativeEvent.layout
          setWidths([...widths, width])
        }

        const opacity = position.interpolate({
          inputRange,
          outputRange: inputRange.map(i => (i === index ? 0.87 : 0.53))
        })

        return (
          <TouchableOpacity
            key={index}
            accessibilityRole='button'
            accessibilityState={isFocused ? { selected: true } : {}}
            accessibilityLabel={options.tabBarAccessibilityLabel}
            testID={options.tabBarTestID}
            onPress={onPress}
            style={{ marginRight: DISTANCE_BETWEEN_TABS }}
          >
            <Animated.Text
              onLayout={onLayout}
              style={{
                opacity,
                color: '#000',
                fontSize: 18,
                fontFamily: 'OpenSans-Bold',
                marginBottom: 15
              }}
            >
              {label}
            </Animated.Text>
          </TouchableOpacity>
        )
      })}
      <View style={{ backgroundColor: '#DDD', height: 2, position: 'absolute', bottom: 0, left: 0, right: 0 }} />
      <Animated.View style={{ position: 'absolute', bottom: 0, left: 0, width: widths.length ? widths[0] : 0, backgroundColor: '#222', height: 2, transform }} />
    </View>
  )
}

export default () => {
  return (
    <>
      <Stack.Navigator tabBar={props => <MyTabBar {...props} />} style={{ paddingHorizontal: 25 }}>
        <Stack.Screen name='Orders' component={() => <Text>A</Text>} />
        <Stack.Screen name='Reviews' component={() => <Text>B</Text>} />
      </Stack.Navigator>
    </>
  )
}

更新:

如果菜单名是静态的,那么在widths内部硬编码宽度可能是一个更健壮的解决方案,尽管这样做的维护成本稍高一些。
资源:

0vvn1miw

0vvn1miw6#

我有同样的问题,我终于能够使指标采取正是文本大小。
我不知道在哪个版本中这是可能的..但显然你可以添加一个自定义指示器组件(旁边的能力,添加一个自定义tabBar组件)

在创建TopTabNavigator时,重要的是要按照下的代码中所述添加属性。

// assuming that you want to add paddingHorizontal: 10 for each item!
const TAB_BAR_ITEM_PADDING = 10;

const Tab = createMaterialTopTabNavigator();

function TopTabNavigator() {
    return (
        <Tab.Navigator
            ..... 
             ... .
            screenOptions={{ 
              .... 
              ... 
               tabBarItemStyle: {
                  // these properties are important for this method to work !!
                  width: "auto",
                  marginHorizontal: 0, // this is to make sure that the spacing of the item comes only from the paddingHorizontal!. 
                  paddingHorizontal: TAB_BAR_ITEM_PADDING, // the desired padding for the item .. stored in a constant to be passed in the custom Indicator 
                 
                },

                tabBarIndicator: props => {
                  return (
                    <CustomTabBarIndicator
                       // the default props 
                       getTabWidth={props.getTabWidth}
                       jumpTo={props.jumpTo}
                       layout={props.layout}
                       navigationState={props.state}
                       position={props.position}
                       width={props.width}
                       style={{
                         left: TAB_BAR_ITEM_PADDING,
                         backgroundColor: Colors.primary,
                       }}

                       // this is an additional property we will need to make the indicator exactly 
                       tabBarItemPadding={TAB_BAR_ITEM_PADDING}
                     />
                    );
                   },
                 }}
                >
            
            <Tab.Screen ....  />
            <Tab.Screen ..... />
            <Tab.Screen .... />
        </Tab.Navigator>
    );
}

现在对于CustomTabBarIndIndIndIndicator组件,我们只需转到react-native-tab-view的官方github存储库,然后转到TabBarIndicator.tsx并将组件复制到名为CustomTabBarIndicator的文件中“只是为了与示例保持一致,但您可以称之为任何您想要的”,并且不要忘记将附加属性添加到tabBarItemPadding的Props类型“如果您使用的是typescript”
现在对图像中高亮显示的线做一个小的修改

变更:

const outputRange = inputRange.map(getTabWidth);

将成为:

const outputRange = inputRange.map(x => {
  // this part is customized to get the indicator to be the same width like the label
  // subtract the tabBarItemPadding from the tabWidth
  // so that you indicator will be exactly the same size like the label text
  return getTabWidth(x) - this.props.tabBarItemPadding * 2;
});

就是这样:)
附注:我添加了图片,因为我不知道如何准确地描述在哪里进行更改
如果你不想使用typescript .. jsut,就从代码中删除所有类型,这样就可以了:)

ubbxdtey

ubbxdtey7#

现在可以使用以下选项实现OP的预期结果:

screenOptions={{
    // Distance between Items
    tabBarGap: 16,
    tabBarStyle: {
        // Offset Tabs to match your layouts margin
        paddingLeft: 16
    },
    tabBarIndicatorContainerStyle: {
        // Offset the View containing the Indicator
        // to match your layouts margin
        marginLeft: 16,
    },
    tabBarItemStyle: {
        // Make Item width adjust to Label width
        width: 'auto',
        paddingHorizontal: 0
    },
    tabBarLabelStyle: {
        marginHorizontal: 0,
    }
}}

相关问题