最终效果
弹窗菜单
点击右上角群聊按钮后,弹窗菜单
无消息
代码实现
app/(tabs)/message.tsx
import icon_no_collection from "@/assets/icons/icon_no_collection.webp";
import FloatMenu, {FloatMenuRef,
} from "@/modules/message/components/FloatMenu";
import Entypo from "@expo/vector-icons/Entypo";
import MaterialCommunityIcons from "@expo/vector-icons/MaterialCommunityIcons";
import { useRef } from "react";
import {FlatList,GestureResponderEvent,Image,StyleSheet,Text,TouchableOpacity,View,
} from "react-native";
import icon_comments from "../../assets/icons/icon_comments.png";
import icon_new_follow from "../../assets/icons/icon_new_follow.png";
import icon_star from "../../assets/icons/icon_star.png";
import Empty from "../../components/Empty";
export default function MessageScreen() {const ref = useRef<FloatMenuRef>(null);const messageList = [{id: 1,name: "春游起飞小组",avatarUrl:"https://img0.baidu.com/it/u=1884273668,3023246561&fm=253&app=138&f=JPEG?w=760&h=760",lastMessage: "你们到了没啊,我都等好久了",lastMessageTime: "昨天",},{id: 2,name: "手游组团菜鸡组",avatarUrl:"https://img0.baidu.com/it/u=3671371982,1586403646&fm=253&app=138&f=JPEG?w=760&h=760",lastMessage: "连跪三把,赢一把睡觉",lastMessageTime: "前天",},{id: 3,name: "抄作业小分队",avatarUrl:"https://img2.baidu.com/it/u=1858586853,494763800&fm=253&app=138&f=JPEG?w=760&h=760",lastMessage: "数学物理还没写呢",lastMessageTime: "星期四",},];const unread = {unreadFavorate: 632,newFollow: 2,comment: 0,};const renderTitle = () => {return (<View style={styles.titleLayout}><Text style={styles.titleTxt}>消息</Text><TouchableOpacitystyle={styles.groupButton}onPress={(event: GestureResponderEvent) => {const { pageY } = event.nativeEvent;ref.current?.show(pageY + 20);}}><MaterialCommunityIconsname="account-supervisor-outline"size={24}color="black"/><Text style={styles.groupTxt}>群聊</Text></TouchableOpacity></View>);};const renderItem = ({item}: {item: MessageListItem;index: number;}) => {const styles = StyleSheet.create({item: {width: "100%",height: 80,flexDirection: "row",alignItems: "center",paddingHorizontal: 16,},avatarImg: {width: 48,height: 48,borderRadius: 24,resizeMode: "cover",},contentLayout: {flex: 1,marginHorizontal: 12,},nameTxt: {fontSize: 18,color: "#333",fontWeight: "bold",},lastMessageTxt: {fontSize: 15,color: "#999",marginTop: 4,},rightLayout: {alignItems: "flex-end",},timeTxt: {fontSize: 12,color: "#999",},iconTop: {marginTop: 6,},});return (<View style={styles.item}><Image style={styles.avatarImg} source={{ uri: item.avatarUrl }} /><View style={styles.contentLayout}><Text style={styles.nameTxt}>{item.name}</Text><Text style={styles.lastMessageTxt}>{item.lastMessage}</Text></View><View style={styles.rightLayout}><Text style={styles.timeTxt}>{item.lastMessageTime}</Text><Entypostyle={styles.iconTop}name="align-top"size={18}color="grey"/></View></View>);};const UnRead = ({ count }: { count: number }) => {const styles = StyleSheet.create({txt: {position: "absolute",top: -6,right: -10,backgroundColor: "#ff2442",paddingHorizontal: 8,height: 24,borderRadius: 12,textAlign: "center",textAlignVertical: "center",fontSize: 12,color: "white",},});return <Text style={styles.txt}>{count > 99 ? "99+" : count}</Text>;};const Header = () => {const styles = StyleSheet.create({headerLayout: {paddingHorizontal: 16,flexDirection: "row",paddingVertical: 20,},headerItem: {flex: 1,alignItems: "center",},itemImg: {width: 60,height: 60,resizeMode: "contain",},itemTxt: {fontSize: 16,color: "#333",marginTop: 8,},});return (<View style={styles.headerLayout}><View style={styles.headerItem}><View><Image style={styles.itemImg} source={icon_star} />{!!unread?.unreadFavorate && (<UnRead count={unread?.unreadFavorate} />)}</View><Text style={styles.itemTxt}>赞和收藏</Text></View><View style={styles.headerItem}><View><Image style={styles.itemImg} source={icon_new_follow} />{!!unread?.newFollow && <UnRead count={unread?.newFollow} />}</View><Text style={styles.itemTxt}>新增关注</Text></View><View style={styles.headerItem}><View><Image style={styles.itemImg} source={icon_comments} />{!!unread?.comment && <UnRead count={unread?.comment} />}</View><Text style={styles.itemTxt}>评论和@</Text></View></View>);};return (<View style={styles.page}>{renderTitle()}<FlatListstyle={{ flex: 1 }}data={messageList}extraData={[unread]}keyExtractor={(item) => `${item.id}`}renderItem={renderItem}ListHeaderComponent={<Header />}ListEmptyComponent={<Empty icon={icon_no_collection} tips="暂无消息" />}/><FloatMenu ref={ref} /></View>);
}
const styles = StyleSheet.create({page: {width: "100%",height: "100%",backgroundColor: "white",},groupTxt: {fontSize: 14,color: "#333",marginLeft: 6,},titleLayout: {width: "100%",height: 48,flexDirection: "row",alignItems: "center",justifyContent: "center",},titleTxt: {fontSize: 18,color: "#333",},groupButton: {height: "100%",flexDirection: "row",alignItems: "center",position: "absolute",right: 16,},
});
相关组件
弹窗菜单
modules/message/components/FloatMenu.tsx
import MaterialCommunityIcons from "@expo/vector-icons/MaterialCommunityIcons";
import React, { forwardRef, useImperativeHandle, useState } from "react";
import { Modal, StyleSheet, Text, TouchableOpacity, View } from "react-native";
export interface FloatMenuRef {show: (pageY: number) => void;hide: () => void;
}
// eslint-disable-next-line react/display-name
export default forwardRef((props: any, ref) => {const [visible, setVisible] = useState<boolean>(false);const [y, setY] = useState<number>(100);const show = (pageY: number) => {setY(pageY);setVisible(true);};const hide = () => {setVisible(false);};useImperativeHandle(ref, () => {return {show,hide,};});const renderMenus = () => {return (<View style={[styles.content, { top: y }]}><TouchableOpacity style={styles.menuItem}><MaterialCommunityIconsname="account-group-outline"size={24}color="black"/><Text style={styles.menuTxt}>群聊广场</Text></TouchableOpacity><View style={styles.line} /><TouchableOpacity style={styles.menuItem}><MaterialCommunityIconsname="chat-plus-outline"size={24}color="black"/><Text style={styles.menuTxt}>创建群聊</Text></TouchableOpacity></View>);};return (<Modaltransparent={true}visible={visible}statusBarTranslucent={true}animationType="fade"onRequestClose={hide}><TouchableOpacity style={styles.root} onPress={hide}>{renderMenus()}</TouchableOpacity></Modal>);
});
const styles = StyleSheet.create({root: {width: "100%",height: "100%",backgroundColor: "#00000040",},content: {width: 170,backgroundColor: "white",borderRadius: 16,position: "absolute",right: 10,},menuItem: {width: "100%",flexDirection: "row",alignItems: "center",height: 56,paddingLeft: 20,},menuIcon: {width: 28,height: 28,},menuTxt: {fontSize: 18,color: "#333",marginLeft: 10,},line: {marginLeft: 20,marginRight: 16,height: 1,backgroundColor: "#eee",},
});
空白页
components/Empty.tsx
import React from "react";
import { Image, StyleSheet, Text, View } from "react-native";
type Props = {icon: number;tips: string;
};
// eslint-disable-next-line react/display-name
export default ({ icon, tips }: Props) => {return (<View style={styles.root}><Image style={styles.icon} source={icon} /><Text style={styles.tipsTxt}>{tips}</Text></View>);
};
const styles = StyleSheet.create({root: {alignItems: "center",paddingTop: 120,},icon: {width: 96,height: 96,resizeMode: "contain",},tipsTxt: {fontSize: 14,color: "#bbb",marginTop: 16,},
});
图片素材