Компоненты/Живая кнопка

Живая кнопка

31 января 2025

Настраиваемая кнопка с анимацией нажатия

Установка

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

Файлы

index.tsx
НазадПрелоадер из ячеекДалееВидеоплеер с субтитрами

Вручную

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

npm install expo-linear-gradient react-native-reanimated
pnpm add expo-linear-gradient react-native-reanimated
yarn add expo-linear-gradient react-native-reanimated
bun add expo-linear-gradient react-native-reanimated

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

Он импортирует ещё один модуль — astra-rn add кладёт их рядом сам, вручную скопируйте и их:

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

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

import { Platform, View, StyleSheet, Text } from "react-native";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { StatusBar } from "expo-status-bar";
import { useFonts } from "expo-font";
import { Ionicons } from "@expo/vector-icons";
import { useCallback, useMemo, useState } from "react";
import { Button } from "@/shared/components/base/button";
import { CircularLoader } from "@/shared/components/molecules/circular-loader";
import { Showcase, useAutoplay, useCustomize } from "~/showcase";

const CUSTOMIZE = [
  { key: "label", label: "Надпись", type: "text", default: "Click Me!", maxLength: 30 },
  { key: "loadingLabel", label: "Надпись загрузки", type: "text", default: "Fetching…", maxLength: 30 },
  { key: "fill", label: "Цвет кнопки", type: "color", default: "#ffffff" },
  { key: "ink", label: "Цвет текста", type: "color", default: "#000000" },
  { key: "loadingFill", label: "Цвет загрузки", type: "color", default: "#cacaca" },
  { key: "width", label: "Ширина", type: "slider", min: 120, max: 320, step: 2, unit: "px", default: 200 },
  { key: "height", label: "Высота", type: "slider", min: 32, max: 80, step: 1, unit: "px", default: 48 },
  { key: "radius", label: "Скругление", type: "slider", min: 0, max: 40, step: 1, unit: "px", default: 16 },
  { key: "fontSize", label: "Размер шрифта", type: "slider", min: 12, max: 26, step: 1, unit: "px", default: 17 },
  { key: "loadingMs", label: "Загрузка", type: "slider", min: 500, max: 8000, step: 100, unit: "ms", default: 3000 },
  { key: "showIcon", label: "Иконка", type: "toggle", default: true },
  { key: "background", label: "Фон", type: "color", default: "#0a0a0a" },
] as const;

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

  const custom = useCustomize(CUSTOMIZE);
  const [loading, setLoading] = useState<boolean>(false);

  const onPress = useCallback(() => {
    setLoading(true);
    setTimeout(() => {
      setLoading(false);
    }, custom.loadingMs);
  }, [custom.loadingMs]);

  const [pressed, setPressed] = useState<boolean>(false);

  useAutoplay(async ({ wait }) => {
    await wait(400);
    try {
      setPressed(true);
      await wait(120);
      setPressed(false);
      setLoading(true);
      await wait(Math.max(0, custom.loadingMs - 100));
    } finally {
      setPressed(false);
      setLoading(false);
    }
    await wait(300);
  });

  const fontReady = fontLoaded || Platform.OS !== "web";

  const layers = useMemo(
    () => (
      <>
        <Button.Content>
          <View
            style={[
              styles.btn,
              { backgroundColor: custom.fill, borderRadius: custom.radius },
            ]}
          >
            {custom.showIcon ? (
              <Ionicons
                name="arrow-forward"
                size={18}
                color={custom.ink === "#000000" ? "black" : custom.ink}
              />
            ) : null}
            <Text
              style={[
                styles.btnText,
                {
                  fontFamily: fontLoaded ? "SfProRounded" : undefined,
                  color: custom.ink,
                  fontSize: custom.fontSize,
                },
              ]}
            >
              {custom.label}
            </Text>
          </View>
        </Button.Content>

        <Button.Loading>
          <Button.Indicator>
            <CircularLoader
              activeColor={custom.ink}
              size={18}
              strokeWidth={2.5}
              enableBlur
              gradientLength={50}
              duration={500}
            />
          </Button.Indicator>
          <Button.Label
            color={custom.ink === "#000000" ? "#000" : custom.ink}
            style={{
              fontFamily: fontLoaded ? "SfProRounded" : undefined,
            }}
          >
            {custom.loadingLabel}
          </Button.Label>
        </Button.Loading>
      </>
    ),
    [
      fontLoaded,
      custom.fill,
      custom.ink,
      custom.radius,
      custom.fontSize,
      custom.showIcon,
      custom.label,
      custom.loadingLabel,
    ],
  );

  return (
    <Showcase>
      <View style={[styles.container, { backgroundColor: custom.background }]}>
        <StatusBar style="light" />

        <View style={fontReady ? undefined : styles.pending}>
          <Button.Root
            isLoading={loading}
            isPressed={pressed}
            onPress={onPress}
            width={custom.width}
            height={custom.height}
            backgroundColor={custom.fill === "#ffffff" ? "#fff" : custom.fill}
            loadingBackgroundColor={custom.loadingFill}
            borderRadius={custom.radius === 16 ? undefined : custom.radius}
          >
            {layers}
          </Button.Root>
        </View>
      </View>
    </Showcase>
  );
}

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

    justifyContent: "center",
  },
  btn: {
    flexDirection: "row",
    alignItems: "center",
    gap: 10,
    backgroundColor: "#fff",
    paddingVertical: 16,
    paddingHorizontal: 32,
    borderRadius: 16,
  },
  btnText: {
    fontSize: 17,
    fontWeight: "600",
    color: "#000",
  },
});

Пропсы

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

Тип ReactNode

isLoading?

Тип boolean

isPressed?

Тип boolean

onPress?

Тип () => void

width?

Тип number

height?

Тип number

backgroundColor?

Тип string

loadingBackgroundColor?

Тип string

borderRadius?

Тип number

gradientColors?

Тип string[]

withPressAnimation?

Тип boolean

animationDuration?

Тип number

disabled?

Тип boolean

accessibilityLabel?

Тип string

style?

Тип StyleProp<ViewStyle>