前言
瀑布流布局(Waterfall Layout)是一种特别适合展示推荐图片或者商品内容的布局。下面介绍下在uniapp中实现瀑布流的实现。
1. 瀑布流的特点和实现思路
1.1 特点
瀑布流布局是一种动态网格布局,特点是:
- 内容块宽度固定,高度不固定
- 新内容会自动填充到当前高度最小的列
- 视觉上呈现参差不齐的多栏效果
- 特别适合展示推荐图片、商品等高度不一致的内容
1.2 实现瀑布流的核心思路
在实现瀑布流之前我们需要考虑几个问题:
- 数据分列处理:将数据源分配到不同列中
- 高度计算:实时计算各列高度以决定新项的放置位置
- 图片加载处理:获取图片实际高度以准确计算布局
实现思路:
- 确定展示的列数,使用columnList、columnHeight
- 若定义为两列
- columnList存放左右列数据,使用二维数组进行存放,可以实现多列瀑布流:
[[leftColumnItem],[rightColumnItem]]
- columnHeight存放列高度:
[leftHeight,rightHeight]
- 计算高度,判断最小列,插入
- 在uniapp中获取图片高度可以使用uni.getImageInfo,找出最小列高度进行插入
- 实现上拉加载
- 可以使用uniapp中的onReachBottom实现,组件内通过监听外部页面传入的触底信号,进行请求加载更多
2. 代码实现
下面直接看代码实现:
2.1 基本结构
我们先定义瀑布流的基本HTML结构:其中columnList定义为二维数组可实现多列的关键
<template>
<view v-if="list.length" class="waterfall">
<view class="title">为您推荐</view>
<view class="waterfall-page">
<view class="waterfall-page-column" v-for="(item, index) in columnList" :key="index" ref="column">
<view class="waterfall-page-item" v-for="(pItem, pIndex) in item" :key="pIndex">
<image class="waterfall-page-img" :src="pItem.logo" mode="widthFix"></image>
<view class="waterfall-page-title">{{ pItem.goodsName }}</view>
</view>
</view>
</view>
<!-- 加载更多 -->
<view class="loading" v-if="loading">
<text>加载中...</text>
</view>
</view>
</template>
2.2 数据初始化
通过外部传入列数,初始化columnList
、columnHeight
数据结构, 初始化数据
props: {
columnCount: {
type: Number,
default: 2
}
},
data() {
return {
list: [], // 列表
pageNum: 1, // 页码
loading: false,
columnList: [], // 每列图片
columnHeight: [] // 每列图片高度
}
},
mounted() {
this.initData()
// 获取数据
this.getProductRecommend()
},
methods: {
initData() {
for (let i = 0; i < this.columnCount; i++) {
this.columnList.push([])
this.columnHeight.push(0)
}
}
}
2.3 瀑布流布局核心计算
在每次获取数据后调用setWaterfallLayout
,实现瀑布流布局的核心是setWaterfallLayout
方法:
async setWaterfallLayout() {
for (let i = 0; i < this.list.length; i++) {
const item = this.list[i]
try {
// 获取图片信息
const imgInfo = await uni.getImageInfo({ src: item.logo })
const h = imgInfo[1].height
if (this.columnCount === 1) {
// 单列处理
this.columnList[0].push(item)
this.columnHeight[0] += h
} else {
// 多列处理:找出高度最小的列
let minHeightIndex = 0
for (let j = 1; j < this.columnCount; j++) {
if (this.columnHeight[j] < this.columnHeight[minHeightIndex]) {
minHeightIndex = j
}
}
// 将图片添加到高度最小的列
this.columnList[minHeightIndex].push(item)
this.columnHeight[minHeightIndex] += h
}
} catch (error) {
console.error('获取图片信息失败:', error)
}
}
this.$emit('complete')
}
也很简单,就是得到哪列少就填入哪列。
2.4 加载更多数据
通过监听滚动到底部事件实现无限加载,需要外部传入,因为uniapp组件不支持onReachBottom
这种方法,只有页面支持。
页面:
<waterfall :isReachBottom="isReachBottom" @complete="handleComplete" />
onReachBottom() {
this.isReachBottom = true
},
methods: {
handleComplete() {
this.isReachBottom = false
},
}
组件内:监听变化,进行加载
watch: {
isReachBottom(val) {
if (val) {
this.getProductRecommend()
}
}
},
methods: {
getProductRecommend() {
this.loading = true
// 调用接口
setWaterfallLayout
this.loading = false
}
}
3. 效果展示
通过控制columnCount
设置,达到效果,下面进行效果展示:
3.1 单列
3.2 双列
4. 完整代码
<template>
<view v-if="list.length" class="waterfall">
<view class="title">为您推荐</view>
<view class="waterfall-page">
<view class="waterfall-page-column" v-for="(item, index) in columnList" :key="index" ref="column">
<view class="waterfall-page-item" v-for="(pItem, pIndex) in item" :key="pIndex">
<image class="waterfall-page-img" :src="pItem.logo" mode="widthFix"></image>
<view class="waterfall-page-title">{{ pItem.goodsName }}</view>
</view>
</view>
</view>
<!-- 加载更多 -->
<view class="loading" v-if="loading">
<text>加载中...</text>
</view>
</view>
</template>
<script>
import api from '@/utils/api';
import { getStorage } from '@/utils/wxuser';
export default {
props: {
isReachBottom: {
type: Boolean,
default: false
},
columnCount: {
type: Number,
default: 1
}
},
data() {
return {
list: [], // 图片列表
pageNum: 1, // 页码
loading: false,
columnList: [], // 每列图片
columnHeight: [] // 每列图片高度
}
},
watch: {
isReachBottom(val) {
if (val) {
this.getProductRecommend()
}
}
},
methods: {
/** 推荐商品*/
getProductRecommend() {
this.loading = true
api.getProductRecommend(obj).then((res) => {
if (res.result == 1) {
this.list = this.list.concat(res.retVal)
this.setWaterfallLayout()
}
this.loading = false
})
},
/** 初始化数据 */
initData() {
for (let i = 0; i < this.columnCount; i++) {
this.columnList.push([])
this.columnHeight.push(0)
}
},
/** 设置瀑布流布局 */
async setWaterfallLayout() {
for (let i = 0; i < this.list.length; i++) {
const item = this.list[i]
try {
// 获取图片信息
const imgInfo = await uni.getImageInfo({ src: item.logo })
const h = imgInfo[1].height
console.log(
'图片高度:',
imgInfo[1],
this.columnList,
this.columnHeight
)
if (this.columnCount === 1) {
// 单列处理
this.columnList[0].push(item)
this.columnHeight[0] += h
} else {
// 多列处理:找出高度最小的列
let minHeightIndex = 0
for (let j = 1; j < this.columnCount; j++) {
if (this.columnHeight[j] < this.columnHeight[minHeightIndex]) {
minHeightIndex = j
}
}
// 将图片添加到高度最小的列
this.columnList[minHeightIndex].push(item)
this.columnHeight[minHeightIndex] += h
}
} catch (error) {
console.error('获取图片信息失败:', error)
}
}
this.$emit('complete')
}
},
mounted() {
this.initData()
this.getProductRecommend()
}
}
</script>
<style lang="less" scoped>
.waterfall {
padding: 0 15px;
.title {
font-family: PingFangSC-Semibold;
font-size: 18px;
color: #333333;
letter-spacing: 0;
line-height: 18px;
font-weight: 600;
margin-bottom: 15px;
}
.waterfall-page {
display: flex;
align-items: flex-start;
.waterfall-page-column {
box-sizing: border-box;
flex: 1;
padding: 0 15rpx;
width: 0;
.waterfall-page-item {
margin-bottom: 20px;
.waterfall-page-img {
display: inline-block;
width: 100%;
}
.waterfall-page-title {
margin-top: 10px;
font-family: PingFangSC-Medium;
font-size: 14px;
color: #111111;
letter-spacing: 0;
text-align: justify;
font-weight: 500;
}
}
}
}
.loading {
text-align: center;
padding: 20rpx 0;
color: #999;
font-size: 24rpx;
}
}
</style>
总结
最后总结一下:实现瀑布流的关键点是图片高度的获取,使用uni.getImageInfo
获取图片实际高度, 进行列高度比较:每次添加新项到高度最小的列,宽度处理:设置width: 0
和flex: 1
确保各列等宽分布,图片模式需要使用mode="widthFix"
确保图片按宽度等比缩放。