缩放过渡

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


重要 Zoom 转场是一个仅在 iOS 上、Expo SDK 55 及更高版本可用的 Alpha 级 API。该 API 可能会发生破坏性更改。

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

🌐 Zoom transitions provide a fluid animation effect when navigating between screens by zooming from a source element to the destination screen. This feature leverages iOS 18+ native zoom transition API to create shared, interactive transitions that produce a sense of spatial awareness between routes. For example, a card thumbnail may transition to become a full-width banner on the next route.

开始使用

🌐 Get started

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

🌐 To implement zoom transitions, you need to use the Link.AppleZoom component to mark the source element and optionally Link.AppleZoomTarget to specify the target alignment on the destination screen.

基本示例

🌐 Basic example

要为链接激活缩放过渡,请在屏幕中使用 Link.AppleZoom 封装源元素(Image):

🌐 To activate zoom transition for a link, wrap the source (Image) element with Link.AppleZoom in your screen:

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 组件:

🌐 In the destination screen, define the Image component:

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

🌐 Using Link.AppleZoom

Link.AppleZoom 组件会封装你想要进行缩放的元素。如果你想在缩放的内容旁边包含其他元素,它在标记缩放过渡的来源时非常有用。

🌐 The Link.AppleZoom component wraps the element you want to zoom from. It is useful for marking the source of the zoom transition, if you want to include additional elements alongside the zoomed content.

<Link href="/image" asChild> <Pressable> <Link.AppleZoom> <View>{/* Your content */}</View> </Link.AppleZoom> <Text>Subtitle</Text> </Pressable> </Link>

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

自定义对齐

🌐 Customizing alignment

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

🌐 You can specify the alignment of the zoomed element on the destination screen by using Link.AppleZoomTarget element.

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,通常不需要这样做。

🌐 If you need more control over the alignment rectangle, you can pass an alignmentRect prop to Link.AppleZoom. However, this is normally not necessary if you use 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>

完整示例

🌐 Complete example

这是一个更复杂的示例,显示带有缩放过渡到详细视图的图库网格。源屏幕组件(src/app/index.tsx)使用 Link.AppleZoom 来封装一个 Image 组件:

🌐 Here's a more complex example showing a gallery grid with zoom transitions to detail views. The source screen component (src/app/index.tsx) uses Link.AppleZoom to wrap an Image component:

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 = [ // Define your array of images here. ]; 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]`, // You need to pass the image size to the detail page, so that the layout can be measured during the first render. 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' }}>Photo {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 用于指定缩放元素的对齐方式:

🌐 In the destination screen, the Link.AppleZoomTarget is used to specify the alignment of the zoomed element:

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(); // Compute the size to fit within the window while maintaining aspect ratio. 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%', }, });

控制解除手势

🌐 Controlling dismissal gestures

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

🌐 The usePreventZoomTransitionDismissal hook allows you to control the interactive swipe-to-dismiss gesture on screens using zoom transitions. This is useful when you want to prevent accidental dismissals or restrict dismissal to specific screen areas.

完全禁用退出

🌐 Disabling dismissal completely

调用该钩子而不使用任何选项可以完全禁用滑动取消手势:

🌐 Call the hook without any options to completely disable the swipe-to-dismiss gesture:

src/app/detail.tsx
import { usePreventZoomTransitionDismissal } from 'expo-router'; export default function DetailScreen() { usePreventZoomTransitionDismissal(); // Dismissal gesture is now disabled - users must use navigation controls to go back return <View>{/* Content */}</View>; }

将退出限制在特定区域

🌐 Restricting dismissal to a specific area

使用 unstable_dismissalBoundsRect 选项来定义允许滑动手势关闭的矩形区域。这在图片查看器中非常有用,因为你可能只希望在图片区域内才能关闭:

🌐 Use the unstable_dismissalBoundsRect option to define a rectangle where dismissal gestures are allowed. This is useful for image viewers where you want dismissal only from the image area:

src/app/image.tsx
import { usePreventZoomTransitionDismissal } from 'expo-router'; import { View, StyleSheet } from 'react-native'; import { Image } from 'expo-image'; export default function DetailScreen() { // Only allow dismissal gestures that start within this rectangle usePreventZoomTransitionDismissal({ unstable_dismissalBoundsRect: { minX: 100, minY: 100, maxX: 300, maxY: 300 }, }); return ( <View style={styles.container}> {/* Visual indicator of the dismissal zone (for demonstration) */} <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。

平台支持

🌐 Platform support

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

🌐 Zoom transitions is only available on iOS 18 and later. On older iOS versions or other platforms, the component will render normally without the zoom animation effect.

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

🌐 The zoom transition components automatically detect platform support and gracefully degrade to standard navigation on unsupported platforms.

已知的限制

🌐 Known limitations

使用带标题的缩放过渡

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

🌐 We recommend avoiding the use of zoom transitions when navigating between screens that have a header (navigation bar). There are known issues with the native iOS zoom transition API that can lead to visual glitches or unexpected behavior when headers are involved.

使用带有 Link.Preview 的缩放过渡

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

🌐 When using Link.Preview in combination with zoom transitions, the target screen must use the modal presentation, for example, presentation: 'fullScreenModal'. This is the limitation of the underlying iOS zoom transition API. When navigating to a non-modal screen from a Link.Preview, the zoom transition will not work as expected and will fall back to a standard navigation transition.

usePreventZoomTransitionDismissal 不能在具有模态渲染的屏幕中使用

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

🌐 The usePreventZoomTransitionDismissal hook cannot be used in screens that have a modal presentation, for example, presentation: 'fullScreenModal'. When used in a modal screen, the hook will not have any effect and dismissal gestures will function as normal.

独生子女政策

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

🌐 Both Link.AppleZoom and Link.AppleZoomTarget only accept a single child component. If you attempt to pass multiple children, a warning will be logged and the component will not render properly.

不正确:

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

正确:

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

在导航到或关闭使用缩放过渡的屏幕时,你可能会感受到明显的延迟(大约 1 秒),尤其是在快速执行打开/关闭/再打开手势时。这种延迟比使用相同缩放过渡 API 的原生 iOS 应用所经历的要高。

🌐 You may experience a noticeable delay (approximately 1 second) when navigating to or dismissing screens that use zoom transitions, especially when performing rapid open/close/open gestures. This latency is higher than what you would see with native iOS apps using the same zoom transition API.

这是 react-native-screens 的上游问题,涉及它在 iOS 上处理过渡的方式。Expo 团队正在积极与 react-native-screens 团队合作改进此问题。有关更新和更多详情,请参见这个 GitHub 问题

🌐 This is an upstream issue in react-native-screens related to how it handles transitions on iOS. The Expo team is actively working with the react-native-screens team to improve this. See this GitHub Issue for updates and more details.

仅在路由的堆栈导航器中受支持

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

🌐 The zoom transition feature is only supported when using the router's built-in Stack navigator. If you attempt to use Link with zoom transition to a screen that is not part of a Stack navigator, the zoom transition will not work as expected.

必须在链接中使用

Link.AppleZoom 必须作为带有 asChild 属性的 Link 组件的直接或间接子组件使用。在此上下文之外使用将导致错误。

仅限 iOS 18 及以上

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

🌐 The zoom transition feature requires iOS 18 or later. While the components will render on older versions, the zoom animation will not be applied.