使用 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-elseclass="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中實現瀑布流布局有所幫助。如果有任何問題或建議,歡迎在評論區交流討論!
參考資料
- UniApp官方文檔:https://uniapp.dcloud.io/
- 瀑布流布局原理:https://www.w3cplus.com/css/pure-css-create-masonry-layout.html