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
相关产品推荐
相关产品推荐

