Обёртка для скриншота или видео в первом экране. Сначала медиа лежит наклонённым назад и немного уменьшенным, а по мере прокрутки выпрямляется, поднимается и увеличивается до полного размера.
Движение привязано к общему значению прокрутки scrollY из useAnimatedScrollHandler и сглажено пружиной — всё считается на UI-потоке Reanimated без перерисовок React. При «Уменьшении движения» медиа сразу стоит ровно. Работает на iOS, Android и в вебе.
Поставьте зависимости:
npm install react-native-reanimatedpnpm add react-native-reanimatedyarn add react-native-reanimatedbun 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>