Компоненты/Гибкая кнопка

Гибкая кнопка

1 февраля 2025

Компактная кнопка, которая разворачивается из иконки в текст

Установка

npx astra-rn add flexi-button
pnpm dlx astra-rn add flexi-button
yarn dlx astra-rn add flexi-button
bunx --bun astra-rn add flexi-button

Файлы

index.tsx
НазадРаскрывающиеся вкладкиДалееЛипкие капли

Вручную

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

npm install @expo/vector-icons expo-blur react-native-reanimated
pnpm add @expo/vector-icons expo-blur react-native-reanimated
yarn add @expo/vector-icons expo-blur react-native-reanimated
bun add @expo/vector-icons expo-blur react-native-reanimated

Скопируйте эти файлы в src/shared/components/micro-interactions/flexi-button/:

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

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

import { useRef, useState } from "react";
import { View, Text, StyleSheet } from "react-native";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { StatusBar } from "expo-status-bar";
import { useFonts } from "expo-font";
import { SymbolView } from "expo-symbols";
import { Ionicons } from "@expo/vector-icons";
import { FlexiButton } from "@/shared/components/micro-interactions/flexi-button";
import { Showcase, useAutoplay, useCustomize, hexToRgb } from "~/showcase";

const CUSTOMIZE = [
  { key: "text", label: "Текст", type: "text", default: "Clear All", maxLength: 20 },
  { key: "icon", label: "Иконка", type: "select", options: [{ value: "notifications", label: "Колокол" }, { value: "trash", label: "Корзина" }, { value: "mail", label: "Почта" }, { value: "heart", label: "Сердце" }, { value: "share-social", label: "Поделиться" }], default: "notifications" },
  { key: "collapsedWidth", label: "Ширина свёрнутой", type: "slider", min: 32, max: 80, step: 1, unit: "px", default: 40 },
  { key: "expandedWidth", label: "Ширина раскрытой", type: "slider", min: 80, max: 240, step: 2, unit: "px", default: 120 },
  { key: "tint", label: "Цвет фона", type: "color", default: "#ffffff" },
  { key: "tintOpacity", label: "Прозрачность фона", type: "slider", min: 0, max: 1, step: 0.05, default: 0.1 },
  { key: "holdMs", label: "Показ", type: "slider", min: 300, max: 4000, step: 100, unit: "ms", default: 900 },
] as const;

function tintOf(hex: string, alpha: number) {
  if (hex.toLowerCase() === "#ffffff" && alpha === 0.1) return "rgba(255, 255, 255, 0.1)";
  const { r, g, b } = hexToRgb(hex);
  return `rgba(${Math.round(r * 255)}, ${Math.round(g * 255)}, ${Math.round(b * 255)}, ${alpha})`;
}

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

  const launchDate = new Date("2026-07-20T14:30:00");
  const [expanded, setExpanded] = useState<boolean>(false);
  const custom = useCustomize(CUSTOMIZE);
  const holdRef = useRef(custom.holdMs);
  holdRef.current = custom.holdMs;

  useAutoplay(
    async ({ wait }) => {
      await wait(300);
      setExpanded(true);
      await wait(holdRef.current);
      setExpanded(false);
      await wait(500);
    },
  );

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

        <View style={styles.content}>
          <FlexiButton
            expanded={expanded}
            onExpandedChange={setExpanded}
            text={custom.text}
            icon={custom.icon as "notifications"}
            collapsedWidth={custom.collapsedWidth}
            expandedWidth={custom.expandedWidth}
            backgroundColor={tintOf(custom.tint, custom.tintOpacity)}
          />
        </View>
      </View>
    </Showcase>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#0a0a0a",
  },
  content: {
    flex: 1,
    justifyContent: "center",
    alignItems: "center",
  },
  iconBox: {
    width: 64,
    height: 64,
    borderRadius: 20,
    backgroundColor: "#1a1a1a",
    justifyContent: "center",
    alignItems: "center",
    marginBottom: 8,
  },
  label: {
    fontSize: 14,
    color: "#555",
    textTransform: "uppercase",
    letterSpacing: 2,
  },
  date: {
    fontSize: 15,
    color: "#333",
    marginTop: 8,
  },
});

Пропсы

Пропс
Описание
onPress?

Тип () => void

collapsedWidth?

Тип number

expandedWidth?

Тип number

text?

Тип string

icon?

Тип IconName | IconRenderFn

onDimensionsChange?

Тип (dimensions: Dimensions) => void

backgroundColor?

Тип string

expanded?

Тип boolean

onExpandedChange?

Тип (expanded: boolean) => void

Необязательные expanded и onExpandedChange делают кнопку управляемой: раскрытием можно управлять из кода, анимация та же пружина. Без expanded кнопка сама хранит состояние, как раньше.