乐于分享
好东西不私藏

UniApp 实现瀑布流自定义多布局(支持图片、图文、视频等)

UniApp 实现瀑布流自定义多布局(支持图片、图文、视频等)

一个分享 技术 | 生活 | 社会 科技 | 经济 | 情感  的前端爱好者!


一、前言

在内容展示类应用中,瀑布流布局因其视觉美观、空间利用率高而被广泛使用。常见的如小红书、抖音、Pinterest 等都采用瀑布流来展示图文或视频内容。

UniApp 作为跨端开发框架,原生不提供瀑布流组件,但通过合理设计,我们可以实现一个高性能、可扩展、支持多类型内容(图片、图文、视频)的自定义瀑布流组件

本文将带你从零搭建一个灵活的瀑布流组件,并支持动态加载、滚动到底自动请求、不同卡片类型混排等功能。

二、核心思路

1. 双列布局 + 动态分配

  • 将内容分为左右两列(也可扩展为多列)
  • 每次新数据到来时,将其插入到当前高度较小的那一列
  • 使用 scroll-view 或页面滚动监听实现懒加载

2. 自定义卡片组件

  • 定义通用卡片基类,支持 type: 'image' | 'text-image' | 'video'
  • 通过 v-if 或动态组件 <component :is="..."> 渲染不同类型

3. 性能优化

  • 虚拟列表(可选,适用于超长列表)
  • 图片懒加载(lazy-load
  • 视频按需加载(进入可视区域再初始化)

三、代码实现

1. 数据结构设计

const dataList = [  { id: 1, type'image', url: 'https://xxx.jpg', height: 300 },  { id: 2, type'text-image', title: '标题', desc: '描述...', cover: 'https://yyy.jpg', height: 400 },  { id: 3, type'video', videoUrl: 'https://zzz.mp4', poster: 'https://poster.jpg', duration: 15, height: 500 }]

注意:每个 item 需要预知高度(可通过后端返回或前端预加载计算),这是瀑布流精准布局的关键。

2. 瀑布流主组件(Waterfall.vue)

<template>  <viewclass="waterfall-container">    <viewclass="columns">      <viewclass="column"ref="leftColumn">        <card-item          v-for="item in leftList"           :key="item.id"          :data="item"        />      </view>      <viewclass="column"ref="rightColumn">        <card-item          v-for="item in rightList"           :key="item.id"          :data="item"        />      </view>    </view>    <!-- 加载更多提示 -->    <viewv-if="loading"class="loading">加载中...</view>    <viewv-else-if="noMore"class="no-more">没有更多了</view>  </view></template><script>import CardItem from './CardItem.vue'export default {  components: { CardItem },  data() {    return {      leftList: [],      rightList: [],      loadingfalse,      noMorefalse,      page1,      pageSize10    }  },  async onReady() {    await this.loadData()  },  // 页面滚动到底部触发(仅 H5/APP 有效)  onReachBottom() {    if (!this.loading && !this.noMore) {      this.page++      this.loadData()    }  },  methods: {    // 分配 item 到较短列    distributeItem(item) {      const leftHeight = this.getColumnHeight('left')      const rightHeight = this.getColumnHeight('right')      if (leftHeight <= rightHeight) {        this.leftList.push(item)      } else {        this.rightList.push(item)      }    },    getColumnHeight(column) {      const ref = column === 'left' ? this.$refs.leftColumn : this.$refs.rightColumn      if (!ref || !ref[0]) return 0      // 注意:uni-app 中 $refs 获取的是数组      return ref[0].$el.offsetHeight || 0    },    async loadData() {      this.loading = true      try {        const res = await this.fetchData(this.pagethis.pageSize)        if (res.length === 0) {          this.noMore = true          return        }        res.forEach(item => this.distributeItem(item))      } catch (e) {        console.error('加载失败', e)      } finally {        this.loading = false      }    },    // 模拟 API 请求    fetchData(page, size) {      return new Promise(resolve => {        setTimeout(() => {          const mock = []          for (let i = 0; i < size; i++) {            const id = (page - 1) * size + i + 1            const type = ['image''text-image''video'][Math.floor(Math.random() * 3)]            mock.push({              id,              type,              // 模拟不同高度              height200 + Math.random() * 300,              url`https://picsum.photos/300/${200 + Math.floor(Math.random() * 300)}`,              title`标题 ${id}`,              desc`这是第 ${id} 条内容的描述信息...`,              videoUrl'https://example.com/video.mp4',              poster`https://picsum.photos/300/200?random=${id}`            })          }          resolve(mock)        }, 800)      })    }  }}</script><stylescoped>.waterfall-container {  display: flex;  padding10rpx;}.columns {  display: flex;  width100%;  gap20rpx;}.column {  flex1;  display: flex;  flex-direction: column;  gap20rpx;}.loading.no-more {  text-align: center;  padding30rpx;  color#999;}</style>

3. 卡片组件(CardItem.vue)

<template>  <viewclass="card":style="{ height: data.height + 'rpx' }">    <blockv-if="data.type === 'image'">      <image        :src="data.url"         mode="widthFix"         lazy-load        class="img"      />    </block>    <blockv-else-if="data.type === 'text-image'">      <image        :src="data.cover"         mode="widthFix"         lazy-load        class="img"      />      <viewclass="content">        <textclass="title">{{ data.title }}</text>        <textclass="desc">{{ data.desc }}</text>      </view>    </block>    <blockv-else-if="data.type === 'video'">      <video        :src="data.videoUrl"         :poster="data.poster"        controls        class="video"      />      <viewclass="video-info">        <text>{{ data.duration }}s</text>      </view>    </block>  </view></template><script>export default {  props: {    data: {      typeObject,      requiredtrue    }  }}</script><stylescoped>.card {  background#fff;  border-radius16rpx;  overflow: hidden;  box-shadow0 4rpx 12rpx rgba(0,0,0,0.08);}.img.video {  width100%;}.content {  padding20rpx;}.title {  font-size28rpx;  font-weight: bold;  display: block;}.desc {  font-size24rpx;  color#666;  margin-top10rpx;  display: block;}.video-info {  position: absolute;  bottom20rpx;  right20rpx;  backgroundrgba(0,0,0,0.5);  color: white;  padding4rpx 12rpx;  border-radius20rpx;  font-size20rpx;}</style>

注意:video 组件在小程序中受平台限制较多,建议使用 cover-view 包裹信息。

四、效果展示(示意图)

五、进阶优化

1. 高度预计算

若后端无法提供高度,可在 onLoad 时用 uni.createSelectorQuery() 动态测量每张图片高度,再分配。
2. 虚拟滚动
对于万级数据,可结合 recycle-list(App 端)或自定义虚拟列表减少 DOM 节点。
3. 骨架屏
在图片/视频加载前显示骨架屏,提升用户体验。
4. 防抖加载
避免快速滚动时多次触发 onReachBottom
5. 平台适配
  • 微信小程序:使用 wx.createIntersectionObserver 监听视频进入视口再播放
  • App 端:可启用 webplazyLoad 提升性能
六、总结

通过双列动态分配 + 自定义卡片组件,我们成功在 UniApp 中实现了支持多类型内容的瀑布流布局

  • 核心实现是通过两列布局 + 高度计算 + 动态分配实现瀑布流效果,保证列高均衡。
  • 采用动态组件方式实现多布局支持,为图片、图文、视频分别设计独立的子组件和高度计算规则。
  • 组件具备自适应能力,通过计算屏幕宽度自动适配不同设备,同时支持懒加载优化性能。
本站文章均为手工撰写未经允许谢绝转载:夜雨聆风 » UniApp 实现瀑布流自定义多布局(支持图片、图文、视频等)

评论 抢沙发

9 + 4 =
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
×
订阅图标按钮