You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React Native切换Image组件source uri时闪烁问题求助

React Native 图片切换闪烁问题解决方案

问题核心

调整Slider切换图片索引时,图片频繁闪烁,已尝试Image.prefetch及缓存相关配置,问题仍存在。

关键修复点

  • 移除key={index}:该属性会导致每次索引变化时Image组件被卸载重建,是闪烁的主要原因之一。
  • 强制缓存策略:为Image的source添加cache: 'force-cache',确保优先读取本地缓存。
  • 可靠预加载:改用Promise.all等待所有图片预加载完成,避免未加载完成就切换。
  • 双图叠加预加载:同时渲染当前和相邻索引的图片,通过zIndex控制显示,切换时无闪烁。

修改后的完整代码

import React, { useRef, useCallback, useState, useEffect } from 'react'
import { ActivityIndicator, Animated, View, Image, StyleSheet } from 'react-native'
import { GestureHandlerRootView, PanGestureHandler, PinchGestureHandler } from 'react-native-gesture-handler'
import { Svg, Path } from 'react-native-svg'
import { useAuthContext } from '../../../context/AuthContext'
import { CertRequesNoEnct, decryptAESWithJson, encryptJsonWithAES } from '../../../helpers/certRequest'
import Slider from '@react-native-community/slider'

export default function PACS({ serie }) {
    const [images, setImages] = useState([])
    const [index, setIndex] = useState(0)
    const { user } = useAuthContext()
    const [loading, setLoading] = useState(false)
    const [preloaded, setPreloaded] = useState(false)

    useEffect(() => {
        if (serie) {
            fetchInfo()
        }
    }, [serie])

    async function fetchInfo() {
        let dataToSend = {
            Serid: serie
        }
        setLoading(true)
        try {
            let a0 = await encryptJsonWithAES(dataToSend, user.s_aes, user.s_iv)
            let a1 = await CertRequesNoEnct(a0, user.token, "", "", "/mobile/imaging/fetch", user.certificate)
            if (a1) {
                let a2 = await decryptAESWithJson(a1.replace("\"", ""), user.s_aes, user.s_iv)
                if (a2) {
                    let a3 = await JSON.parse(a2)
                    const imageList = [...a3[0].array_agg]
                    setImages(imageList)
                    // 等待所有图片预加载完成
                    await Promise.all(imageList.map(imgUrl => Image.prefetch(imgUrl)))
                    setPreloaded(true)
                }
            }
        } catch (error) {
            console.error('预加载失败:', error)
        } finally {
            setLoading(false)
        }
    }

    const scale = useRef(new Animated.Value(1)).current
    const translateX = useRef(new Animated.Value(0)).current
    const translateY = useRef(new Animated.Value(0)).current

    const onHandlerStateChange = useCallback(() => {
        translateY.extractOffset();
        translateX.extractOffset();
    }, []);

    const handlePinch = Animated.event([{ nativeEvent: { scale: scale } }], { 
        useNativeDriver: false,
        listener: (event) => {
            // 限制最小缩放为1
            if (event.nativeEvent.scale < 1) {
                scale.setValue(1)
            }
        }
    })

    const handlePan = Animated.event([{
        nativeEvent: {
            translationX: translateX,
            translationY: translateY
        }
    }], { useNativeDriver: false })

    // 获取当前和相邻索引的图片,用于预加载显示
    const getDisplayImages = () => {
        if (!preloaded || images.length === 0) return []
        const currentImg = images[index]
        // 预加载上一张和下一张
        const prevImg = index > 0 ? images[index - 1] : null
        const nextImg = index < images.length - 1 ? images[index + 1] : null
        return [
            prevImg && <Animated.Image
                key="prev"
                resizeMethod="resize"
                resizeMode="cover"
                source={{ uri: prevImg, cache: 'force-cache' }}
                style={{ ...StyleSheet.absoluteFillObject, transform: [{ scale }, { translateX }, { translateY }], zIndex: 0 }}
            />,
            <Animated.Image
                key="current"
                resizeMethod="resize"
                resizeMode="cover"
                source={{ uri: currentImg, cache: 'force-cache' }}
                style={{ ...StyleSheet.absoluteFillObject, transform: [{ scale }, { translateX }, { translateY }], zIndex: 1 }}
            />,
            nextImg && <Animated.Image
                key="next"
                resizeMethod="resize"
                resizeMode="cover"
                source={{ uri: nextImg, cache: 'force-cache' }}
                style={{ ...StyleSheet.absoluteFillObject, transform: [{ scale }, { translateX }, { translateY }], zIndex: 0 }}
            />
        ].filter(Boolean)
    }

    return (
        loading ? <ActivityIndicator size="large" /> :
            <View style={{ flex: 1, position: "relative" }}>
                <View style={{ position: "absolute", zIndex: 99999, right: 0 }}>
                    <Slider
                        style={{ height: 100, zIndex: 9999, width: images.length, transform: [{ rotate: "-270deg" }] }}
                        minimumValue={0}
                        maximumValue={images.length - 1}
                        value={index}
                        onValueChange={value => setIndex(Math.floor(value))}
                        step={1}
                        minimumTrackTintColor="#00b1e6"
                        maximumTrackTintColor="#00b1e6"
                        tapToSeek={true}
                    />
                </View>

                <GestureHandlerRootView style={{ flex: 1 }}>
                    <PanGestureHandler onGestureEvent={handlePan} onHandlerStateChange={onHandlerStateChange}>
                        <Animated.View style={{ flex: 1 }}>
                            <PinchGestureHandler onGestureEvent={handlePinch}>
                                {images.length > 0 ?
                                    <View style={{ flex: 1, position: 'relative' }}>
                                        {getDisplayImages()}
                                    </View> :
                                    <Svg width="407" height="298" viewBox="0 0 407 298" fill="none" xmlns="http://www.w3.org/2000/svg">
                                        <Path d="M406.331 280.377C406.331 290.072 315.371 297.932 203.166 297.932C90.9611 297.932 6.10352e-05 290.072 6.10352e-05 280.377C6.10352e-05 270.681 90.9611 262.821 203.166 262.821C315.371 262.821 406.331 270.681 406.331 280.377Z" fill="#ECF0FD" />
                                        <Path d="M301.613 266.621C300.804 271.971 295.428 276.349 289.666 276.349H119.173C113.41 276.349 107.975 271.979 107.094 266.639L78.8657 95.4407C77.9857 90.1007 81.9797 85.7307 87.7417 85.7307H318.477C324.24 85.7307 328.293 90.1087 327.484 95.4597L301.613 266.621Z" fill="#C5C5C5" />
                                    </Svg>}
                            </PinchGestureHandler>
                        </Animated.View>
                    </PanGestureHandler>
                </GestureHandlerRootView>
            </View >
    )
}

额外说明

  • 移除key={index}避免组件重建,改用双图叠加方式切换显示,确保切换时无卸载过程。
  • Promise.all配合Image.prefetch确保所有图片加载完成后再允许切换,避免未缓存的图片导致闪烁。
  • 添加cache: 'force-cache'强制使用本地缓存,减少网络请求导致的加载延迟。
  • 修复了缩放逻辑的错误,通过listener正确限制最小缩放为1。

内容的提问来源于stack exchange,提问作者thelittlemaster

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.30 10:37:01