Карточка с фото, надзаголовком, заголовком и подписью. По нажатию она пружинно вырастает прямо из своего места в модальное окно по центру экрана: фото растягивается, а под ним проявляется полный текст из children. Закрыть окно можно крестиком, нажатием на фон или системной кнопкой «Назад» — карточка так же плавно возвращается на место.
Положение карточки измеряется перед открытием, поэтому морфинг работает и внутри прокручиваемых списков. Вся анимация идёт на UI-потоке Reanimated, а при «Уменьшении движения» окно просто проявляется. Работает на iOS, Android и в вебе.
Поставьте зависимости:
npm install expo-linear-gradient react-native-reanimated react-native-svgpnpm add expo-linear-gradient react-native-reanimated react-native-svgyarn add expo-linear-gradient react-native-reanimated react-native-svgbun add expo-linear-gradient react-native-reanimated react-native-svgСкопируйте эти файлы в src/shared/components/organisms/expanding-card/:
Проверьте алиас путей. Импорты в коде выше записаны так, как их пишет astra-rn add: @/shared/components, @/shared/utils и @/shared/hooks. Нужен алиас "@/*": ["./src/*"] в tsconfig.json — или поправьте импорты под свои пути.
import React from "react";
import { StyleSheet, View } from "react-native";
import { ExpandingCard } from "@/shared/components/organisms/expanding-card";
import { Showcase, useCustomize } from "~/showcase";
import { PHOTO_SOURCES } from "~/assets/photos";
const CUSTOMIZE = [
{ key: "height", label: "Высота карточки", type: "slider", min: 220, max: 520, step: 4, unit: "px", default: 360 },
{ key: "width", label: "Ширина карточки", type: "slider", min: 200, max: 440, step: 4, unit: "px", default: 300 },
{ key: "radius", label: "Скругление", type: "slider", min: 0, max: 48, step: 1, unit: "px", default: 28 },
{ key: "openWidth", label: "Ширина окна", type: "slider", min: 320, max: 800, step: 8, unit: "px", default: 512 },
{ key: "eyebrow", label: "Надзаголовок", type: "text", default: "Избранное", maxLength: 30 },
{ key: "title", label: "Заголовок", type: "text", default: "Горы на рассвете", maxLength: 50 },
{ key: "subtitle", label: "Подпись", type: "text", default: "Выходные у самого подножия гор.", maxLength: 80 },
{ key: "image", label: "Фото", type: "image", default: PHOTO_SOURCES["cinematic-carousel-2"] },
] as const;
export default function ExpandingCardScreen() {
const custom = useCustomize(CUSTOMIZE);
return (
<Showcase>
<View style={styles.container}>
<View style={styles.stage}>
<View style={[styles.slot, { maxWidth: custom.width }]}>
<ExpandingCard
image={custom.image}
eyebrow={custom.eyebrow || undefined}
title={custom.title}
subtitle={custom.subtitle || undefined}
height={custom.height}
radius={custom.radius}
openWidth={custom.openWidth}
>
{"Выезжайте до рассвета, пока озеро ещё неподвижно и отражает вторую вершину. Первая электричка из города привозит вас туда, когда пик уже ловит свет, которого улицы ещё не видели."}
{"Берите вещи на все четыре сезона. Горы создают свою погоду, а лучшие снимки случаются в те десять минут, которые никто не планировал."}
</ExpandingCard>
</View>
</View>
</View>
</Showcase>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: "center",
justifyContent: "center",
paddingVertical: 48,
backgroundColor: "#0a0a0a",
},
stage: {
width: "100%",
alignItems: "center",
justifyContent: "center",
paddingHorizontal: 16,
paddingVertical: 48,
},
slot: {
width: "100%",
},
});imageТип ExpandingCardImage
eyebrow?Тип string
titleТип string
subtitle?Тип string
children?Тип ReactNode
height?Тип number
radius?Тип number
openWidth?Тип number
theme?Тип "dark" | "light"
style?Тип StyleProp<ViewStyle>