1.实现过程
在主页点击某一条帖子可以跳转到帖子的详情页,使用路由导航跳转到详情页并传递参数,同时主页默认只展示帖子的第一种图片,如果有多张图片,则在图片右下角显示+N(比如有两张图片则显示+1)
<MasonryList
innerRef={scrollRef}
data={posts}
keyExtractor={(item: Post) => item.id.toString()}
numColumns={2}
contentContainerStyle={styles.content}
showsVerticalScrollIndicator={true}
refreshing={refreshing}
onRefresh={onRefresh}
refreshControl={true}
refreshControlProps={{
colors: [COLORS.primary], // Android 上的颜色
tintColor: COLORS.primary, // iOS 上的颜色
}}
renderItem={({ item }: any) => {
const handlePress = () => {
navigation.navigate('PostDetail', { post: item });
};
if (item.images && item.images.length > 0) {
return (
<TouchableOpacity onPress={handlePress}>
<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={{ marginTop: 8 }}>
{/* 只展示第一张图片 */}
<Image
source={{ uri: item.images[0] }}
style={{ width: '100%', height: 120, borderRadius: 8 }}
resizeMode="cover"
/>
{/* 如果有多张图片,显示+N的提示 */}
{item.images.length > 1 && (
<View style={{
position: 'absolute',
right: 8,
bottom: 8,
backgroundColor: 'rgba(0,0,0,0.6)',
paddingHorizontal: 8,
paddingVertical: 4,
borderRadius: 12,
}}>
<Text style={{ color: 'white', fontSize: 12 }}>+{item.images.length - 1}</Text>
</View>
)}
</View>
</View>
</TouchableOpacity>
);
}
return (
<TouchableOpacity onPress={handlePress}>
<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>
</TouchableOpacity>
);
}}
/>
帖子详情页采用FlatList实现图片的横向滚动
slideSize 和 roundIndex 是在 PostDetailScreen.tsx 文件中用于实现图片轮播功能的关键变量
slideSize 表示每个轮播项(在这个例子中是图片)的宽度。在代码中,它是从滚动事件中获取的:
const slideSize = event.nativeEvent.layoutMeasurement.width;
这里的 event.nativeEvent.layoutMeasurement.width 获取的是 FlatList 组件可见区域的宽度,也就是一个完整轮播项的宽度。在应用中,由于我们设置了 pagingEnabled={true},每次滚动都会停在完整的一项上,所以这个宽度通常等于设备的屏幕宽度。
roundIndex 是当前显示的轮播项的索引(从0开始计数)。它是通过计算滚动位置与轮播项宽度的比值,然后四舍五入得到的:
const roundIndex = Math.round(event.nativeEvent.contentOffset.x / slideSize);
这里的计算过程是:
event.nativeEvent.contentOffset.x 获取水平滚动的偏移量(即用户滑动了多远)
将这个偏移量除以 slideSize(每个轮播项的宽度)得到一个比值
使用 Math.round() 将这个比值四舍五入到最接近的整数,这就是当前显示的轮播项的索引
例如:
- 如果用户没有滚动(在第一张图片),contentOffset.x 为 0,则 roundIndex 为 0
- 如果用户滚动到第二张图片,contentOffset.x 大约为屏幕宽度,则 roundIndex 为 1
- 如果用户正在从第一张图片滑向第二张图片,但还没完全滑过去(比如滑了屏幕宽度的40%),则 contentOffset.x 约为屏幕宽度的0.4倍,roundIndex 仍为0(因为四舍五入)
- 如果用户已经滑过了屏幕宽度的50%以上,则 roundIndex 会变为1
const handleScroll = (event: any) => {
const slideSize = event.nativeEvent.layoutMeasurement.width;
const roundIndex = Math.round(event.nativeEvent.contentOffset.x / slideSize);
setActiveIndex(roundIndex);
};
const renderImageCarousel = () => {
if (!post.images || post.images.length === 0) {
return null;
}
return (
<View>
<FlatList
ref={flatListRef}
data={post.images}
horizontal
pagingEnabled
showsHorizontalScrollIndicator={false}
onScroll={handleScroll}
renderItem={({ item }) => (
<View style={styles.imageContainer}>
<Image
source={{ uri: item }}
style={styles.postImage}
resizeMode="cover"
/>
</View>
)}
keyExtractor={(_, index) => index.toString()}
/>
{post.images.length > 1 && (
<View style={styles.paginationContainer}>
{post.images.map((_: any, index: number) => (
<View
key={index}
style={[
styles.paginationDot,
activeIndex === index ? styles.activeDot : undefined,
]}
/>
))}
</View>
)}
</View>
);
};
2.踩坑记录
在上传完成图片后,下次在刷新登录进入首页发现帖子的图片显示不出来了
分析代码后,我发现了问题所在。当上传图片并发布帖子时,图片的 URI 是以本地临时文件路径的形式存储的,这些路径在应用重启或刷新后可能会失效。
解决方案:将图片从临时位置复制到应用的永久存储区域
安装依赖:npm install expo-file-system
import * as FileSystem from 'expo-file-system';
import { Platform } from 'react-native';
// 创建永久存储图片的目录
const setupImageDirectory = async (): Promise<string> => {
// 检查可用的目录常量
let baseDir = './';
const imageDir = `${baseDir}images/`;
try {
const dirInfo = await FileSystem.getInfoAsync(imageDir);
if (!dirInfo.exists) {
await FileSystem.makeDirectoryAsync(imageDir, { intermediates: true });
}
} catch (error) {
console.error('创建图片目录失败:', error);
}
return imageDir;
};
// 将临时图片保存到永久存储
export const saveImageToPermanentStorage = async (uri: string): Promise<string> => {
try {
// 对于 web 平台,直接返回 URI,因为 web 平台的资源 URI 通常是网络 URL
if (Platform.OS === 'web') {
return uri;
}
const imageDir = await setupImageDirectory();
const filename = `image_${Date.now()}_${Math.floor(Math.random() * 10000)}.jpg`;
const destinationUri = `${imageDir}${filename}`;
await FileSystem.copyAsync({
from: uri,
to: destinationUri
});
return destinationUri;
} catch (error) {
console.error('保存图片失败:', error);
return uri; // 失败时返回原始 URI
}
};
// 保存多张图片
export const saveMultipleImages = async (uris: string[]): Promise<string[]> => {
const savedUris = await Promise.all(
uris.map(uri => saveImageToPermanentStorage(uri))
);
return savedUris;
};
然后,修改 HomeScreen.tsx 中的发布帖子逻辑,在保存帖子前先保存图片:
// 在文件顶部导入新的图片存储工具
import { saveMultipleImages } from '../utils/imageStorage';
// 修改 handleGoPostEditor 函数
const handleGoPostEditor = () => {
navigation.navigate('PostEditor', {
onPublish: async (post: { title: string; text: string; images: string[] }) => {
let permanentImages = post.images || [];
// 如果有图片,先保存到永久存储
if (post.images && post.images.length > 0) {
permanentImages = await saveMultipleImages(post.images);
}
const newPost = {
id: Date.now(),
title: post.title || '新发布',
summary: post.text || '',
images: permanentImages // 使用永久存储的图片路径
};
await addPost(newPost);
setPosts(prev => [newPost, ...prev]);
}
});
};
这个解决方案的工作原理是:
- 当用户选择或拍摄图片时,获取到临时 URI
- 在保存帖子前,将图片从临时位置复制到应用的永久存储区域
- 使用永久存储的 URI 来保存帖子数据
- 这样,即使应用重启或刷新,图片也能正常显示