Компоненты/Сегменты на жестах

Сегменты на жестах

31 января 2025

Сегментированный переключатель на жестах со скользящим индикатором

Установка

npx astra-rn add segmented-control
pnpm dlx astra-rn add segmented-control
yarn dlx astra-rn add segmented-control
bunx --bun astra-rn add segmented-control

Файлы

index.tsx
НазадПеремоткаДалееШейдерный индекс

Вручную

Поставьте зависимости:

npm install expo-blur expo-haptics react-native-gesture-handler react-native-reanimated react-native-worklets
pnpm add expo-blur expo-haptics react-native-gesture-handler react-native-reanimated react-native-worklets
yarn add expo-blur expo-haptics react-native-gesture-handler react-native-reanimated react-native-worklets
bun add expo-blur expo-haptics react-native-gesture-handler react-native-reanimated react-native-worklets

Скопируйте эти файлы в src/shared/components/organisms/segmented-control/:

Проверьте алиас путей. Импорты в коде выше записаны так, как их пишет astra-rn add: @/shared/components, @/shared/utils и @/shared/hooks. Нужен алиас "@/*": ["./src/*"] в tsconfig.json — или поправьте импорты под свои пути.

Использование

import { Dimensions, StyleSheet, Text, View } from "react-native";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { StatusBar } from "expo-status-bar";
import { useState } from "react";
import SegmentedControl from "@/shared/components/organisms/segmented-control";
import { useFonts } from "expo-font";

import { Showcase, useAutoplay, useCustomize } from "~/showcase";

const CUSTOMIZE = [
  { key: "label1", label: "Вкладка 1", type: "text", default: "Home", maxLength: 16 },
  { key: "label2", label: "Вкладка 2", type: "text", default: "Favorites", maxLength: 16 },
  { key: "label3", label: "Вкладка 3", type: "text", default: "Profile", maxLength: 16 },
  {
    key: "preset",
    label: "Тема",
    type: "select",
    options: [
      { value: "ios", label: "iOS" },
      { value: "light", label: "Светлая" },
      { value: "dark", label: "Тёмная" },
      { value: "ocean", label: "Океан" },
      { value: "sunset", label: "Закат" },
      { value: "forest", label: "Лес" },
      { value: "purple", label: "Фиолетовая" },
      { value: "rose", label: "Роза" },
    ],
    default: "ios",
  },
  { key: "textColor", label: "Цвет текста", type: "color", default: "#000000" },
  { key: "fontSize", label: "Размер текста", type: "slider", min: 10, max: 20, step: 1, unit: "px", default: 14 },
  { key: "paddingVertical", label: "Отступ", type: "slider", min: 2, max: 20, step: 0.5, unit: "px", default: 8.5 },
  { key: "borderRadius", label: "Скругление", type: "slider", min: 0, max: 200, step: 1, unit: "px", default: 200 },
  { key: "scaleEffect", label: "Эффект нажатия", type: "toggle", default: true },
] as const;

type TPreset = "ios" | "light" | "dark" | "ocean" | "sunset" | "forest" | "purple" | "rose";
const { width: _width } = Dimensions.get("window");

const TABS = [
  { icon: "home", label: "Home", color: "#000000" },
  { icon: "heart", label: "Favorites", color: "#000" },
  { icon: "user", label: "Profile", color: "#000000" },
] as const;

export default function App() {
  const [fontLoaded] = useFonts({
    SfProRounded: require("@/assets/fonts/sf-pro-rounded.ttf"),
    HelveticaNowDisplay: require("@/assets/fonts/HelveticaNowDisplayMedium.ttf"),
  });

  const custom = useCustomize(CUSTOMIZE);
  const labels = [custom.label1, custom.label2, custom.label3];
  const [index, setIndex] = useState(0);

  useAutoplay(async ({ wait }) => {
    const steps: [number, number][] = [
      [0, 700],
      [1, 620],
      [2, 620],
      [1, 580],
      [0, 580],
      [1, 620],
      [2, 1000],
      [1, 900],
      [0, 1100],
    ];
    for (const [next, hold] of steps) {
      setIndex(next);
      await wait(hold);
    }
  });

  return (
    <Showcase>
      <GestureHandlerRootView style={styles.container}>
        <StatusBar style="light" />

        <View style={styles.card}>
          <SegmentedControl
            currentIndex={index}
            onChange={setIndex}
            preset={custom.preset as TPreset}
            paddingVertical={custom.paddingVertical}
            borderRadius={custom.borderRadius}
            width={_width - 120}
            disableScaleEffect={!custom.scaleEffect}
          >
            {TABS.map((tab, tabIndex) => (
              <View key={tab.label} style={styles.tab}>
                <Text
                  style={[
                    styles.tabText,
                    {
                      color: custom.textColor,
                      fontSize: custom.fontSize,
                      fontFamily: fontLoaded ? "SfProRounded" : undefined,
                    },
                  ]}
                >
                  {labels[tabIndex]}
                </Text>
              </View>
            ))}
          </SegmentedControl>
        </View>
      </GestureHandlerRootView>
    </Showcase>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#0a0a0a",
    alignItems: "center",
    justifyContent: "center",
  },

  card: {},

  tab: {
    flexDirection: "row",
    alignItems: "center",
    justifyContent: "center",
    gap: 6,
  },

  tabText: {
    textAlign: "center",
    fontSize: 14,
    fontWeight: "500",
    color: "#000000",
  },

  content: {
    marginTop: 24,
    alignItems: "center",
  },

  title: {
    fontSize: 20,
    fontWeight: "600",
    color: "#fff",
    marginBottom: 6,
  },

  subtitle: {
    fontSize: 13,
    color: "rgba(255,255,255,0.6)",
    textAlign: "center",
    maxWidth: 260,
  },
});

Пропсы

Пропс
Описание
children

Тип React.ReactNode

onChange

Тип (index: number) => void

currentIndex

Тип number

preset?

Тип SegmentedControlPreset

segmentedControlBackgroundColor?

Тип string

activeSegmentBackgroundColor?

Тип string

paddingVertical?

Тип number

dividerColor?

Тип string

borderRadius?

Тип number

disableScaleEffect?

Тип boolean

width?

Total width of the control. Defaults to the screen width minus 32.

Тип number