猿论
首发于猿论
动态分栏布局实现

动态分栏布局实现

最近项目上要在浏览器端实现一个类似 Linux 下 ls 命令的显示效果。虽然实现过程没花多长时间,但是觉得这个效果还挺赞的,所以分享一下。

<!-- more -->


神奇的ls命令

ls命令对于前端同学可能比较陌生,简单来说就是Linux终端中的一个常用功能,用来显示某个目录下的文件(夹)列表。不过这它的功能不是本文讨论的重点。重点是它神奇的显示效果,比如下面这样:



看似并无神奇之处,就是文件(夹)名称逐行显示而已,别急,再来看下面两张截图:





不知道你现在有没有发现布局发生了变化:由最初的1列变成了多列!
你很容易想到列数量会随着窗口的宽度而改变,这种猜测是对的,但并不全面,因为决定其显示排列的还有文件(夹)名本身的长度。比如我再指定目录执行一条 ls 命令进行对比:



两条命令执行时窗口宽度是一样的,但是在长文件(夹)名的情况下是3列,而短文件(夹)下是4列。

ok~了解完ls命令的显示特点之后问题来了,我们该怎么实现呢?


实现难点与解决方案

这种列数发生变化的情况在网站页面中并不少见,你可能第一个就会想到媒体查询或者Bootstrap。但很遗憾事情并没有这么简单。媒体查询和Bootstrap都是针对不同屏幕宽度进行的自适应,而并不会根据内容自动调整,无法满足我们的要求。


似乎浮动可以根据内容大小来进行自动排列,但和我们的需求相比也有一个很大的差别。那就是ls命令在调整好合适的列数之后,每列文件(夹)名都是等宽左对齐的。而使用浮动后的效果是每行长的名称排列少,短的名称排列多,造成列数不统一,参差不齐。


到此看似毫无头绪,不过可以参考“把大象放进冰箱里”分几步的例子,分步骤来解决这个问题。这个问题我们其实可以分两步进行分析:

  1. 显示的列数需要根据窗口大小和名称长度两者共同决定,按照已知的使用css的方式无法解决。那么我们只能用最笨的方式进行动态计算。
  2. 列数确定以后要确保每列等宽等间距,且左对齐,这个比较容易实现,可以设置等宽等间距。

按照这个思路我们来实现第1步,那就是怎么计算出合适的列数。看看列数是由哪些因素决定的:

  • 窗口的宽度。
  • 名称的长度。
  • 名称之间的横向间距。


窗口的宽度可以通过dom获取或者样式设定,名称宽度可以通过fontSize进行计算,一般一个中文字符宽度为一个fontSize值,而英文数字则为一半,这里我们为了方便计算,只考虑英文和数字的情况。横向间距则可以由样式设定。那么可以得出计算公式:

窗口宽度 >= 列数 * (名称长度 * fontSize/2 + 间距)


这个不等式并不准确,因为当列数等于1的时候是最有可能满足这个不等式的,但显然不是我们想要的结果,所以还要加上另一个不等式:

窗口宽度 < (列数 + 1) * (名称长度 * fontSize/2 + 间距)


满足这两个条件的列数才是我们的最优解。不等式右边的表达式实际上就是行宽,更准确的说是每一行的行宽。当然这个表达式其实也不严谨,更严谨的是下面这样:

窗口宽度 >= (名称长度1 * fontSize/2 + 间距) + ... + (名称长度n * fontSize/2 + 间距)
窗口宽度 < (名称长度1 * fontSize/2 + 间距) + ... + (名称长度n+1 * fontSize/2 + 间距)


代码如下:

/ **
  * 根据列数和名称长度进行分列,以二维数组的方式返回结果,如果超出宽度则返回空数组
  * list {Array} 待分列的名称数组
  * colnum {Number} 列数
  * size {Number} 字体大小
  * width {Number} 用父容器的宽度替代前面所说的窗口宽度
  * spaceWidth {Number} 间距
  */
var subfield = function(list, colnum, size, width, spaceWidth) {
  // 使用lodash的chunk函数将数组按照列数分割成二维数组
  var result = _.chunk(list, Math.min(colnum, list.length))
  var rowWidth = 0
  // 逐列计算宽度
  for(var col=0;col<result[0].length;col++) {
    colWidth = 0
    for(var row=0; row<result.length; row++){
      var item = result[row][col] || ''
      // 每列宽度取决于最长的名称宽度
      colWidth = Math.max(colWidth, item.length * size / 2 + spaceWidth)
    }
    rowWidth += colWidth
  }
  return rowWidth < width ? result : []
}


有了上面的不等式,那么现在我们就可以开始逐行计算了,不过我们从分几列开始算起?如果从分1列的方式算起,那么大多数情况下前面的分列方式都不是最优解,所以我们考虑直接从最优解算起,找到最长的名称和最短的名称并假设它们在一列,如果不满足则减少一列。

// 先取最理想的列数
var colnum = Math.max(1, Math.floor((containerWidth - maxFile.length * fontSize / 2 - spaceWidth) / (spaceWidth + fontSize / 2 * minFile.length)))
var result = []
// 按照递减方式直到找到最优解
while (colnum > 0 && result.length===0) {
  if (colnum === 1) {
    result = []
    for(var i=files.length;i>0;i--) {
      result.push([files.shift()])
    }
  } else {
    result = subfield(files, colnum, fontSize, containerWidth, spaceWidth)
  }
  colnum--
}


至此,我们就完成了第1步。第2步这种布局方式其实很常见,其实就是古老的table布局,那么我们渲染到页面的时候直接使用table来实现。

var render = function(list) {
  var str = ''
  list.forEach(function(row) {
    str += '<tr><td>'+ row.join('</td><td>') + '</td></tr>'
  })
  document.querySelector('.ls table').innerHTML = str
}


具体代码:

jsbin.com/zipabig/1/edi




作者:亚里士朱德

链接:imooc.com/article/35632

来源:慕课网

本文原创发布于慕课网 ,转载请注明出处,谢谢合作


推荐阅读:

【重磅】认证作者招募 | 打造个人品牌 so easy !

活动 | 618重磅福利来袭,一大波限时优惠券发放中,速领!!

记一次Node项目的优化

原生ES-Module在浏览器中的尝试

关于RecyclerView你知道的不知道的都在这了(上)

编辑于 2018-06-14

文章被以下专栏收录

    “只整码,不整事”。程序员界依据所用的不同语言划分不同帮派,会出现各种自黑互黑现象,这个专栏希望能进行正向的知识传播和分享,不相互拆台,不互怼,和谐友好进行交流。