在鸿蒙环境下做 React Native 应用,图片预览是最常见的高频场景之一。真正麻烦的是手势缩放、多图切换和下滑关闭这些交互,如果组件依赖原生代码,就得针对鸿蒙重新适配。react-native-image-zoom-viewer 是一个纯 JS 实现的图片缩放浏览组件,不依赖任何原生链接,在鸿蒙上可以直接运行。它内部依赖的 react-native-image-pan-zoom 也是纯 JS 手势计算库,所以整个方案可以做到开箱即用。
安装很简单,直接使用 yarn 添加依赖即可,3.0.1 版本就是纯 JS 库,不需要配置原生链接。
- yarn add react-native-image-zoom-viewer
复制代码
基础用法是把 ImageViewer 放在 Modal 中,通过 imageUrls 传入图片数组,然后设置 enableSwipeDown 和 onSwipeDown 来实现下滑关闭。onClick 也可以用来在轻点时退出预览。下面是一个最简单的实现。
- import React, { useState } from 'react';
- import { Modal, View, Text, TouchableOpacity } from 'react-native';
- import ImageViewer from 'react-native-image-zoom-viewer';
- const images = [
- { url: 'https://picsum.photos/id/1/800/600', width: 800, height: 600 },
- { url: 'https://picsum.photos/id/10/1200/800', width: 1200, height: 800 },
- ];
- export default function Gallery() {
- const [visible, setVisible] = useState(false);
- return (
- <Modal visible={visible} transparent onRequestClose={() => setVisible(false)}>
- <ImageViewer
- imageUrls={images}
- index={0}
- onClick={() => setVisible(false)}
- enableSwipeDown
- onSwipeDown={() => setVisible(false)}
- />
- </Modal>
- );
- }
复制代码
实际项目里通常不会只用默认样式,还需要自定义头部信息、加载状态,以及监听当前预览到第几张。ImageViewer 提供了 renderHeader 和 loadingRender 这类接口,配合 onChange 可以拿到当前索引。下面这个完整示例展示了如何实现一个带关闭按钮、页码显示和加载动画的图片浏览器。
- import React, { useState } from 'react';
- import { Modal, View, Text, TouchableOpacity, ActivityIndicator } from 'react-native';
- import ImageViewer from 'react-native-image-zoom-viewer';
- const images = [
- { url: 'https://picsum.photos/id/1/800/600' },
- { url: 'https://picsum.photos/id/10/1200/800' },
- { url: 'https://picsum.photos/id/100/600/900' },
- ];
- export default function ImageViewerDemo() {
- const [visible, setVisible] = useState(false);
- const [currentIndex, setCurrentIndex] = useState(0);
- return (
- <Modal visible={visible} transparent animationType="fade">
- <ImageViewer
- imageUrls={images}
- index={currentIndex}
- enableSwipeDown
- enablePreload
- saveToLocalByLongPress
- backgroundColor="#000"
- onClick={() => setVisible(false)}
- onSwipeDown={() => setVisible(false)}
- onChange={(index) => console.log('切换到第', index! + 1, '张')}
- renderHeader={(currentIndex) => (
- <View style={{
- position: 'absolute', top: 0, left: 0, right: 0,
- paddingTop: 50, paddingHorizontal: 16, flexDirection: 'row',
- justifyContent: 'space-between', zIndex: 100,
- }}>
- <TouchableOpacity onPress={() => setVisible(false)}
- style={{ backgroundColor: 'rgba(0,0,0,0.5)', borderRadius: 20, padding: 8 }}>
- <Text style={{ color: '#fff' }}>✕ 关闭</Text>
- </TouchableOpacity>
- <Text style={{ color: '#fff' }}>
- {(currentIndex || 0) + 1} / {images.length}
- </Text>
- </View>
- )}
- loadingRender={() => (
- <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
- <ActivityIndicator size="large" color="#fff" />
- </View>
- )}
- />
- </Modal>
- );
- }
复制代码
接入过程中有几个容易踩的坑,这里整理一下。
第一个是图片数组格式。imageUrls 不接受 string[],必须使用对象数组。很多新手直接传入 url 字符串列表,结果图片不显示。
- // 错误:imageUrls 不接受 string[]
- const images = ['url1', 'url2'];
- // 正确:必须用对象数组
- const images = [
- { url: 'https://example.com/image.jpg' },
- { url: '', props: { source: require('./local.png') } },
- ];
复制代码
第二个是本地图片的传入方式。如果要展示本地图片,需要通过 props.source 来传,而不能直接塞一个 path 进去。url 可以留空,props.source 里用 require 加载资源。
- const images = [{
- url: '',
- props: {
- source: require('./assets/photo.png'),
- },
- }];
复制代码
第三个是 index 变化不触发重渲染。ImageViewer 内部使用 getDerivedStateFromProps 检测 index prop 的变化,但在某些场景下父组件直接修改 index 可能不会让视图跳转到对应图片。一个可靠的解决方法是给 ImageViewer 加 key={index},强制重建组件。
- // 可能不生效
- export default function Gallery() {
- const [index, setIndex] = useState(0);
- return <ImageViewer imageUrls={images} index={index} />;
- }
- // 使用 key 强制重建
- const [index, setIndex] = useState(0);
- <ImageViewer key={index} imageUrls={images} index={index} />
复制代码
第四个是 onClick 和 onSwipeDown 可能冲突。同时开启 enableSwipeDown 并设置 onClick 后,轻点屏幕时可能两个回调都会被触发。尽量避免同时使用,或者在 onClick 里加防抖或手势判断。
第五个是长按保存菜单。saveToLocalByLongPress 在鸿蒙上长按图片时会弹出一个内置的 JS 菜单,包含“保存图片”和“取消”。这个菜单本身可以正常工作,但保存动作依赖 CameraRoll 模块,如果工程里没有安装这个模块,保存会静默失败,不会报错也不会弹提示。
第六个是图片宽高信息。imageUrls 里的对象可以带 width 和 height,建议尽量提供。提供后组件可以立即按尺寸渲染,不用等图片加载完再自动测量;不提供的话组件会自己去拉取图片尺寸,首次渲染会有明显延迟。
- const images = [
- { url: '...' }, // 自动获取尺寸,稍慢
- { url: '...', width: 800, height: 600 }, // 指定尺寸,立即渲染
- ];
复制代码
性能方面,可以交给几个配置项来处理。开启 enablePreload 后,库会预加载当前图片前后各一张,减少切换时的等待。对于图片数量较多的场景,建议只在 Modal 打开时才渲染 ImageViewer,关闭时直接销毁,避免在后台做无谓的视图计算。网络图片也应尽量先压缩到合适尺寸,最大宽度控制在 2048px 左右,既能保证清晰度,又不会让内存和带宽压力太大。
整体来看,react-native-image-zoom-viewer 是目前鸿蒙 RN 项目里比较值得推荐的图片预览方案。它不依赖原生代码,功能覆盖了双指缩放、双击放大、多图切换、下滑关闭和长按保存等核心交互,接入成本低。真正需要留神的只有数据格式、状态同步和手势冲突这几个点,处理好了就能稳定用起来。 |