首页/发布页

193 阅读4分钟

1.双列瀑布流布局

安装依赖:npm install @react-native-seoul/masonry-list

1.1MasonryList 简介

MasonryList 是一个 React Native 组件库(如 @react-native-seoul/masonry-list),用于实现类似瀑布流(Masonry Layout)的列表展示。它的特点是:

  • 支持多列(numColumns),每列高度自适应,内容像“砖块”一样错落排列。
  • 适合展示图片、卡片等高度不一的内容,常见于小红书、Pinterest 等应用。
  • 性能优良,支持大数据量虚拟滚动。

1.2在本项目中的运用

在(HomeScreen.tsx)中,MasonryList 主要用于首页内容区域的瀑布流展示:

<MasonryList
  data={posts}
  keyExtractor={(item: Post) => item.id.toString()}
  numColumns={2}
  contentContainerStyle={styles.content}
  showsVerticalScrollIndicator={true}
  renderItem={({ item }: any) => {
    // 渲染每个帖子卡片,支持图片和文字
  }}
/>

具体作用:

  • data={posts}:传入帖子数据数组。
  • numColumns={2}:设置为2列瀑布流。
  • renderItem:自定义每个卡片的渲染方式,支持图片、标题、摘要等。
  • contentContainerStyle:自定义内容区样式,保证左右留白和顶部间距。
  • 在 Web 端和移动端都能自适应高度,实现内容错落有致的排列和流畅滚动。

2.发布页面

2.1向首页传递帖子数据

  • 首页跳转发布页时传递 onPublish 回调。
// 跳转发布页时传递回调
  const handleGoPostEditor = () => {
    navigation.navigate('PostEditor', {
      onPublish: async (post: { title: string; text: string; images: string[] }) => {
        const newPost = {
          id: Date.now(),
          title: post.title || '新发布',
          summary: post.text || '',
          images: post.images || []
        };
        await addPost(newPost);
        setPosts(prev => [newPost, ...prev]);
      }
    });
  };
  • 发布页发布时调用 onPublish 并 goBack,数据回传并插入首页列表。
// 发布帖子
  const handlePublish = () => {
    if (!title.trim() && !text.trim() && images.length === 0) {
      alert('请输入标题、内容或添加图片');
      return;
    }
    // 回调传递数据
    if (route.params && typeof route.params.onPublish === 'function') {
      route.params.onPublish({ title, text, images });
    }
    navigation.goBack();
  };

2.2发帖支持本地相册和拍照图片上传

安装依赖:npm install expo-image-picker

ImagePicker 是 Expo 提供的一个图片/视频选择与拍照库(expo-image-picker),用于在 React Native 应用中实现从相册选择图片或调用摄像头拍照的功能。

主要功能

  • 打开系统相册选择图片或视频
  • 调用摄像头拍照或录像
  • 支持多选、压缩、裁剪等参数
  • 兼容 iOS、Android、Web

在本项目中的使用

在PostEditorScreen.tsx 文件中,ImagePicker 主要用于发帖时选择图片或拍照:

import * as ImagePicker from 'expo-image-picker';

// 选择图片
const pickImage = async () => {
  const result = await ImagePicker.launchImageLibraryAsync({
    mediaTypes: ImagePicker.MediaTypeOptions.Images,
    allowsMultipleSelection: true,
    quality: 0.8,
  });
  if (!result.canceled && result.assets) {
    setImages([...images, ...result.assets.map(a => a.uri)]);
  }
};

// 拍照
const takePhoto = async () => {
  const result = await ImagePicker.launchCameraAsync({
    quality: 0.8,
  });
  if (!result.canceled && result.assets) {
    setImages([...images, ...result.assets.map(a => a.uri)]);
  }
};
具体说明
  • launchImageLibraryAsync:打开系统相册,允许多选图片,设置图片压缩质量。
  • launchCameraAsync:调用摄像头拍照,设置图片压缩质量。
  • 选择或拍照后,将图片的 uri 存入 images 状态,供发帖时展示和上传。

3.踩坑记录

3.1web端底部导航栏消失

在测试的过程中发现,底部导航栏在android手机上能正常显示,web端直接消失。查看dom发现由于定位的问题,在web端该导航栏跑到内容区的下面去了,而内容区又是一个很长的滚动列表,导致导航栏不可见。

解决办法:

tabBar: {
  flexDirection: 'row',
  backgroundColor: COLORS.primary,
  height: 56,
  alignItems: 'center',
  justifyContent: 'space-around',
  // 兼容 web 端底部固定
  position: Platform.OS === 'web' ? 'fixed' : 'absolute',
  left: 0,
  right: 0,
  bottom: 0,
  borderTopLeftRadius: 12,
  borderTopRightRadius: 12,
  shadowColor: COLORS.cardShadow,
  shadowOffset: { width: 0, height: -2 },
  shadowOpacity: 0.08,
  shadowRadius: 8,
  elevation: 8,
  zIndex: 100,
},
  • Platform.OS === 'web' ? 'fixed' : 'absolute' 这行代码让底部导航栏在 Web 端用 CSS 的 position: fixed,在原生端用 position: absolute。
  • fixed 能让导航栏在 Web 页面滚动时始终固定在底部,不会被内容遮挡或随内容滚动。
  • 其他属性(如 left: 0, right: 0, bottom: 0)确保导航栏横跨底部。

3.2内容区域在web端无法滚动

MasonryList 在 web 端无法滚动,主要原因是底部导航栏用 position: fixed,而内容区的父容器高度被“撑满”,导致内容溢出时不会出现滚动条。

解决办法:

  1. 让内容区高度减去底部导航栏高度在 web 端,内容区需要用 height: calc(100vh - 56px),这样内容区才会出现滚动条。React Native 的 StyleSheet 不支持 calc,但可以用内联样式+平台判断实现。
  2. MasonryList 不要设置 flex: 1,而是让外层 View 控制高度

这样 MasonryList 会自动填满内容区并可滚动。

import { Platform } from 'react-native';
// ...existing code...


{
  /* 内容区域:真正瀑布流 */
}
<View
  style={
    Platform.OS === 'web'
      ? {
          height: 'calc(100vh - 56px)', // 56px为底部tabBar高度
          overflowY: 'auto',
        }
      : { flex: 1, minHeight: 0 }
  }
>
  <MasonryList
    data={posts}
    keyExtractor={(item: Post) => item.id.toString()}
    numColumns={2}
    contentContainerStyle={styles.content}
    showsVerticalScrollIndicator={true}
    renderItem={({ item }: any) => {
      if (item.images && item.images.length > 0) {
        return (
          <View style={[styles.card, { width: CARD_WIDTH, alignSelf: 'flex-start' }]}>
            <Text style={styles.cardTitle}>{item.title}</Text>
            <Text style={styles.cardSummary}>{item.summary}</Text>
            <View style={{ flexDirection: 'row', flexWrap: 'wrap', marginTop: 8 }}>
              {item.images.map((uri: string, idx: number) => (
                <Image
                  key={idx}
                  source={{ uri }}
                  style={{ width: 80, height: 80, borderRadius: 8, marginRight: 8, marginBottom: 8 }}
                />
              ))}
            </View>
          </View>
        );
      }
      return (
        <View style={[styles.card, { width: CARD_WIDTH, alignSelf: 'flex-start' }]}>
          <Text style={styles.cardTitle}>{item.title}</Text>
          <Text style={styles.cardSummary}>{item.summary}</Text>
        </View>
      );
    }}
  />
</View>

是不是感觉跨端开发要考虑的问题还挺多的呢

项目地址如下,欢迎关注:github.com/ggbond11/pi…