小程序里瀑布流布局的实现思路—— H5、PC也通用

文章目录

    • 瀑布流布局
    • 1. 高度固定的横向瀑布流
    • 2. 宽度固定的纵向瀑布流
        • 2.1 一次性渲染所有元素
        • 2.2 当需要动态加载新数据时
          • 情况一:每次获取到的数据数量相同
          • 情况二:每次获取到的数据数量不确定
          • 情况三: 元素之间高度差变大,每次的数量也可能不一样
          • Masonry插件
          • 小程序的实现细节
        • 2.3 项目中遇到的情况
    • 总结

瀑布流是现在很流行很常见的一种布局,表现为参差不齐的多栏布局,产品很喜欢用。
刚开始写小程序就遇到了瀑布流的需求,也是有点刺激~~

瀑布流布局

瀑布流分为横向和纵向,前者是每个元素高度固定,宽度参差不齐;后者是宽度相对固定,高度不一。
虽然展现的效果不太一样,但实现原理差不多,大同小异,以下内容主要基于实际项目遇到的情况来总结的,更多的实现方式欢迎补充~~

1. 高度固定的横向瀑布流

横向的瀑布流是每个元素的高度固定,通过css的flex布局就能实现出一个大差不离的瀑布流


<template>
  <div>
    <div class="layout-container">
      <div class="ly-item" v-for="(item, index) in list" :key="index" :style="{width: item.width + 'px'}">
        <div class="ly-inner" :style="{background: item.color}">div>
      div>
    div>
  div>
template>

<script>
const colors = ['red', 'yellow', 'blue', 'pink', 'purple', 'green', 'gray', 'skyblue'];

export default {
  name: 'pubo-layout',
  data() {
    return {
      list: [],
    }
  },
  mounted() {
    this.initList()
  },
  methods: {
    initList() {
      let list = []
      for(let i = 0; i < 18; i ++) {
        list.push({
          width: 2 * parseInt(Math.random() * 100),
          color: colors[Math.ceil(Math.random() * colors.length - 1)]
        })
      }
      this.list = list;
    }
  }
}
script>
<style>
  .layout-container {
    margin-top: 32px;
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
  }
  .ly-item {
    flex: none;
    height: 80px;
  }
  .ly-inner {
    height: 100%;
  }
style>

小程序里瀑布流布局的实现思路—— H5、PC也通用_第1张图片
emmmm…,追究起来仅靠css实现的瀑布流也是很粗糙的,勉强能用

2. 宽度固定的纵向瀑布流

宽度固定的瀑布流我总结出两类情况:

  • 一种是初始化时拿到所有数据渲染, 全程渲染一次,不在乎排列顺序;
  • 一种是注重排列顺序或者懒加载的,例如滚动到底部加载新数据;

2.1 一次性渲染所有元素

有一种简单粗暴的方法,通过CSS设置(CSS干啥都有些简单粗暴的赶脚~~)

即通过column-count属性来控几列,从而实现瀑布流。

column-count的具体使用方法可以看张鑫旭大佬的博客⬇️
https://www.zhangxinxu.com/wordpress/2019/01/css-css3-columns-layout/

接下来,上Demo

// 
<template>
  <div>
    <div class="ly-vertical">
      <div class="ly-item" v-for="(item, index) in list2" :key="index" :style="{height: item.height + 'px'}">
        <div class="ly-inner" :style="{background: item.color}"></div>
      </div>
    </div>
  </div>
</template>

<script>
// 找图片太麻烦了,直接用纯色块来代表吧
const colors = ['red', 'yellow', 'blue', 'pink', 'purple', 'green', 'gray', 'skyblue'];

export default {
  name: 'pubo-layout',
  data() {
    return {
      list2: [],
    }
  },
  mounted() {
    this.initList2();
  },
  methods: {
    initList2() {
      let list = []
      for(let i = 0; i < 12; i ++) {
        list.push({
        // 模拟高度不同的元素
          height: 2 * parseInt(Math.random() * 100),
          color: colors[Math.ceil(Math.random() * colors.length - 1)]
        })
      }
      this.list2 = list;
    }
  }
}
</script>
<style>
 
  .ly-vertical {
    width: 60%;
    margin: 32px auto 0 auto;
    column-count: 2;  /*设置列数*/ 
    column-gap: 2px;
  }
  .ly-vertical .ly-item {
    break-inside: avoid; // 知识点
    margin: 8px 0;
  }
</style>

效果是这个样子滴,怎么说呢 , 跟flex实现的横向瀑布流一样粗糙

小程序里瀑布流布局的实现思路—— H5、PC也通用_第2张图片

注意注意:

  1. 子元素里有个属性break-inside; 表示断行方式,设置成avoid以后,当前列剩余空间不足以存放下一个元素时,会把他整个放到第二列,而不会对该元素切分
  2. 由1可知,这种方法里,元素的排列顺序会从左倒右,从上到下依次插入;所以当数据一次性渲染且对顺序没有要求的情况下,这个方法可以用一下;但是如果要求是瀑布流且元素顺序是从左到右的话(常见的商品列表),这个方法就不行咯 实现方法可参照下一节

2.2 当需要动态加载新数据时

比如商品列表页,下拉到底部后会加载新数据,并且一般都要求商品的展示顺序是从左到右。这个时候单靠column-count是不行了

情况一:每次获取到的数据数量相同

情景:
进入商品列表页,获取10条数据,页面下拉到底部,动态加载再次获取10条数据,依次循环。

实现思路:

  • html中一个容器里放置左、右两个容器元素,实现两列的效果;
  • 维护两个商品数组,分别对应左列和右列;
  • 拿到商品数据后根据索引的奇偶,拆分到左、右的商品数组中;

接下来的所有例子都是基于 flex布局 + js动态拆分数据来实现的

// 以下代码以小程序为例

// wxml
// list-vertical设置flex布局,将内部元素分为两列
<view class="list-vertical">
	// 注意这里又包了1层, 父容器display:flex时,两个good-list的高度是一样的
    <view class="good-list">
    // left-card,right-card这一层是为了获取高度用的
      <view id="left-card">
        <card-item
          pt="{{pt}}"
          wx:key="index"
          wx:for="{{leftList}}"
          info="{{item}}"
        />
      </view>
    </view>
    <view class="good-list">
      <view id="right-card">
        <card-item
          pt="{{pt}}"
          wx:key="index"
          wx:for="{{rightList}}"
          info="{{item}}"
        />
      </view>
    </view>
  </view>
// js
Component({
	data: {
	    leftList: [],
	    rightList: [],
	},
	fetchGoodsList(beforeFetchData, options = {}) {
      // 父组件传入调用的接口名
      const { api } = this.properties;
      let { leftList, rightList } = this.data;
      if (api) {
        // params 自己组装查询参数
        return api(params)
          .then(res => {
            const {
              items,
              paginator: { totalCount }
            } = res;
            // 商品数据分为左右两列, 优先插入较短的一列
            let { leftList: leftGoodsList = [], rightList: rightGoodsList = [] } = this.splitList(
              leftList,
              rightList,
              items
            );
            this.setData(
              {
                leftList: [...leftGoodsList],
                rightList: [...rightGoodsList],
              }
            );
          })
      },
      splitList(left, right, newList) {
      	let leftList = [...left], rightList = [...right];
      	// 按照奇偶分
		newList.forEach((item, index) => {
			if(index % 2 == 0){
				left.push(item)
			} else {
				right.push(item)
			}
		})
		return { leftList, rightList }
	  }
    },
})
// wcss

.list-vertical {
    display: flex;
    flex-direction: row;
    justify-content: space-between;
  }
  // 每个卡片有设定好的宽度

效果:
小程序里瀑布流布局的实现思路—— H5、PC也通用_第3张图片
总结:
优点

  • 每次获取新数据后,是按照一左一右分别追加到两侧的商品列表,基本上实现了商品列表的排版需求;

缺点:

  • 在每次拿回的数据数量一致,且每个卡片高度差别不大时可以使用,因为这个时候两侧的商品数量是相同的。
情况二:每次获取到的数据数量不确定

场景:
当搜索的商品涉及到算法的时候,因为算法的过滤去重等操作,每次拿到的数据数量就不一定了,仍然按照奇偶来划分就容易出现一侧的数据多一侧数据少。
思路:
沿用上面的思路,仍然按照奇偶拆分,但是在拆分商品数据的时候,先获取当前的左侧、右侧数组长度,优先从短数组的一侧开始插入。

const splitList = (_this, left, right, newList) => {
  let leftList = [...left],
    rightList = [...right];
	// 判断数组长度
  if (leftList.length <= rightList.length) {
    pushList(leftList, rightList, newList);
  } else {
    pushList(rightList, leftList, newList);
  }

  return {
    leftList,
    rightList
  };
};

function pushList(_left, _right, newList) {
  newList.forEach((item, index) => {
    if (index % 2 === 0) {
      _left.push(item);
    } else {
      _right.push(item);
    }
  });
}

总结:
上面的方法只是为了应付每次查到的数据数量不同,当商品数量差太多时,还是会出现排版的问题。甚至可以说,这不是解决方法(但我就是想记录下来)

尤其当每个卡片的高度差值变得不可控的时候,这个方法肯定是不行的;

这个时候就没啥简单好用的方法了,只能开始计算左右两列的高度,来决策新的一条数据到底插入哪里。

情况三: 元素之间高度差变大,每次的数量也可能不一样

场景:
这个时候如果只是插入简单的图片,或者说在插入可确定高度的新元素时,处理起来稍微舒服一点。

思路:

  • 在插入前计算当前的左侧和右侧的高度
  • 遍历新数据的高度,通过累加高度来判断下一个应该插入左侧还是右侧。

这个时候如果是在H5或者PC端就简单一些,用document.querySelector().getBoundingClientRect()获得容器高度,累加计算就完事了(或者不用获取dom高度,本地维护一个变量记录左、右两列的高度也可以。)

splitList() {
    // 或者本地维护一个left, right 值来记录左右两侧的高度
    //反正每个元素的padding,margin等是一样的,不影响计算的准确性
    let left = document.querySelector('#left-list').getBoundingClientRect(),
      right = document.querySelector('#right-list').getBoundingClientRect();
    goodsList.forEach(item => {
      let h = item.height;
      if (left <= right) {
        leftList.push(item);
        left += h;
      } else {
        rightList.push(item);
        right += h;
      }
    })
  }
Masonry插件

更省心舒服的方法,可以使用第三方插件 Masonry,配置化的实现瀑布流布局,简单好用。

Masonry算是很好用的瀑布流的插件了。
但是小程序里是不支持document.querySelector的,所以Masonry在小程序里就无能为力了

小程序的实现细节

小程序里要获得DOM元素的信息时,需要用小程序自带的wx.createSelectorQuery(),详细用法可以找官方文档⬇️
https://developers.weixin.qq.com/miniprogram/dev/api/wxml/wx.createSelectorQuery.html

直接上示例代码:

// 封装一个Behavior,在组件里通过设置behaviors: [basic]属性来使用
export const basic = Behavior({
    methods: {
        getRect(selector, all) {
            return new Promise(resolve => {
                wx.createSelectorQuery()
                    .in(this)[all ? 'selectAll' : 'select'](selector)
                    .boundingClientRect(rect => {
                    if (all && Array.isArray(rect) && rect.length) {
                        resolve(rect);
                    }
                    if (!all && rect) {
                        resolve(rect);
                    }
                })
                    .exec();
            });
        }
    }
});

// 获取元素高度
this.getRect(select).then(res => {
// 高度
	console.log(res.height);
})

如何获取元素高度?

  1. 小程序里图片有bindload可以获得图片高度,插入图片后获得到实际高度,累加到左侧或者右侧的列表高度
    在这里插入图片描述
  2. 通过wx.getImageInfo(Object object)在未加载图片之前获得高度
  3. 当然如果后端返回的数据里自带宽高信息是最好的。

2.3 项目中遇到的情况

  1. 商品列表的商品以卡片形式展示,可能有营销标签,商品名很长用两行展示;

  2. 因为1,所以每个卡片在渲染到页面之前是不知道具体高度的,只知道每个卡片的高度差不会太大;

  3. 商品通过算法过滤,每次拿到的商品数量不一定;

  4. 在小程序里的,尽量不要性能差的太明显

最后我想到的偷懒方法:

  • 每次渲染新数据前,获取当前左、右商品数组的长度,优先从数量少的数组开始插入;
  • 而且是把0~length - 2的数据插入,插入完成后,在this.setData()的回调里再获取一下左、右商品列表节点的高度,把最后一个商品插入短的那一列。

优点:

  • 不用每次插入商品时都计算高度,性能影响不大;
  • 在商品卡片高度差不大的这种情况下,通过最后一条商品来均衡左右列表高度,基本上可以做到瀑布流。

缺点:

  • 其实并没有做到真正的瀑布流,当每个卡片高度差变大后,还是不行的。

总结

以上主要是在实现商品列表是对瀑布流的实现方法的汇总,通过flex布局+js拆分数据来实现的简单瀑布流;

鉴于商品卡片里包含了图片,商品名,营销标签等内容,导致商品卡片高度不确定,只有渲染到页面上以后才能明确高度;而且每次拿到的商品数量也不确定,所以通过索引奇偶拆分的方法就行不通了,必须要计算一下左右商品列表的高度才行。

出于性能考虑,最后决定用末尾的一个商品来填充短的商品列表,尽可能的做到瀑布流的效果。

当然瀑布流里其实很有很多学问,比如还可以通过absolute定位来排列,或者高级一点的可以做到高度、宽度同时瀑布流,这就涉及到动态规划之类的算法了。

有更好的方法,欢迎补充~~

你可能感兴趣的:(日常总结,css,微信小程序)