缩放过渡

编辑页面

了解如何使用缩放过渡,在使用 Expo Router for iOS 时创建屏幕之间的流畅动画。


For the complete documentation index, see llms.txt. Use this Use this file to discover all available pages.

重要 缩放过渡是一个 alpha API,仅在 iOS 上可用,适用于 Expo SDK 55 及更高版本。该 API 可能会发生破坏性更改。

缩放过渡通过从源元素放大到目标屏幕,在屏幕之间导航时提供流畅的动画效果。此功能利用 iOS 18+ 原生缩放过渡 API 创建共享的、交互式的过渡,从而在路由之间产生空间感。例如,某个卡片缩略图可以在下一个路由中过渡为全宽横幅。

开始使用

要实现缩放过渡,你需要使用 Link.AppleZoom 组件来标记源元素,并可选地使用 Link.AppleZoomTarget 来指定目标屏幕上的对齐方式。

基本示例

要为链接启用缩放过渡,请在你的屏幕中用 Link.AppleZoom 包裹源(Image)元素:

src/app/index.tsx
import { View, Text, StyleSheet, Pressable } from 'react-native'; import { Link } from 'expo-router'; import { Image } from 'expo-image'; export default function HomeScreen() { return ( <View style={styles.container}> <Link href="/image" asChild> <Link.AppleZoom> <Pressable> <Image source={{ uri: 'https://example.com/image-1.jpg' }} style={{ width: 100, height: 200 }} /> </Pressable> </Link.AppleZoom> </Link> </View> ); }

在目标屏幕中,定义 Image 组件:

src/app/image.tsx
import { View, Text, StyleSheet } from 'react-native'; import { Image } from 'expo-image'; export default function DetailsScreen() { return <Image source={{ uri: 'https://example.com/image-1.jpg' }} style={{ flex: 1 }} />; }

使用 Link.AppleZoom

Link.AppleZoom 组件会包裹你想要从中进行缩放的元素。如果你想在缩放内容旁边包含其他元素,它可用于标记缩放过渡的源。

<Link href="/image" asChild> <Pressable> <Link.AppleZoom> <View>{/* 你的内容 */}</View> </Link.AppleZoom> <Text>副标题</Text> </Pressable> </Link>

信息 Link.AppleZoom 只接受一个子组件。如果你需要包裹多个子元素,请使用 View 或其他容器组件。

自定义对齐方式

你可以使用 Link.AppleZoomTarget 元素来指定缩放后元素在目标屏幕上的对齐方式。

src/app/image.tsx
export default function ImageScreen() { return ( <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}> <Link.AppleZoomTarget> <Image source={{ uri: 'https://example.com/image-1.jpg' }} style={{ width: '100%' }} /> </Link.AppleZoomTarget> </View> ); }

如果你需要对对齐矩形进行更多控制,可以向 Link.AppleZoom 传递 alignmentRect 属性。不过,通常在使用 Link.AppleZoomTarget 时并不需要这样做。

信息 alignmentRect 属性在内部依赖 alignmentRectProvider API。

<Link.AppleZoom alignmentRect={{ x: 0, y: 0, width: 200, height: 300 }}> <Image source={{ uri: 'https://example.com/image-1.jpg' }} style={{ width: 100, height: 150 }} /> </Link.AppleZoom>

完整示例

下面是一个更复杂的示例,展示了一个带有缩放过渡到详情视图的画廊网格。源屏幕组件(src/app/index.tsx)使用 Link.AppleZoom 包裹一个 Image 组件:

src/app/index.tsx
import { Image } from 'expo-image'; import { Link } from 'expo-router'; import { useState } from 'react'; import { Text, Pressable, ScrollView, StyleSheet } from 'react-native'; const IMAGES = [ // 在此定义你的图片数组。 ]; export default function Index() { return ( <ScrollView style={styles.scrollView} contentContainerStyle={styles.scrollViewContent} contentInsetAdjustmentBehavior="automatic"> {IMAGES.map((_, index) => ( <Thumbnail key={index} index={index} /> ))} </ScrollView> ); } function Thumbnail({ index }: { index: number }) { const [size, setSize] = useState<{ width: number; height: number } | null>(null); return ( <Link href={{ pathname: `/image/[id]`, // 你需要将图片尺寸传递给详情页,这样布局才能在首次渲染时被测量。 params: { id: index, width: size?.width, height: size?.height }, }} asChild> <Pressable style={styles.thumbnail}> <Link.AppleZoom> <Image source={IMAGES[index % IMAGES.length]} style={styles.thumbnailImage} onLoad={e => setSize({ width: e.source.width, height: e.source.height })} /> </Link.AppleZoom> <Text style={{ textAlign: 'center' }}>照片 {index + 1}</Text> </Pressable> </Link> ); } const styles = StyleSheet.create({ scrollView: { flex: 1, backgroundColor: '#fff', padding: 16, }, scrollViewContent: { justifyContent: 'center', flexDirection: 'row', flexWrap: 'wrap', gap: 16, }, thumbnail: { width: 170, aspectRatio: 1, }, thumbnailImage: { width: '100%', height: '100%', borderRadius: 8, }, });

在目标屏幕中,使用 Link.AppleZoomTarget 来指定缩放后元素的对齐方式:

src/app/image/[id].tsx
import { Image } from 'expo-image'; import { Link, useLocalSearchParams } from 'expo-router'; import { useMemo } from 'react'; import { StyleSheet, useWindowDimensions, View } from 'react-native'; export default function ImagePage() { const params = useLocalSearchParams(); const index = params.id ? parseInt(params.id as string, 10) : 0; const imageSource = IMAGES[index % IMAGES.length]; const imageSize = { width: parseInt(params.width as string, 10), height: parseInt(params.height as string, 10), }; const windowDimensions = useWindowDimensions(); // 计算在保持宽高比的情况下适配窗口的尺寸。 const computedSize = useMemo(() => { if (!imageSize.width || !imageSize.height) { return { width: windowDimensions.width, height: windowDimensions.height }; } const widthRatio = windowDimensions.width / imageSize.width; const heightRatio = windowDimensions.height / imageSize.height; const minRatio = Math.min(widthRatio, heightRatio); return { width: imageSize.width * minRatio, height: imageSize.height * minRatio, }; }, [imageSize, windowDimensions]); return ( <View style={styles.container}> <Link.AppleZoomTarget> <View style={{ ...computedSize }}> <Image source={imageSource} style={styles.image} /> </View> </Link.AppleZoomTarget> </View> ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#fff', justifyContent: 'center', alignItems: 'center', }, image: { width: '100%', height: '100%', }, });

控制关闭手势

usePreventZoomTransitionDismissal 钩子允许你控制使用缩放过渡的屏幕上的交互式滑动关闭手势。当你想防止误关闭或将关闭限制在特定屏幕区域时,这非常有用。

完全禁用关闭

不传任何选项调用该钩子,即可完全禁用滑动关闭手势:

src/app/detail.tsx
import { usePreventZoomTransitionDismissal } from 'expo-router'; export default function DetailScreen() { usePreventZoomTransitionDismissal(); // 现在已禁用关闭手势 - 用户必须使用导航控件返回 return <View>{/* 内容 */}</View>; }

将关闭限制在特定区域

使用 unstable_dismissalBoundsRect 选项定义允许关闭手势的矩形区域。这对于图片查看器很有用,因为你可能只希望从图片区域触发关闭:

src/app/image.tsx
import { usePreventZoomTransitionDismissal } from 'expo-router'; import { View, StyleSheet } from 'react-native'; import { Image } from 'expo-image'; export default function DetailScreen() { // 仅允许从此矩形内开始的关闭手势 usePreventZoomTransitionDismissal({ unstable_dismissalBoundsRect: { minX: 100, minY: 100, maxX: 300, maxY: 300 }, }); return ( <View style={styles.container}> {/* 关闭区域的可视化指示(用于演示) */} <View style={styles.dismissalZone} /> </View> ); } const styles = StyleSheet.create({ container: { flex: 1, }, image: { flex: 1, }, dismissalZone: { position: 'absolute', left: 100, top: 100, width: 200, // maxX - minX = 300 - 100 height: 200, // maxY - minY = 300 - 100 borderWidth: 2, borderColor: 'rgba(0, 122, 255, 0.5)', borderStyle: 'dashed', backgroundColor: 'rgba(0, 122, 255, 0.1)', }, });

信息 unstable_dismissalBoundsRect 选项在内部依赖 interactiveDismissShouldBegin API。

平台支持

缩放过渡仅适用于 iOS 18 及更高版本。在较旧的 iOS 版本或其他平台上,组件会正常渲染,但不会有缩放动画效果。

缩放过渡组件会自动检测平台支持情况,并在不支持的平台上优雅降级为标准导航。

已知限制

在带有标题栏的情况下使用缩放过渡

我们建议避免在带有标题栏(导航栏)的屏幕之间导航时使用缩放过渡。原生 iOS 缩放过渡 API 存在已知问题,当涉及标题栏时可能导致视觉故障或意外行为。

在 `Link.Preview` 中使用缩放过渡

当将 Link.Preview 与缩放过渡结合使用时,目标屏幕必须使用模态展示,例如 presentation: 'fullScreenModal'。这是底层 iOS 缩放过渡 API 的限制。当从 Link.Preview 导航到非模态屏幕时,缩放过渡将不会按预期工作,并会回退为标准导航过渡。

usePreventZoomTransitionDismissal 不能用于带有模态展示的屏幕

usePreventZoomTransitionDismissal 钩子不能用于带有模态展示的屏幕,例如 presentation: 'fullScreenModal'。在模态屏幕中使用时,该钩子不会产生任何效果,关闭手势将正常工作。

单子组件要求

Link.AppleZoomLink.AppleZoomTarget 都只接受一个子组件。如果你尝试传递多个子组件,将会记录警告,并且组件将无法正常渲染。

错误:

<Link.AppleZoom> <View /> <Text /> </Link.AppleZoom>

正确:

<Link.AppleZoom> <View> <Image /> <Text /> </View> </Link.AppleZoom>
打开或关闭屏幕时出现明显延迟

在导航到使用缩放过渡的屏幕或从这些屏幕关闭时,你可能会遇到明显的延迟(大约 1 秒),尤其是在快速执行打开/关闭/打开手势时。这个延迟高于使用相同缩放过渡 API 的原生 iOS 应用中通常会看到的情况。

这是 react-native-screens 上游的一个问题,与其在 iOS 上处理过渡的方式有关。Expo 团队正在与 react-native-screens 团队积极合作以改进这一点。有关更新和更多细节,请参见此 GitHub Issue

仅支持 router 的 Stack 导航器

缩放过渡功能仅在使用 router 内置的 Stack 导航器时受支持。 如果你尝试将带有缩放过渡的 Link 用于不属于 Stack 导航器的屏幕,缩放过渡将不会按预期工作。

仅限 iOS 18+

缩放过渡功能需要 iOS 18 或更高版本。虽然这些组件会在较旧版本上渲染,但不会应用缩放动画。