使用 UniApp 制作动态加载的瀑布流布局
前言
最近在开发一个小程序项目时,遇到了需要实现瀑布流布局的需求。众所周知,瀑布流布局在展示不规则尺寸内容(如图片、商品卡片等)时非常美观和实用。但在实际开发过程中,我发现普通的 flex 或 grid 布局很难满足这一需求,尤其是当需要配合上拉加载更多功能时,更是增加了实现难度。
经过一番摸索和实践,我总结出了一套在 UniApp 中实现动态加载瀑布流布局的方案,希望能给大家提供一些参考和帮助。
瀑布流布局原理
瀑布流布局的核心思想是:将元素按照自上而下的方式依次排列,但不是简单地一列一列排,而是始终将新元素放置在当前高度最小的那一列,这样就能保证各列高度尽可能接近,整体视觉效果更加协调。
技术实现思路
在 UniApp 中实现瀑布流布局,我尝试过以下几种方案:
- 使用原生 CSS 的 column 属性
- 使用 flex 布局模拟
- 使用 JavaScript 计算每个元素的位置
最终我选择了第三种方案,因为它具有最好的兼容性和最大的灵活性,特别是在处理动态加载数据时。
具体实现步骤
1. 页面结构设计
首先,我们需要创建一个基本的页面结构:
<template>
<view class="waterfall-container">
<view class="waterfall-column" v-for="(column, columnIndex) in columns" :key="columnIndex">
<view
class="waterfall-item"
v-for="(item, itemIndex) in column"
:key="item.id"
@click="handleItemClick(item)"
>
<image
class="item-image"
:src="item.imageUrl"
:style="{ height: item.height + 'rpx' }"
mode="widthFix"
@load="onImageLoad(item, columnIndex, itemIndex)"
/>
<view class="item-content">
<text class="item-title">{{ item.title }}</text>
<view class="item-info">
<text class="item-price">¥{{ item.price }}</text>
<text class="item-likes">{{ item.likes }}赞</text>
</view>
</view>
</view>
</view>
</view>
</template>
2. 数据结构和状态管理
<script>
export default {
data() {
return {
columns: [[], []], // 默认两列瀑布流
columnHeights: [0, 0], // 记录每列的当前高度
page: 1,
loading: false,
hasMore: true,
dataList: []
};
},
onLoad() {
this.loadInitialData();
},
// 上拉加载更多
onReachBottom() {
if (this.hasMore && !this.loading) {
this.loadMoreData();
}
},
methods: {
async loadInitialData() {
this.loading = true;
try {
const result = await this.fetchData(1);
this.dataList = result.data;
this.arrangeItems(this.dataList);
this.hasMore = result.hasMore;
this.page = 1;
} catch (error) {
console.error('加载数据失败:', error);
uni.showToast({
title: '加载失败,请重试',
icon: 'none'
});
} finally {
this.loading = false;
}
},
async loadMoreData() {
if (this.loading) return;
this.loading = true;
uni.showLoading({
title: '加载中...'
});
try {
const nextPage = this.page + 1;
const result = await this.fetchData(nextPage);
if (result.data && result.data.length > 0) {
this.dataList = [...this.dataList, ...result.data];
this.arrangeItems(result.data);
this.page = nextPage;
this.hasMore = result.hasMore;
} else {
this.hasMore = false;
}
} catch (error) {
console.error('加载更多数据失败:', error);
uni.showToast({
title: '加载失败,请重试',
icon: 'none'
});
} finally {
this.loading = false;
uni.hideLoading();
}
},
// 模拟从服务器获取数据
fetchData(page) {
return new Promise((resolve) => {
setTimeout(() => {
// 模拟数据,实际项目中应该从服务器获取
const mockData = Array.from({ length: 10 }, (_, i) => ({
id: page * 100 + i,
title: `商品${page * 100 + i}`,
price: Math.floor(Math.random() * 1000 + 100),
likes: Math.floor(Math.random() * 1000),
imageUrl: `https://picsum.photos/200/300?random=${page * 100 + i}`,
height: Math.floor(Math.random() * 200 + 200) // 随机高度,让瀑布流效果更明显
}));
resolve({
data: mockData,
hasMore: page < 5 // 模拟只有5页数据
});
}, 1000);
});
},
// 核心算法:将新项目添加到高度最小的列中
arrangeItems(items) {
items.forEach(item => {
// 找出当前高度最小的列
const minHeightIndex = this.columnHeights.indexOf(Math.min(...this.columnHeights));
// 将项目添加到该列
this.columns[minHeightIndex].push(item);
// 更新该列的高度(这里用item.height加上内容区域的估计高度)
this.columnHeights[minHeightIndex] += (item.height + 120); // 120是内容区域的估计高度
});
},
// 图片加载完成后,调整列高度计算
onImageLoad(item, columnIndex, itemIndex) {
// 这里可以根据实际加载后的图片高度重新计算列高度
// 真实项目中可能需要获取图片实际渲染高度
console.log('图片加载完成', item.id);
},
handleItemClick(item) {
uni.navigateTo({
url: `/pages/detail/detail?id=${item.id}`
});
}
}
};
</script>
3. 样式设计
<style lang="scss">
.waterfall-container {
display: flex;
padding: 20rpx;
box-sizing: border-box;
background-color: #f5f5f5;
}
.waterfall-column {
flex: 1;
display: flex;
flex-direction: column;
&:first-child {
margin-right: 10rpx;
}
&:last-child {
margin-left: 10rpx;
}
}
.waterfall-item {
background-color: #ffffff;
border-radius: 12rpx;
margin-bottom: 20rpx;
overflow: hidden;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
transition: transform 0.3s;
&:active {
transform: scale(0.98);
}
}
.item-image {
width: 100%;
display: block;
}
.item-content {
padding: 16rpx;
}
.item-title {
font-size: 28rpx;
color: #333;
margin-bottom: 12rpx;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
text-overflow: ellipsis;
}
.item-info {
display: flex;
justify-content: space-between;
align-items: center;
}
.item-price {
font-size: 32rpx;
color: #ff4b2b;
font-weight: bold;
}
.item-likes {
font-size: 24rpx;
color: #999;
}
</style>
优化与进阶
1. 图片懒加载
为了优化性能,特别是在图片较多的情况下,我们可以实现图片的懒加载功能。UniApp提供了lazyLoad
属性,配合scrollview
可以很容易实现:
<image
class="item-image"
:src="item.imageUrl"
:style="{ height: item.height + 'rpx' }"
mode="widthFix"
lazy-load
@load="onImageLoad(item, columnIndex, itemIndex)"
/>
2. 列数动态调整
针对不同屏幕尺寸,我们可以动态调整瀑布流的列数:
data() {
return {
columns: [],
columnHeights: [],
columnCount: 2, // 默认列数
// 其他数据...
};
},
onLoad() {
// 获取设备信息,动态设置列数
const systemInfo = uni.getSystemInfoSync();
// 如果是平板等大屏设备,可以显示更多列
if (systemInfo.windowWidth > 768) {
this.columnCount = 3;
}
// 初始化列数据
this.columns = Array.from({ length: this.columnCount }, () => []);
this.columnHeights = Array(this.columnCount).fill(0);
this.loadInitialData();
}
3. 下拉刷新
结合UniApp的下拉刷新功能,我们可以很容易实现列表刷新:
// 页面配置
export default {
enablePullDownRefresh: true,
// ...其他配置
}
// 方法实现
onPullDownRefresh() {
this.resetAndReload();
},
resetAndReload() {
// 重置数据
this.columns = Array.from({ length: this.columnCount }, () => []);
this.columnHeights = Array(this.columnCount).fill(0);
this.page = 1;
this.hasMore = true;
this.dataList = [];
// 重新加载
this.loadInitialData().then(() => {
uni.stopPullDownRefresh();
});
}
实际应用案例
我在一个电商类小程序的商品列表页面应用了这种瀑布流布局,效果非常好。用户反馈说浏览商品时比传统的列表更加舒适,能够在同一屏幕内看到更多不同的商品,提升了浏览效率。
特别是对于衣服、家居用品等视觉信息很重要的商品,瀑布流布局可以根据商品图片的实际比例来展示,避免了固定比例裁剪可能带来的信息丢失,商品展示效果更佳。
遇到的问题与解决方案
1. 图片加载速度不一致导致布局跳动
问题:由于网络原因,图片加载速度可能不一致,导致已计算好位置的元素在图片加载后发生位置变化。
解决方案:预设图片高度,或者使用骨架屏占位,在图片完全加载后再显示真实内容。
<view class="waterfall-item">
<view v-if="!item.imageLoaded" class="skeleton-image" :style="{ height: item.height + 'rpx' }"></view>
<image
v-else
class="item-image"
:src="item.imageUrl"
:style="{ height: item.height + 'rpx' }"
mode="widthFix"
/>
<!-- 内容部分 -->
</view>
2. 性能优化
在数据量很大时,可能会出现性能问题。我的解决方案是:
- 使用虚拟列表,只渲染可见区域的元素
- 分批次添加数据,而不是一次性添加所有数据
- 对于复杂计算,使用防抖和节流技术
// 分批次添加数据
arrangeItemsInBatches(items) {
const batchSize = 5;
const totalItems = items.length;
let processedCount = 0;
const processBatch = () => {
const batch = items.slice(processedCount, processedCount + batchSize);
this.arrangeItems(batch);
processedCount += batch.length;
if (processedCount < totalItems) {
setTimeout(processBatch, 50);
}
};
processBatch();
}
总结
通过在UniApp中实现动态加载的瀑布流布局,我们可以为用户提供更好的视觉体验和浏览效率。这种布局特别适合展示不规则尺寸的内容,如图片、商品卡片等。
实现这种布局的关键在于:
- 正确计算每列的高度并动态分配新元素
- 处理好图片加载和元素高度计算的问题
- 结合动态加载实现无限滚动效果
- 针对性能问题进行优化
希望这篇文章对你在UniApp中实现瀑布流布局有所帮助。如果有任何问题或建议,欢迎在评论区交流讨论!