React Native实现FlatList项点击弹出带背景的详情Modal
实现方案:将优惠券详情改为弹窗展示
需求说明
当前在HomeScreen.js中通过FlatList渲染Cupons组件,点击列表项时会通过navigation跳转到CuponDetails页面。需要修改为点击列表项后弹出弹窗展示详情,弹窗背景保留HomeScreen原界面。
修改步骤
1. 修改HomeScreen.js
在父组件中统一管理弹窗状态和选中的优惠券数据,添加Modal组件承载详情内容:
import { StatusBar } from 'expo-status-bar'; import { Button, Image, Modal, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; import { useNavigation } from '@react-navigation/native'; import {FontAwesome} from "@expo/vector-icons" import { TextInput } from 'react-native-gesture-handler'; import TopHeading from './home_components/TopHeading'; import TopSlider from './home_components/TopSlider'; import { FlatList } from 'react-native'; import Cupons from './home_components/Cupons'; import { useState, useEffect } from 'react'; import CuponDetails from './CuponDetails'; export default function HomeScreen () { const [dataa= [], setData] = useState([]); const [isModalVisible, setIsModalVisible] = useState(false); const [selectedCoupon, setSelectedCoupon] = useState(null); useEffect(() => { fetch('https://newsapi.org/v2/everything?q=Pakistan&from=2022-06-26&sortBy=publishedAt&apiKey=******************') .then(response => response.json()) .then(data => { setData(data.articles || []); }) .catch(err => console.error('请求失败:', err)); }, []); const common = [ {name: "Test-1", url: require("../assets/image.jpg")}, {name: "Test-2", url: require("../assets/image.jpg")}, {name: "Test-3", url: require("../assets/image.jpg")}, {name: "Test-4", url: require("../assets/image.jpg")}, {name: "Test-5", url: require("../assets/image.jpg")}, ]; const cupon= [ {url: require("../assets/image2.jpg"), head: "Dabdob", desc: "Offer: 10%"}, {url: require("../assets/image2.jpg"), head: "Noon", desc: "Offer: cashback 10%"}, {url: require("../assets/image2.jpg"), head: "Deraah", desc: "Offer: Free Shipping"}, {url: require("../assets/image2.jpg"), head: "Al-Qassim Health Water", desc: "Offer: 11%"}, {url: require("../assets/image2.jpg"), head: "Danube", desc: "( SR 10 (3 times use"}, {url: require("../assets/image2.jpg"), head: "Styli Shop", desc: "Offer: 10%"}, {url: require("../assets/image2.jpg"), head: "Naseem", desc: "Offer: 15%-10%"}, {url: require("../assets/image2.jpg"), head: "Shgardi", desc: "Offer: 25%"}, {url: require("../assets/image2.jpg"), head: "Click Toys", desc: "Offer: 10%"}, {url: require("../assets/image2.jpg"), head: "Nanaa", desc: "Offer: 50% cash back"}, ]; const navigation = useNavigation(); const openModal = (couponData) => { setSelectedCoupon(couponData); setIsModalVisible(true); }; const closeModal = () => { setIsModalVisible(false); setSelectedCoupon(null); }; return ( <View style={styles.container}> {/* 原头部内容 */} <View style={styles.header}> <View style={styles.innerContainer}> <Image style={styles.logo} source={require("../assets/mainLogo.png")} /> <View style={styles.searchbarContainer}> <FontAwesome color="orange" style={{marginTop:12, paddingLeft:5}} size={15} name='search' /> <TextInput style={styles.searchBar} placeholder='Search..'/> </View> </View> </View> {/* 原轮播内容 */} <View style={{alignSelf: "stretch", borderColor: "red", height:160, justifyContent:"space-between", alignItems: "center",marginHorizontal:20 }}> <TopHeading /> <FlatList showsHorizontalScrollIndicator={false} bounces data={common} renderItem = {({item, index})=>{ return <TopSlider heading= {item.name} img={item.url} index={index} /> }} horizontal /> </View> {/* 优惠券列表 */} <View style={{flex: 1, marginBottom: 95,marginHorizontal: 0}}> <FlatList showsVerticalScrollIndicator={false} data={dataa} renderItem = {({item})=>{ return <Cupons imgUrl={item.urlToImage} name={item.author} title={item.title} desc={item.description} content={item.content} onCouponPress={() => openModal(item)} /> }} /> </View> {/* 详情弹窗 */} <Modal visible={isModalVisible} animationType="slide" transparent={true} onRequestClose={closeModal} > <View style={styles.modalOverlay}> <View style={styles.modalContent}> <TouchableOpacity style={styles.closeButton} onPress={closeModal}> <FontAwesome name="times" size={20} color="#333" /> </TouchableOpacity> {selectedCoupon && ( <CuponDetails ImgUrl={selectedCoupon.urlToImage} Name={selectedCoupon.author} Desc={selectedCoupon.description} Title={selectedCoupon.title} Content={selectedCoupon.content} /> )} </View> </View> </Modal> <StatusBar style={'auto'} /> </View> ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: "white", alignItems: "center" }, header:{ borderWidth: 1, height: 130, backgroundColor: "rgb(32, 68, 124)", alignSelf: "stretch", alignItems: 'center', }, logo:{ width: 40, height: 40, tintColor: "white", }, innerContainer:{ flexDirection: "row", marginTop: 70, alignSelf: "stretch", justifyContent: "space-between", marginHorizontal: 20, alignItems: "center", height: 50, }, searchbarContainer:{ flexDirection: "row", backgroundColor: "white", borderRadius: 20, width: 300, height: 40, }, searchBar:{ paddingLeft: 5, width: 280, }, modalOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.5)', justifyContent: 'center', alignItems: 'center', }, modalContent: { width: '90%', maxHeight: '90%', backgroundColor: 'white', borderRadius: 15, overflow: 'hidden', position: 'relative', }, closeButton: { position: 'absolute', top: 15, right: 15, zIndex: 10, backgroundColor: '#f5f5f5', borderRadius: 15, padding: 5, } });
2. 修改Cupons.js
移除导航跳转逻辑,改为调用父组件传递的点击回调:
import { Image, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; export default function Cupons({imgUrl, name, desc, title, content, onCouponPress}) { return ( <TouchableOpacity onPress={onCouponPress}> <View style= {styles.container}> <View style={styles.topContainer}> <View style= {styles.imageContainer}> <Image resizeMode='cover' style={styles.image} source={{uri: `${imgUrl}`}}/> </View> <View style={styles.textContainer}> <Text style={{fontWeight: "bold", fontSize: 16}} >{name}</Text> <Text style={{fontSize:16, color: "grey", overflow: "hidden", padding:5}}>{title}</Text> </View> </View> <Text style={{alignSelf: "center", fontSize:20, fontWeight: "bold"}}>Description</Text> <Text style={{marginHorizontal:10, marginVertical:10, height: "auto", overflow: "scroll", fontSize: 14, padding:5, color: "grey", alignItems:"center", justifyContent: "center" }}> {desc} </Text> </View> </TouchableOpacity> ); } const styles = StyleSheet.create({ container: { backgroundColor: "rgb(250,250,250)", height: "auto", margin: 10, width: 350, borderRadius: 10, elevation: 5, shadowColor: "rgb(100,100,255)", position: "relative", overflow: "hidden", justifyContent: "center", }, topContainer:{ margin: 10, alignItems: "center", width: "auto", height: "auto", justifyContent: "center", }, imageContainer:{ borderRadius: 10, height: 60, width:80, elevation: 10, shadowColor: "rgb(100,100,255)", marginBottom: 10, }, image:{ height:70, width: 80, borderRadius: 10, marginRight: 20, marginBottom: 10, }, textContainer:{ margin: 5, height:"auto", width:300, justifyContent: "space-between", alignItems: 'center' } });
3. 修改CuponDetails.js
移除路由依赖,直接接收父组件传递的详情数据:
import { Image, ScrollView, StyleSheet, Text, View } from 'react-native'; export default function CuponDetails({ImgUrl, Name, Desc, Title, Content}) { return ( <ScrollView style={{backgroundColor: "white", flex:1}}> <View style= {styles.container}> <View style = {styles.imageContainer}> <Image style={{width:250, height: 200,}} resizeMode='cover' source={{uri: `${ImgUrl}`}} /> </View> <View style={styles.headingContainer}> <Text style={{fontWeight: "bold", fontSize: 18}}>{Name}</Text> <Text style={{fontWeight: "normal", fontSize: 14,}}>{Title}</Text> </View> <View style={styles.headingContainer}> <Text style={{fontWeight: "bold", fontSize: 18}}>Description</Text> <Text style={{fontWeight: "normal", fontSize: 14,}}>{Desc}</Text> </View> <View style={styles.headingContainer}> <Text style={{fontWeight: "bold", fontSize: 18}}>Content</Text> <Text style={{fontWeight: "normal", fontSize: 14,}}>{Content}</Text> </View> </View> </ScrollView> ); } const styles = StyleSheet.create({ container: { alignItems: "center", backgroundColor: "white", paddingTop: 50, paddingBottom: 20, }, imageContainer:{ height: 200, width: 250, marginBottom: 10, borderRadius: 10, overflow: "hidden", elevation: 2, shadowColor: "rgb(100,100,255)", }, headingContainer:{ width: "90%", marginVertical: 10, height: "auto", alignItems: "center", borderRadius: 15, padding: 8, elevation: 2, shadowColor: "rgb(100,100,255)", backgroundColor: "white" } })
核心说明
- 统一状态管理:弹窗显示状态和选中数据放在
HomeScreen中,避免子组件重复维护状态。 - 透明弹窗效果:通过
transparent={true}和半透明遮罩层,实现背景保留原界面的视觉效果。 - 组件解耦:
Cupons只负责渲染列表项,点击逻辑交给父组件;CuponDetails只负责渲染详情,不再依赖路由参数。
内容的提问来源于stack exchange,提问作者Ozelea
相关产品推荐
相关产品推荐

