Компоненты/Выезд медиа в герое

Выезд медиа в герое

28 сентября 2026

Скриншот в герое выпрямляется из наклона и увеличивается по мере прокрутки на пружинной анимации.

Установка

npx astra-rn add hero-media-scroll-out
pnpm dlx astra-rn add hero-media-scroll-out
yarn dlx astra-rn add hero-media-scroll-out
bunx --bun astra-rn add hero-media-scroll-out

Файлы

index.tsx
НазадВеер карточекДалееПортфолио-полосы

Обёртка для скриншота или видео в первом экране. Сначала медиа лежит наклонённым назад и немного уменьшенным, а по мере прокрутки выпрямляется, поднимается и увеличивается до полного размера.

Движение привязано к общему значению прокрутки scrollY из useAnimatedScrollHandler и сглажено пружиной — всё считается на UI-потоке Reanimated без перерисовок React. При «Уменьшении движения» медиа сразу стоит ровно. Работает на iOS, Android и в вебе.

Вручную

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

npm install react-native-reanimated
pnpm add react-native-reanimated
yarn add react-native-reanimated
bun add react-native-reanimated

Скопируйте эти файлы в src/shared/components/organisms/hero-media-scroll-out/:

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

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

import React, { useState } from "react";
import { Image, StyleSheet, Text, View } from "react-native";
import Animated, { useAnimatedScrollHandler, useSharedValue } from "react-native-reanimated";

import { HeroMediaScrollOut } from "@/shared/components/organisms/hero-media-scroll-out";
import { PHOTO_SOURCES } from "~/assets/photos";
import { Showcase, useCustomize } from "~/showcase";

const HEIGHT = 560;

const CUSTOMIZE = [
  { key: "tilt", label: "Начальный наклон", type: "slider", min: 0, max: 45, step: 1, unit: "°", default: 18 },
  { key: "scale", label: "Начальный масштаб", type: "slider", min: 0.6, max: 1, step: 0.01, default: 0.9 },
  { key: "distance", label: "Точка выравнивания", type: "slider", min: 0, max: 1, step: 0.05, default: 0.3 },
  { key: "radius", label: "Скругление", type: "slider", min: 0, max: 48, step: 1, unit: "px", default: 22 },
  { key: "title", label: "Заголовок", type: "text", default: "Запускайте быстрее, вместе.", maxLength: 60 },
  { key: "subtitle", label: "Подзаголовок", type: "text", default: "Прокрутите, чтобы поднять скриншот.", maxLength: 80 },
  { key: "image", label: "Скриншот", type: "image", default: PHOTO_SOURCES["cinematic-carousel-1"] },
] as const;

export default function HeroMediaScrollOutScreen() {
  const custom = useCustomize(CUSTOMIZE);
  const scrollY = useSharedValue(0);
  const onScroll = useAnimatedScrollHandler((e) => {
    scrollY.value = e.contentOffset.y;
  });
  const [viewport, setViewport] = useState(HEIGHT);
  const source = typeof custom.image === "string" ? { uri: custom.image } : custom.image;

  return (
    <Showcase>
      <View style={styles.stage}>
        <Animated.ScrollView
          style={styles.scroller}
          onLayout={(e) => setViewport(Math.round(e.nativeEvent.layout.height) || HEIGHT)}
          contentContainerStyle={styles.content}
          onScroll={onScroll}
          scrollEventThrottle={16}
          showsVerticalScrollIndicator={false}
        >
          <View style={[styles.intro, { height: 0.7 * viewport }]}>
            <Text style={styles.title}>{custom.title}</Text>
            <Text style={styles.subtitle}>{custom.subtitle}</Text>
          </View>
          <HeroMediaScrollOut
            key={custom.distance}
            scrollY={scrollY}
            containerHeight={viewport}
            distance={custom.distance}
            tilt={custom.tilt}
            scale={custom.scale}
            radius={custom.radius}
            style={styles.media}
          >
            <Image source={source} resizeMode="cover" style={styles.image} />
          </HeroMediaScrollOut>
          <View style={{ height: 0.5 * viewport }} />
        </Animated.ScrollView>
      </View>
    </Showcase>
  );
}

const styles = StyleSheet.create({
  stage: {
    flex: 1,
    alignItems: "center",
    justifyContent: "center",
    backgroundColor: "#0a0a0a",
  },
  scroller: {
    flex: 1,
    width: "100%",
  },
  content: {
    paddingHorizontal: 32,
    alignItems: "center",
  },
  intro: {
    width: "100%",
    alignItems: "center",
    justifyContent: "center",
  },
  title: {
    fontSize: 30,
    lineHeight: 36,
    fontWeight: "600",
    letterSpacing: -1.2,
    textAlign: "center",
    color: "#fafafa",
  },
  subtitle: {
    marginTop: 12,
    fontSize: 14,
    lineHeight: 20,
    textAlign: "center",
    color: "rgba(250,250,250,0.6)",
  },
  media: {
    maxWidth: 768,
  },
  image: {
    width: "100%",
    aspectRatio: 16 / 9,
  },
});

Передайте scrollY из Animated.ScrollView, внутри которого стоит компонент, и containerHeight — высоту видимой области.

Пропсы

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

Тип ReactNode

scrollY?

Тип SharedValue<number>

containerHeight?

Тип number

offsetTop?

Тип number

tilt?

Тип number

scale?

Тип number

distance?

Тип number

radius?

Тип number

style?

Тип StyleProp<ViewStyle>

frameStyle?

Тип StyleProp<ViewStyle>