Между двумя словами стоит небольшая плашка. При прокрутке она плавно растягивается до размеров всей сцены, а слова по бокам растворяются. Движение идёт через пружину Reanimated, поэтому остановка прокрутки не обрывает анимацию резко, а при прокрутке назад плашка сжимается до исходного размера.
Внутрь можно положить видео, картинку или любой другой элемент — он растянется по размеру рамки. Прокрутку даёт хук useZoomRevealScroll: подключите его onScroll к Animated.ScrollView и передайте scrollY в компонент. При «Уменьшении движения» пружина отключается. Работает на iOS, Android и в вебе.
Поставьте зависимости:
npm install react-native-reanimatedpnpm add react-native-reanimatedyarn add react-native-reanimatedbun add react-native-reanimatedСкопируйте эти файлы в src/shared/components/organisms/scroll-zoom-reveal/:
Проверьте алиас путей. Импорты в коде выше записаны так, как их пишет astra-rn add: @/shared/components, @/shared/utils и @/shared/hooks. Нужен алиас "@/*": ["./src/*"] в tsconfig.json — или поправьте импорты под свои пути.
import React, { useState } from "react";
import { StyleSheet, Text, View, type LayoutChangeEvent } from "react-native";
import Animated from "react-native-reanimated";
import { VideoView, useVideoPlayer } from "expo-video";
import {
ScrollZoomReveal,
useZoomRevealScroll,
} from "@/shared/components/organisms/scroll-zoom-reveal";
import VIDEO from "~/assets/video/video-artist.mp4";
import { Showcase, useCustomize } from "~/showcase";
const CUSTOMIZE = [
{ key: "leading", label: "Слово слева", type: "text", default: "Над", maxLength: 24 },
{ key: "trailing", label: "Слово справа", type: "text", default: "морем", maxLength: 32 },
{ key: "pillWidth", label: "Ширина плашки", type: "slider", min: 32, max: 240, step: 4, unit: "px", default: 88 },
{ key: "pillHeight", label: "Высота плашки", type: "slider", min: 32, max: 240, step: 4, unit: "px", default: 88 },
{ key: "radius", label: "Скругление", type: "slider", min: 0, max: 60, step: 1, unit: "px", default: 24 },
{ key: "screens", label: "Длина прокрутки", type: "slider", min: 2, max: 6, step: 0.5, unit: "экр.", default: 3 },
] as const;
function ArtistVideo() {
const player = useVideoPlayer(VIDEO, (instance) => {
instance.loop = true;
instance.muted = true;
instance.play();
});
return (
<VideoView
player={player}
style={styles.video}
contentFit="cover"
nativeControls={false}
allowsPictureInPicture={false}
/>
);
}
export default function ScrollZoomRevealScreen() {
const custom = useCustomize(CUSTOMIZE);
const { scrollY, onScroll } = useZoomRevealScroll();
const [stage, setStage] = useState(0);
const onLayout = (event: LayoutChangeEvent) => {
const next = Math.round(event.nativeEvent.layout.height);
setStage((prev) => (prev === next ? prev : next));
};
return (
<Showcase>
<View style={styles.page} onLayout={onLayout}>
{stage > 0 ? (
<Animated.ScrollView
style={styles.scroller}
onScroll={onScroll}
scrollEventThrottle={16}
showsVerticalScrollIndicator={false}
>
<ScrollZoomReveal
scrollY={scrollY}
height={custom.screens * stage}
stageHeight={stage}
leading={custom.leading}
trailing={custom.trailing}
leadingStyle={styles.leading}
pillWidth={custom.pillWidth}
pillHeight={custom.pillHeight}
radius={custom.radius}
>
<ArtistVideo />
</ScrollZoomReveal>
<View style={[styles.after, { height: stage / 2 }]}>
<Text style={styles.afterText}>Прокрутите обратно, чтобы снова уменьшить.</Text>
</View>
</Animated.ScrollView>
) : null}
</View>
</Showcase>
);
}
const styles = StyleSheet.create({
page: {
flex: 1,
backgroundColor: "#0a0a0a",
},
scroller: {
flex: 1,
},
video: {
width: "100%",
height: "100%",
},
leading: {
color: "#71717a",
},
after: {
alignItems: "center",
justifyContent: "center",
padding: 24,
},
afterText: {
fontSize: 14,
lineHeight: 20,
color: "rgba(250,250,250,0.44)",
},
});scrollYТип SharedValue<number>
childrenТип ReactNode
leading?Тип ReactNode
trailing?Тип ReactNode
pillWidth?Тип number
pillHeight?Тип number
radius?Тип number
height?Тип number
stageHeight?Тип number
viewportHeight?Тип number
style?Тип StyleProp<ViewStyle>
frameStyle?Тип StyleProp<ViewStyle>
textStyle?Тип StyleProp<TextStyle>
leadingStyle?Тип StyleProp<TextStyle>
trailingStyle?Тип StyleProp<TextStyle>