当前位置: 首页 > news >正文

ReactNative【实战系列教程】我的小红书 4 -- 首页(含顶栏tab切换,横向滚动频道,频道编辑弹窗,瀑布流布局列表等)

最终效果

在这里插入图片描述

顶栏

在这里插入图片描述

modules/index/components/topBar.tsx

import icon_daily from "@/assets/images/icon_daily.png";
import MaterialIcons from "@expo/vector-icons/MaterialIcons";
import { useCallback, useState } from "react";
import { Image, StyleSheet, Text, TouchableOpacity, View } from "react-native";
export default function IndexTopBar() {const [tab, setTab] = useState("关注");const tabList = [{label: "关注",},{label: "发现",},{label: "成都",},];const handlePress = useCallback((item: string) => {setTab(item);}, []);return (<View style={styles.container}><TouchableOpacity style={styles.dailyButton}><Image style={styles.icon} source={icon_daily} /></TouchableOpacity><View style={styles.typeBox}>{tabList.map((item) => (<TouchableOpacityactiveOpacity={0.5}onPress={() => handlePress(item.label)}key={item.label}style={[styles.itemBox, tab === item.label && styles.activeItemBox]}><Textstyle={[styles.itemText,tab === item.label && styles.activeItemText,]}>{item.label}</Text></TouchableOpacity>))}</View><MaterialIcons name="search" size={24} color="black" /></View>);
}
const styles = StyleSheet.create({container: {width: "100%",flexDirection: "row",alignItems: "center",justifyContent: "space-between",paddingHorizontal: 12,paddingVertical: 6,borderBottomWidth: 1,borderBottomColor: "#f5f5f5",backgroundColor: "#fff",},icon: {width: 28,height: 28,},dailyButton: {justifyContent: "center",alignItems: "center",},typeBox: {flexDirection: "row",justifyContent: "space-between",alignItems: "center",},itemBox: {marginHorizontal: 12,paddingVertical: 4,},activeItemBox: {borderBottomWidth: 3,borderBottomColor: "#ff2442",},itemText: {fontSize: 16,color: "#999",},activeItemText: {fontSize: 17,color: "#333",},
});

首页导入

app/(tabs)/index.tsx

import TopBar from "@/modules/index/components/topBar";
<TopBar />

频道栏(含编辑弹窗)

在这里插入图片描述

编辑弹窗

在这里插入图片描述

modules/index/components/typeBar.tsx

import MaterialIcons from "@expo/vector-icons/MaterialIcons";
import { useCallback, useEffect, useRef, useState } from "react";
import {ScrollView,StyleSheet,Text,TouchableOpacity,View,
} from "react-native";
import TypeModal from "./typeModal";
type Props = {allCategoryList: Category[];onCategoryChange: (category: Category) => void;
};
// eslint-disable-next-line react/display-name
export default ({ allCategoryList, onCategoryChange }: Props) => {const modalRef = useRef<{show: () => void;hide: () => void;}>(null);const [category, setCategory] = useState<Category>();const [myTypeList, setMyTypeList] = useState<Category[]>([]);useEffect(() => {setMyTypeList(allCategoryList.filter((i) => i.isAdd));setCategory(myTypeList.find((i) => i.name === "推荐"));}, []);const onAllCategoryListChange = (categoryList: Category[]) => {setMyTypeList(categoryList.filter((i) => i.isAdd));};const handlePress = useCallback((item: Category) => {setCategory(item);onCategoryChange(item);}, []);return (<View style={styles.container}><ScrollViewhorizontalkeyboardDismissMode="on-drag"scrollEventThrottle={16}>{myTypeList.map((item, index) => (<TouchableOpacitykey={index}style={styles.itemBox}onPress={() => handlePress(item)}><Textstyle={[styles.itemText,category?.name === item.name && styles.activeItemText,]}>{item.name}</Text></TouchableOpacity>))}</ScrollView><TouchableOpacity onPress={() => modalRef.current?.show()}><MaterialIcons name="keyboard-arrow-down" size={24} color="black" /></TouchableOpacity><TypeModalref={modalRef}categoryList={allCategoryList}onCategoryListChange={onAllCategoryListChange}/></View>);
};
const styles = StyleSheet.create({container: {flexDirection: "row",paddingHorizontal: 12,paddingVertical: 6,backgroundColor: "#fff",},scrollBox: {marginHorizontal: 12,paddingVertical: 4,},itemBox: {paddingRight: 26,},itemText: {fontSize: 16,color: "#999",},activeItemText: {fontSize: 16,color: "#333",fontWeight: "bold",},
});

modules/index/components/typeModal.tsx

import { save } from "@/utils/Storage";
import AntDesign from "@expo/vector-icons/AntDesign";
import MaterialIcons from "@expo/vector-icons/MaterialIcons";
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
import {Dimensions,Modal,StyleSheet,Text,TouchableOpacity,View,
} from "react-native";
const { width: SCREEN_WIDTH } = Dimensions.get("window");
type Props = {categoryList: Category[];onCategoryListChange: (categoryList: Category[]) => void;
};
// eslint-disable-next-line react/display-name
export default forwardRef(({ categoryList, onCategoryListChange }: Props, ref) => {const [visible, setVisible] = useState(false);const [edit, setEdit] = useState<boolean>(false);const [myList, setMyList] = useState<Category[]>([]);const [otherList, setOtherList] = useState<Category[]>([]);useEffect(() => {if (!categoryList) {return;}const list1 = categoryList.filter((i) => i.isAdd);const list2 = categoryList.filter((i) => !i.isAdd);setMyList(list1);setOtherList(list2);}, [categoryList]);const hide = () => {setVisible(false);};const saveChanges = () => {const newCategoryList = [...myList, ...otherList];save("categoryList", JSON.stringify(newCategoryList));onCategoryListChange(newCategoryList);};const show = () => {setVisible(true);};const delItem = (item: Category) => {const newMyList = myList.filter((i) => i.name !== item.name);const item_copy = { ...item, isAdd: false };const newOtherList = [...otherList, item_copy];setMyList(newMyList);setOtherList(newOtherList);};const addItem = (item: Category) => {if (!edit) {return;}const newOtherList = otherList.filter((i) => i.name !== item.name);const item_copy = { ...item, isAdd: true };const newMyList = [...myList, item_copy];setMyList(newMyList);setOtherList(newOtherList);};// 暴露方法给父组件useImperativeHandle(ref, () => ({show,}));return (<ModalanimationType="fade"transparent={true}visible={visible}onRequestClose={hide}><View style={styles.winBox}><View style={styles.contentBox}><View style={styles.titleBox}><Text style={styles.titleTxt}>我的频道</Text><Text style={styles.subTitleTxt}>{edit ? "点击移除频道" : "点击进入频道"}</Text><TouchableOpacitystyle={styles.editButton}onPress={() => {if (edit) {saveChanges();}setEdit(!edit);}}><Text style={styles.editTxt}>{edit ? "完成编辑" : "进入编辑"}</Text></TouchableOpacity><TouchableOpacity onPress={() => hide()}><MaterialIconsname="keyboard-arrow-up"size={24}color="black"/></TouchableOpacity></View><View style={styles.listBox}>{myList.map((item: Category, index: number) => {return (<TouchableOpacitykey={`${item.name}`}style={item.default? styles.itemLayoutDefault: styles.itemLayout}onPress={() => {if (edit && !item.default) {delItem(item);}}}><Text style={styles.itemTxt}>{item.name}</Text>{edit && !item.default && (<AntDesignstyle={styles.delIcon}name="closecircle"size={14}color="#cecece"/>)}</TouchableOpacity>);})}</View><View style={styles.otherBox}><View style={styles.titleBox}><Text style={styles.titleTxt}>推荐频道</Text><Text style={styles.subTitleTxt}>点击添加频道</Text></View><View style={styles.listBox}>{otherList.map((item: Category, index: number) => {return (<TouchableOpacitykey={`${item.name}`}style={item.default? styles.itemLayoutDefault: styles.itemLayout}onPress={() => {addItem(item);}}><Text style={styles.itemTxt}> + {item.name}</Text></TouchableOpacity>);})}</View></View></View><View style={styles.bottomBox}></View></View></Modal>);}
);
const styles = StyleSheet.create({winBox: {flex: 1,alignItems: "center",backgroundColor: "transparent",},contentBox: {marginTop: 56,width: "100%",backgroundColor: "#fff",},titleBox: {flexDirection: "row",alignItems: "center",paddingHorizontal: 12,},titleTxt: {fontSize: 16,color: "#333",fontWeight: "bold",marginLeft: 6,},subTitleTxt: {fontSize: 13,color: "#999",marginLeft: 12,flex: 1,},bottomBox: {flex: 1,width: "100%",backgroundColor: "rgba(0,0,0,0.5)",},editButton: {paddingHorizontal: 10,height: 28,backgroundColor: "#EEE",borderRadius: 14,justifyContent: "center",alignItems: "center",marginRight: 6,},editTxt: {fontSize: 13,},listBox: {marginTop: 6,width: "100%",flexDirection: "row",flexWrap: "wrap",},itemLayout: {width: (SCREEN_WIDTH - 80) >> 2,height: 32,justifyContent: "center",alignItems: "center",borderWidth: 1,borderColor: "#f5f5f5",borderRadius: 6,marginLeft: 16,marginTop: 12,},itemLayoutDefault: {width: (SCREEN_WIDTH - 80) >> 2,height: 32,justifyContent: "center",alignItems: "center",backgroundColor: "#f5f5f5",borderRadius: 6,marginLeft: 16,marginTop: 12,},itemTxt: {fontSize: 14,color: "#666",},otherBox: {marginVertical: 30,},delIcon: {position: "absolute",right: -6,top: -6,},
});

首页导入

import TypeBar from "@/modules/index/components/typeBar";

作为列表的页眉渲染

        // 列表顶部renderHeader={() =>(!isLoading_type && (<TypeBarallCategoryList={store.categoryList}onCategoryChange={(category: Category) => {console.log(JSON.stringify(category));}}/>)) || <></>}

数据来自 store

import IndexStore from "@/modules/index/IndexStore";
const store = useLocalObservable(() => new IndexStore());

因数据是异步加载,需跟进其加载状态

const [isLoading_type, setIsLoading_type] = useState(true);

在页面初始渲染时异步加载数据

  useEffect(() => {const Loading_type = async () => {try {await store.getCategoryList();} catch (error) {console.error("Failed to fetch category list:", error);} finally {setIsLoading_type(false);}};Loading_type();store.requestHomeList();}, []);

modules/index/IndexStore.ts

暂用的 mock 数据,解开注释,可访问真实接口。

import articles from "@/mock/articles";
import { load } from "@/utils/Storage";
import { Toast } from "@ant-design/react-native";
import { action, observable } from "mobx";
const SIZE = 10;
export default class IndexStore {page: number = 1;@observable homeList: ArticleSimple[] = [];@observable refreshing: boolean = false;@observable categoryList: Category[] = [];@actionresetPage = () => {this.page = 1;};requestHomeList = async () => {if (this.refreshing) {return;}const loading = Toast.loading("加载中...");try {this.refreshing = true;const params = {page: this.page,size: SIZE,};// const { data } = await request("homeList", params);let data = articles.map((item) => ({...item,image: item.images[0],}));if (data?.length) {if (this.page === 1) {this.homeList = data;} else {// this.homeList = [...this.homeList, ...data];}this.page = this.page + 1;} else {if (this.page === 1) {this.homeList = [];} else {// 已经加载完了,没有更多数据}}} catch (error) {console.log(error);} finally {this.refreshing = false;Toast.remove(loading);}};getCategoryList = async () => {const cacheListStr = await load("categoryList");if (cacheListStr) {const cacheList = JSON.parse(cacheListStr);if (cacheList?.length) {this.categoryList = cacheList;} else {this.categoryList = DEFAULT_CATEGORY_LIST;}} else {this.categoryList = DEFAULT_CATEGORY_LIST;}};
}
const DEFAULT_CATEGORY_LIST: Category[] = [// 默认添加频道{ name: "推荐", default: true, isAdd: true },{ name: "视频", default: true, isAdd: true },{ name: "直播", default: true, isAdd: true },{ name: "摄影", default: false, isAdd: true },{ name: "穿搭", default: false, isAdd: true },{ name: "读书", default: false, isAdd: true },{ name: "影视", default: false, isAdd: true },{ name: "科技", default: false, isAdd: true },{ name: "健身", default: false, isAdd: true },{ name: "科普", default: false, isAdd: true },{ name: "美食", default: false, isAdd: true },{ name: "情感", default: false, isAdd: true },{ name: "舞蹈", default: false, isAdd: true },{ name: "学习", default: false, isAdd: true },{ name: "男士", default: false, isAdd: true },{ name: "搞笑", default: false, isAdd: true },{ name: "汽车", default: false, isAdd: true },{ name: "职场", default: false, isAdd: true },{ name: "运动", default: false, isAdd: true },{ name: "旅行", default: false, isAdd: true },{ name: "音乐", default: false, isAdd: true },{ name: "护肤", default: false, isAdd: true },{ name: "动漫", default: false, isAdd: true },{ name: "游戏", default: false, isAdd: true },// 默认添加频道{ name: "家装", default: false, isAdd: false },{ name: "心理", default: false, isAdd: false },{ name: "户外", default: false, isAdd: false },{ name: "手工", default: false, isAdd: false },{ name: "减脂", default: false, isAdd: false },{ name: "校园", default: false, isAdd: false },{ name: "社科", default: false, isAdd: false },{ name: "露营", default: false, isAdd: false },{ name: "文化", default: false, isAdd: false },{ name: "机车", default: false, isAdd: false },{ name: "艺术", default: false, isAdd: false },{ name: "婚姻", default: false, isAdd: false },{ name: "家居", default: false, isAdd: false },{ name: "母婴", default: false, isAdd: false },{ name: "绘画", default: false, isAdd: false },{ name: "壁纸", default: false, isAdd: false },{ name: "头像", default: false, isAdd: false },
];

瀑布流布局列表

https://blog.csdn.net/weixin_41192489/article/details/149202367

首页最终代码

app/(tabs)/index.tsx

import Heart from "@/components/Heart";
import ResizeImage from "@/components/ResizeImage";
import articleList from "@/mock/articleList";
import TopBar from "@/modules/index/components/topBar";
import TypeBar from "@/modules/index/components/typeBar";
import IndexStore from "@/modules/index/IndexStore";
import { useRouter } from "expo-router";
import { observer, useLocalObservable } from "mobx-react-lite";
import { useCallback, useEffect, useState } from "react";
import {Dimensions,Image,StyleSheet,Text,TouchableOpacity,View,
} from "react-native";
import WaterfallFlow from "../../components/WaterfallFlow";
const { width: SCREEN_WIDTH } = Dimensions.get("window");
export default observer(function IndexScreen() {const router = useRouter();const store = useLocalObservable(() => new IndexStore());const [isLoading_type, setIsLoading_type] = useState(true);useEffect(() => {const Loading_type = async () => {try {await store.getCategoryList();} catch (error) {console.error("Failed to fetch category list:", error);} finally {setIsLoading_type(false);}};Loading_type();store.requestHomeList();}, []);const onArticlePress = useCallback((article: ArticleSimple) => () => {router.push(`/articleDetail?id=${article.id}`);},[]);const renderItem = (item: ArticleSimple) => {return (<TouchableOpacity style={styles.item} onPress={onArticlePress(item)}><ResizeImage uri={item.image} /><Text style={styles.titleTxt}>{item.title}</Text><View style={[styles.nameLayout]}><Image style={styles.avatarImg} source={{ uri: item.avatarUrl }} /><Text style={styles.nameTxt}>{item.userName}</Text><Heartvalue={item.isFavorite}onValueChanged={(value: boolean) => {console.log(value);}}/><Text style={styles.countTxt}>{item.favoriteCount}</Text></View></TouchableOpacity>);};const loadMoreData = () => {store.requestHomeList();};const refreshNewData = () => {store.resetPage();store.requestHomeList();};const Footer = () => {return <Text style={styles.footerTxt}>---- 没有更多数据了 ---- </Text>;};return (<View style={styles.page}><TopBar /><WaterfallFlowdata={articleList}// 列数numColumns={2}// 列间距columnGap={8}// 行间距rowGap={4}// 触顶下拉刷新onRefresh={refreshNewData}// 触底加载更多数据onLoadMore={loadMoreData}// 是否在刷新refreshing={store.refreshing}// 列表顶部renderHeader={() =>(!isLoading_type && (<TypeBarallCategoryList={store.categoryList}onCategoryChange={(category: Category) => {console.log(JSON.stringify(category));}}/>)) || <></>}// 列表项renderItem={renderItem}// 列表底部renderFooter={Footer}/></View>);
});
const styles = StyleSheet.create({page: {paddingBottom: 50,},item: {width: (SCREEN_WIDTH - 18) >> 1,backgroundColor: "white",marginLeft: 6,marginBottom: 6,borderRadius: 8,overflow: "hidden",},countTxt: {fontSize: 14,color: "#999",marginLeft: 4,},titleTxt: {fontSize: 14,color: "#333",marginHorizontal: 10,marginVertical: 4,},nameLayout: {width: "100%",flexDirection: "row",alignItems: "center",paddingHorizontal: 10,marginBottom: 10,},avatarImg: {width: 20,height: 20,resizeMode: "cover",borderRadius: 10,},nameTxt: {fontSize: 12,color: "#999",marginLeft: 6,flex: 1,},footerTxt: {width: "100%",fontSize: 14,color: "#999",marginVertical: 16,textAlign: "center",textAlignVertical: "center",},
});
http://www.lryc.cn/news/582504.html

相关文章:

  • 闲庭信步使用图像验证平台加速FPGA的开发:第五课——HSV转RGB的FPGA实现
  • Java连接Emqx实现订阅发布消息
  • 恒创科技:香港站群服务器做seo站群优化效果如何
  • ReactNative【实战】瀑布流布局列表(含图片自适应、点亮红心动画)
  • Rust DevOps框架管理实例
  • ffmpeg下编译tsan
  • iOS 性能测试工具全流程:主流工具实战对比与适用场景
  • cocos2dx3.x项目升级到xcode15以上的iconv与duplicate symbols报错问题
  • CSP-S模拟赛二总结(实际难度大于CSP-S)
  • 力扣 239 题:滑动窗口最大值的两种高效解法
  • Android kotlin 协程的详细使用指南
  • C++--AVL树
  • 微前端框架对比
  • (16)Java+Playwright自动化测试-iframe操作-监听事件和执行js脚本
  • 精益管理与数字化转型的融合:中小制造企业降本增效的双重引擎
  • Nexus zkVM 3.0 及未来:迈向模块化、分布式的零知识证明
  • 生成PDF文件(基于 iText PDF )
  • Android framework修改解决偶发开机时有两个launcher入口的情况
  • Prompt Injection Attack to Tool Selection in LLM Agents
  • 论文略读:Prefix-Tuning: Optimizing Continuous Prompts for Generation
  • C++11标准库算法:深入理解std::find, std::find_if与std::find_if_not
  • Python中os.path和pathlib模块路径操作函数汇总
  • react的条件渲染【简约风5min】
  • C#使用Semantic Kernel实现Embedding功能
  • 【知足常乐ai笔记】机器人强化学习
  • TVS管工作原理是什么?主要的应用场景都有哪些?
  • MySQL数据库访问(C/C++)
  • 赛博威破解快消品渠道营销三重困局,助力企业实现“活动即战力”
  • 小米YU7预售现象深度解析:智能电动汽车的下一个范式革命
  • 内容页模板表格显示不全的问题处理