jatfret / blog

Blog posts in the repository's issues
2 stars 0 forks source link

瀑布流布局,你会怎么排? #2

Open jatfret opened 6 years ago

jatfret commented 6 years ago

瀑布流布局是一种常见的页面布局的表现形式,当开发者谈到它的时候,往往会结合元素的排列方式、无限滚动、懒加载这些特点来综合描述。

瀑布流布局中文反译为“waterfall layout”,在 google 上搜索得到的页面布局相关结果并不是太多,并且这些结果里由国内开发者发布的内容比例较高。我们一般所说的瀑布流布局,更接近于“pinterest layout”或者 “masonry layout”,是由国外的一个创意搜索和图片分享类网站 pinterest 正式上线采用后,这种瀑布流布局被大家所熟知。pinterest 页面布局的特点为每个图片元素宽度相等,高度取决于图片的原始宽高比例,这样的好处是既显示完整图片又不用固定用户发布图片的宽高比。另外,也因为用这种方式布局的图文混排的内容元素,带来的视觉效果的比较新颖,后来在很多以图片为主要展示内容的网页上,有比较广泛的应用,例如花瓣网果壳科学人堆糖

本文所讨论的瀑布流布局不局限于 pinterest style 的元素等宽不一定等高的限制,不论每个元素对应的宽高是否相等,只要满足多个元素分为多列自上而下流式排列,就归类为瀑布流布局。根据每个元素对应的宽高是否相等,可以分为四种组合:等宽等高、等宽不等高、等高不等宽、既不等宽也不等高,每一个组合的元素排列的实现方式都不同,前三种情况的实现方法相对第四种较简单,也可以找到比较丰富的资料,这里只做简单的说明,针对第四种情况,因为既不等宽又不等高的元素按照简单的布局方式会出现大量的空白区域,文中会提出一个具体的算法实现来解决。

一、等宽等高

1509441516_77_w2656_h1490

dribbble是一个非常流行的设计师作品发布网站,它的作品列表展示页的布局方式采用的是等高等宽的瀑布流布局,这是最普遍的瀑布流,只需要用到前端开发者最基础的 css 知识,利用inline-block或者 float 的方式都可以实现。

二、等宽不等高

1509441555_91_w2782_h1364

pinterest 代表的等宽不等高的瀑布流排列,最方便的实现是直接调用 Masonry 插件,不过自己编写 js 代码来实现也不算复杂,另外利用元素等宽的特点,可将主容器拆分为多列,这样只用 css 的属性就可以实现这样的效果,下面分别来介绍如何用 js 和 css 来实现的等宽不等高的瀑布流排列效果的思路:

  1. js 的实现方法 首先需要确定网页应用的宽度和每个需要放置元素的宽度,通过计算可以得到布局的列数 C 。布局元素是从主容器的左上角开始依次向下放置,因此将左上角为坐标原点(0, 0),布局元素利用相对于主容器绝对定位的方式来确定位置。维护一个长度为 C 的数组 A,分别记录该列在垂直方向上的坐标,每放置一个元素时,遍历数组 A 得到垂直坐标最小数组元素的索引 i ,计算出坐标值并分别设置布局元素的 topleft 值,最后更新数组 A,将索引为 i 的数组元素的值加上布局元素的高度值。
  2. css 的实现方法 对于不需要考虑 css3 兼容性的页面应用,可以直接利用 flexbox 布局或者 multi-columns 布局来实现。flexbox 布局的思路是将设置主容器属性 display: "flex",排列方向设置为 flex-direction: "row",每一列用一个容器包裹,并设置该容器属性 display: "flex",排列方向设置为 flex-direction: "column",最后设置列容器的宽度值即可得到想要的布局效果。multi-columns 布局是通过设置主容器的属性 column-count 的数目来分隔出对应的列数,然后每个布局元素设置属性 break-inside: "avoid" 来避免元素跨列,只需要简单的连个属性设置就可以实现基本的布局效果,另外还可以设置主容器的 column-gap 来调整每列之间的间距。

三、等高不等宽 1509456168_47_w1613_h747

上图是 500px 图片列表页,另外还有百度图片搜索结果页,为典型的等高不等宽的瀑布流布局。这种排列方式主要的特点是每一行上图片的高度相等,图片按照设定的高度根据原始宽高比进行缩放,不会出现缩放变形的情况,因为检索到的图片尺寸各异,缩放后每行的图片元素个数可能不相等。js 实现的思路为:设定一个基准行高 H,每个图片元素的宽高比 R 乘以 H 然后累加,每次累加后的值 S 与主容器的宽度 W 做比较,若 S > W,则最后一张图片被放置到下一行, 之前已经累加的图片元素作为一个整体,进行等比拉伸,使其宽度等于主容器宽度,这样左右两边的元素都是对齐的,不会出现空白。若要在图片横向之间加入 margin 以区分不同的图片,则宽度 W 的值为主容器宽度减去所有横向 margin 值叠加的总和。

四、既不等宽又不等高 1509456954_49_w1585_h886

既不等宽又不等高的瀑布流布局,因为元素的宽和高都可能不相等,在视觉上缺少整体性和秩序感, 一般在图片为主内容的网页上较少使用。上图花了很长时间才找到,来自网站 ColRD 。这类的瀑布流排列方式,在网页上直接应用的不多,但是前端开发者在 css-sprite (就是大家说的“雪碧图”)上会经常见到,排除那些把所有 icon 都排成一行或一列的插件,考虑比较完整的插件一般会为节省最终生成的图片尺寸,为每个小 icon 尽量的利用空白区域。下图这张合并后的图片就是一个比较好例子: 另外,在一些网页上可能会间接应用到不等宽不等高的瀑布流布局。下图截自云顶分析平台首页的个人数据中心,用户在详情页可以自行添加感兴趣的图表到首页,这些图表的宽度和高度是不确定的,同时每个图表都具有位置拖拽,宽高缩放的功能。 这里用到一个网格布局插件 react-grid-layout,来实现图表拖拽和缩放的功能。插件要求为每个网格元素——单个图表,传入相对于左上角的原点(0, 0)的 xy坐标,为了得到这两个坐标值,就需要计算每个图表的应该摆放的位置,而这就是一个既不等宽有不等高的瀑布流布局排列问题。通过查找资料发现该类问题在算法上早有人研究,称作 集装箱放置优化问题,就是如何把很多小盒子放到一个大盒子里并尽可能少留空隙。

对于网页应用来说,问题简化为在 2D 空间里,如何把小矩形放到一个大矩形容器并尽可能少留空隙。这篇文档 Packing Lightmaps 提出一种区域划分的方法,随着矩形的不断的放入,区域也不断的拆分,过程如下图所示: 1509458625_73_w615_h201

在主容器 R 里放入矩形 A 后,主容器区域被拆分为 A 右侧区域(Aright)和下侧区域(Abottom),放置矩形 B 时, 由于 B 的宽度大于 Aright 宽度, Abottom 区域被占用,同时矩形 B 将原 Abottom 拆分为 Bright 和 Bbottom,在放入矩形 C 时会先与 Aright 区域比较宽高,若空间不够,则依次比较 Bright、Bbottom,直到找到合适的空间,放置后也是按照规则拆分出右侧和下侧区域。 由上图可以看出,这是一个类似于一个构建二叉树的问题,每插入一个子叶的过程,都从根结点开始遍历,先从左子叶开始比较有没有合适的区域位置,若没有,再到右子叶开始比较,最终得到一个放置点。有了这个思路,就可以用 js 来实现它了:

class BST{
  constructor(rects, width, height){
    this.root = {
      w: width,
      h: height,
      x: 0,
      y: 0
    }
    this.rects = rects
    this.width = width
    this.height = height
    this.packedRects = []
  }
  splitContainer(container, rect){
    container.used = true
    container.right = {
      w: container.w - rect.w,
      h: rect.h,
      x: container.x + rect.w,
      y: container.y
    }
    container.bottom = {
      w: container.w,
      h: container.h === undefined ? undefined : container.h - rect.h,
      x: rect.x,
      y: container.y + rect.h
    }
    return container
  }
  traversel(container, rect){
    if(container.used){
      return this.traversel(container.right, rect) || this.traversel(container.bottom, rect);
    }else if(rect.w <= container.w && (container.h === undefined ? true : rect.h <= container.h)){
      return container
    }else{
      return null
    }
  }
  packingRects(){
    this.rects.forEach((rect, index)=>{
      const container = this.traversel(this.root, rect)
      if(container){
        rect.x = container.x
        rect.y = container.y
        this.packedRects.push(rect)
        this.splitContainer(container, rect)
      }
    })
  }
  getPacked(){
    this.packingRects()
    return this.packedRects
  }
}

/**
*  @params rects  Array  eg: [{w: 2, h: 2}, {w: 1, h: 1}]
*  @params width  Number eg:  3
*  @params height Number eg: 100
*  output Array eg: [{w:2, h:2, x: 0, y: 0}, {w: 1, h: 1, x: 2, y: 0}]
*/
export function BinPacking2D(rects, width, height) {
  if(rects && width){
    const packingInstance = new BST(rects, width, height)
    return packingInstance.getPacked()
  }
}

通过调用暴露出来的 BinPacking2D 方法,传入由待布局元素的宽高组成的数组,可得到每个布局元素的 xy 坐标值。


以上针对布局元素的对应宽高是否相等的四种情况,分别介绍了瀑布流布局排列的实现思路。每种情况下的实现方式除了上面所提到的,肯定还有更多的方法,欢迎大家一起讨论,这篇文章就算抛砖引玉了。

SaltFish-X commented 6 years ago

最近正在研究瀑布流, multi-columns 布局,实质是多列文章布局看似瀑布流 但是我们期待的瀑布流应该是 1 2 3 4 5 6 7 8 而 multi-columns 布局的结果是 1 3 5 7 2 4 6 8 这样的,实际操作中不建议使用css进行瀑布流 (flex 也会做出类似的效果,不建议使用)