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

React Native:如何动态设置Image组件及切换Video的Source?

解决动态切换Video组件source及Image组件动态设置的问题

核心问题分析

你的代码中存在几个关键问题导致动态切换视频源失效:

  • 使用普通变量 getArrowVideoPath 存储视频路径,而非React状态变量,因此变量更新时组件不会触发重渲染
  • saveTheme 函数中调用 setThemeFunction(themeColor) 时,themeColor 的状态更新是异步的,此时获取的仍是旧值
  • useEffect 未监听 themeColor 变化,导致主题切换后无法重新加载对应资源

修正方案及代码示例

以下是修复后的完整代码,同时包含动态图片的实现方式:

import React, { useState, useRef, useEffect, useCallback } from 'react';
import { View, Text, TouchableOpacity, StyleSheet, Image, Modal, Platform } from 'react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { RadioButton } from 'react-native-paper';
import Ionicons from '@expo/vector-icons/Ionicons';
import { Video, ResizeMode } from 'expo-av';

export default function StopwatchScreen() {
    const [menuVisible, setMenuVisible] = useState(false);
    const videoArrows = useRef(null);
    const [themeColor, setThemeColor] = useState('blue'); // 默认主题设为blue

    // 根据主题获取视频源
    const getVideoSource = () => {
        switch(themeColor) {
            case 'red':
                return require('../assets/images/red_arrows.mp4');
            case 'blue':
            default:
                return require('../assets/images/blue_arrows.mp4');
        }
    };

    // 根据主题获取图片源(示例)
    const getImageSource = () => {
        switch(themeColor) {
            case 'red':
                return require('../assets/images/red_some_image.png');
            case 'blue':
            default:
                return require('../assets/images/blue_some_image.png');
        }
    };

    const handleLapButtonPress = useCallback(() => {
        console.log("lap clicked");
    }, []);

    const handleStartButtonPress = () => {
        console.log("clicked 2");
    };

    const toggleMenu = () => {
        setMenuVisible(!menuVisible);
    };

    const retrieveTheme = async () => {
        try {
            const jsonValue = await AsyncStorage.getItem('theme');
            if (jsonValue !== null) {
                const parsedValue = JSON.parse(jsonValue);
                setThemeColor(parsedValue);
            } else {
                await AsyncStorage.setItem('theme', JSON.stringify('blue'));
            }
        } catch (e) {
            console.log('error: ', e);
        }
    };

    const saveTheme = async (value) => {
        await AsyncStorage.setItem('theme', JSON.stringify(value));
        setThemeColor(value);
        // 主题切换后重新加载视频
        if(videoArrows.current) {
            videoArrows.current.reloadAsync();
        }
    };

    useEffect(() => {
        retrieveTheme();
    }, []);

    // 主题变化时确保视频重新加载
    useEffect(() => {
        if(videoArrows.current) {
            videoArrows.current.reloadAsync();
        }
    }, [themeColor]);

    return (
        <View style={[styles.container, Platform.OS === 'android' && styles.androidPadding]}>
            <View style={styles.header}>
                <Text style={[styles.title, styles.textFont]}>Futuristic Stopwatch</Text>
                <TouchableOpacity onPress={toggleMenu}>
                    <Image source={require('../assets/images/three_dots.png')} style={styles.dotsIcon} />
                </TouchableOpacity>
            </View>

            <Modal
                transparent={true}
                animationType="slide"
                visible={menuVisible}
                onRequestClose={() => setMenuVisible(false)}
            >
                <View style={styles.modalContainer}>
                    <View style={styles.modalContent}>
                        <Ionicons name="close-circle" size={32} color="#11167F" onPress={() => setMenuVisible(false)} style={styles.closeButton} />
                        <View style={styles.sectionTitle}>
                            <Text style={[styles.sectionTitleText, styles.textFont]}>Theme</Text>
                        </View>
                        <View style={styles.radioBox}>
                            <View style={styles.radioOptions}>
                                <RadioButton
                                    value="blue"
                                    status={ themeColor === 'blue' ? 'checked' : 'unchecked' }
                                    onPress={() => saveTheme('blue')}
                                    color="blue"
                                />
                                 <Text style={[styles.textFont]}>Blue</Text>
                            </View>
                            <View style={styles.radioOptions}>
                                <RadioButton
                                    value="red"
                                    status={ themeColor === 'red' ? 'checked' : 'unchecked' }
                                    onPress={() => saveTheme('red')}
                                    color="#DE382A"
                                />
                                <Text style={[styles.textFont]}>Red</Text>
                            </View>
                        </View>
                    </View>
                </View>
            </Modal>

            <View style={styles.contentContainer}>
                <View style={styles.rowArrow}>
                    <Video
                        ref={videoArrows}
                        style={{width: '100%', height: '100%'}}
                        source={getVideoSource()}
                        useNativeControls={false}
                        resizeMode={ResizeMode.CONTAIN}
                        onError={(error) => console.error("Video playback error:", error)}
                        isLooping={true}
                        shouldPlay={true}
                    />
                </View>
                {/* 动态图片示例 */}
                <View style={styles.imageContainer}>
                    <Image source={getImageSource()} style={styles.image} />
                </View>
                <View style={styles.controls}>
                    <TouchableOpacity onPress={handleStartButtonPress} style={[styles.controlButtonBorder, { backgroundColor: '#0a2a12' }]}>
                        <View style={styles.controlButton}>
                            <Text style={{ color: "#37d05c" }}>Start</Text>
                        </View>
                    </TouchableOpacity>
                    <TouchableOpacity onPress={handleLapButtonPress} style={[styles.controlButtonBorder, { backgroundColor: '#1c1c1e' }]}>
                        <View style={styles.controlButton}>
                            <Text style={{ color: "#9d9ca2" }}>Reset</Text>
                        </View>
                    </TouchableOpacity>
                </View>
            </View>
        </View>
    );
}

const styles = StyleSheet.create({
    container: {
      flex: 1,
      backgroundColor: '#fff',
      padding: 0,
    },
    androidPadding: {
        paddingTop: Platform.OS === 'android' ? 25 : 0,
    },
    header: {
        flexDirection: 'row',
        justifyContent: 'space-between',
        alignItems: 'center',
        marginBottom: 0,
        paddingTop: 10,
        paddingBottom: 10,
        paddingLeft: 10,
        paddingRight: 10,
        backgroundColor: '#0a1055',
    },
    title: {
        fontSize: 18,
        color: 'white',
    },
    dotsIcon: {
        width: 20,
        height: 20,
        resizeMode: 'contain',
    },
    modalContainer: {
        flex: 1,
        justifyContent: 'center',
        alignItems: 'center',
        backgroundColor: 'rgba(0, 0, 0, 0.5)',
    },
    textFont: {
        fontFamily: 'Orbitron Black',
    },
    modalContent: {
        backgroundColor: '#fff',
        padding: 20,
        borderRadius: 10,
        elevation: 5,
    },
    closeButton: {
        position: 'absolute',
        top: 10,
        right: 10,
    },
    sectionTitle: {
        flexDirection: 'row',
        alignItems: 'center',
        marginTop: 10,
    },
    sectionTitleText: {
        fontFamily: 'Orbitron Black',
    },
    radioBox: {
        marginTop: 20,
        alignItems: 'flex-start',
        width: 300,
    },
    radioOptions: {
        flexDirection: 'row',
        alignItems: 'center',
        marginBottom: 10,
    },
    contentContainer: {
        flex: 1,
        backgroundColor: '#0E1B84',
    },
    rowArrow: {
        flex: 0.1,
        flexDirection: 'row',
        marginBottom: 0,
    },
    imageContainer: {
        flex: 0.2,
        marginVertical: 10,
        paddingHorizontal: 10,
    },
    controls: {
        flexDirection: 'row',
        justifyContent: 'space-between',
        paddingLeft: 20,
        paddingRight: 20,
        marginVertical: 20,
    },
    controlButtonBorder: {
        justifyContent: "center",
        alignItems: "center",
        width: 70,
        height: 70,
        borderRadius: 70,
    },
    controlButton: {
        justifyContent: "center",
        alignItems: "center",
        width: 65,
        height: 65,
        borderRadius: 65,
        borderColor: "#000",
        borderWidth: 1
    },
    image: {
        width: '100%',
        height: '100%',
        resizeMode: 'contain'
    }
});

关键修复点说明

  • 状态驱动主题变化:将 themeColor 设为带初始值的状态变量,确保主题更新时触发组件重渲染
  • 动态资源直接推导:通过 getVideoSource 和 getImageSource 函数根据当前主题返回对应资源,避免中间变量导致的更新失效
  • 视频强制重载:主题切换后调用 reloadAsync() 方法,确保Video组件加载新的视频源
  • 异步状态修正:saveTheme 中直接使用传入的 value 操作,避免异步状态更新导致的旧值问题
  • 完善播放配置:为Video组件补充高度、设置自动播放,解决视频不显示的基础问题

动态Image组件实现要点

  • 与Video组件逻辑一致,通过函数根据主题返回 require() 包裹的资源路径
  • 注意:React Native本地资源必须静态引入,不能通过字符串拼接路径,需提前预加载所有主题对应的资源

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.03 03:22:03