uniapp实现自定义多列瀑布流

前言

瀑布流布局(Waterfall Layout)是一种特别适合展示推荐图片或者商品内容的布局。下面介绍下在uniapp中实现瀑布流的实现。

1. 瀑布流的特点和实现思路

1.1 特点

瀑布流布局是一种动态网格布局,特点是:

  • 内容块宽度固定,高度不固定
  • 新内容会自动填充到当前高度最小的列
  • 视觉上呈现参差不齐的多栏效果
  • 特别适合展示推荐图片、商品等高度不一致的内容

1.2 实现瀑布流的核心思路

在实现瀑布流之前我们需要考虑几个问题:

  1. 数据分列处理:将数据源分配到不同列中
  2. 高度计算:实时计算各列高度以决定新项的放置位置
  3. 图片加载处理:获取图片实际高度以准确计算布局

实现思路:

  1. 确定展示的列数,使用columnList、columnHeight
    • 若定义为两列
    • columnList存放左右列数据,使用二维数组进行存放,可以实现多列瀑布流:[[leftColumnItem],[rightColumnItem]]
    • columnHeight存放列高度:[leftHeight,rightHeight]
  2. 计算高度,判断最小列,插入
    • 在uniapp中获取图片高度可以使用uni.getImageInfo,找出最小列高度进行插入
  3. 实现上拉加载
    • 可以使用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 数据初始化

通过外部传入列数,初始化columnListcolumnHeight数据结构, 初始化数据

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: 0flex: 1确保各列等宽分布,图片模式需要使用mode="widthFix"确保图片按宽度等比缩放。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值