Компоненты/Нажимаемое

Нажимаемое

21 августа 2026

Обёртка нажатия с настраиваемым масштабом, откликом и движением

Установка

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

Файлы

index.tsx
НазадПоток цифрДалееКнопка сохранения

Вручную

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

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

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

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

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

import { useEffect, useMemo, useRef } from "react";
import { 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 Animated, {
  useAnimatedStyle,
  useSharedValue,
  withSpring,
} from "react-native-reanimated";
import { Pressable } from "@/shared/components/atoms/pressable";
import { Showcase, useAutoplay, useCustomize } from "~/showcase";

const PRESS_SPRING = { stiffness: 550, mass: 0.5, damping: 50 };
const BUTTON_HEX = "#ffffff";
const LABEL_HEX = "#000000";

const CUSTOMIZE = [
  { key: "label", label: "Текст кнопки", type: "text", default: "Get Started", maxLength: 24 },
  { key: "background", label: "Цвет кнопки", type: "color", default: BUTTON_HEX },
  { key: "color", label: "Цвет текста", type: "color", default: LABEL_HEX },
  { key: "scale", label: "Сжатие", type: "slider", min: 0.8, max: 1, step: 0.01, default: 0.97 },
  { key: "rotate", label: "Поворот", type: "slider", min: -15, max: 15, step: 0.5, unit: "°", default: -5 },
  { key: "stiffness", label: "Жёсткость", type: "slider", min: 100, max: 1000, step: 10, default: 550 },
  { key: "damping", label: "Затухание", type: "slider", min: 5, max: 100, step: 1, default: 50 },
  { key: "hold", label: "Удержание", type: "slider", min: 200, max: 2000, step: 20, unit: "ms", default: 580 },
] as const;

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

  const custom = useCustomize(CUSTOMIZE);
  const demoPress = useSharedValue(0);
  const pressDepth = useSharedValue(1 - custom.scale);
  const pressRotate = useSharedValue(custom.rotate);

  useEffect(() => {
    pressDepth.value = 1 - custom.scale;
    pressRotate.value = custom.rotate;
  }, [custom.scale, custom.rotate, pressDepth, pressRotate]);

  const spring = useMemo(
    () =>
      custom.stiffness === PRESS_SPRING.stiffness &&
      custom.damping === PRESS_SPRING.damping
        ? PRESS_SPRING
        : { ...PRESS_SPRING, stiffness: custom.stiffness, damping: custom.damping },
    [custom.stiffness, custom.damping],
  );
  const live = useRef({ spring, hold: custom.hold });
  live.current = { spring, hold: custom.hold };

  const demoPressStyle = useAnimatedStyle(() => ({
    transform: [
      { scale: 1 - pressDepth.value * demoPress.value },
      { rotate: `${pressRotate.value * demoPress.value}deg` },
    ],
  }));

  useAutoplay(async ({ wait }) => {
    try {
      await wait(800);
      demoPress.value = withSpring(1, live.current.spring);
      await wait(live.current.hold);
      demoPress.value = withSpring(0, live.current.spring);
      await wait(300);
    } catch (error) {
      demoPress.value = withSpring(0, live.current.spring);
      throw error;
    }
  });

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

        <Animated.View style={demoPressStyle}>
          <Pressable
            pressAnimation={{
              scale: custom.scale,
              useSpring: true,
              stiffness: custom.stiffness,
              mass: 0.5,
              damping: custom.damping,
              rotate: custom.rotate,
            }}
            feedback={{ haptic: true, hapticType: "medium" }}
          >
            <View
              style={[
                styles.btn,
                custom.background !== BUTTON_HEX && { backgroundColor: custom.background },
                !fontLoaded && styles.pending,
              ]}
            >
              <Text
                style={[
                  styles.btnText,
                  custom.color !== LABEL_HEX && { color: custom.color },
                  fontLoaded && { fontFamily: "SfProRounded" },
                ]}
              >
                {custom.label}
              </Text>
            </View>
          </Pressable>
        </Animated.View>
      </GestureHandlerRootView>
    </Showcase>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#0a0a0a",
    justifyContent: "center",
    alignItems: "center",
  },
  btn: {
    flexDirection: "row",
    alignItems: "center",
    gap: 10,
    backgroundColor: "#fff",
    paddingVertical: 16,
    paddingHorizontal: 32,
    borderRadius: 99,
  },
  pending: {
    opacity: 0,
  },
  btnText: {
    fontSize: 17,
    fontWeight: "600",
    color: "#000",
  },
});

Пропсы

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

Тип ReactNode

onPress?

Тип (event: GestureResponderEvent) => void

onLongPress?

Тип (event: GestureResponderEvent) => void

onPressIn?

Тип (event: GestureResponderEvent) => void

onPressOut?

Тип (event: GestureResponderEvent) => void

longPressDuration?

Тип number

disabled?

Тип boolean

style?

Тип ViewStyle | ViewStyle[]

pressAnimation?

Тип AnimationConfig

longPressAnimation?

Тип AnimationConfig

customAnimation?

Тип CustomAnimation

feedback?

Тип FeedbackConfig

blur?

Тип BlurConfig

disableAnimations?

Тип boolean

skipGlobalCallback?

Тип boolean

testID?

Тип string

accessibilityLabel?

Тип string

accessibilityHint?

Тип string

accessibilityRole?

Тип "button" | "link" | "none"