动态分栏布局实现

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

神奇的ls命令

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

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

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

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

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

实现难点与解决方案

更多文章请关注公众号“Web学习社”。

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

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

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

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

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

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

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

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

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

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

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

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

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/ **
* 根据列数和名称长度进行分列,以二维数组的方式返回结果,如果超出宽度则返回空数组
* 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列的方式算起,那么大多数情况下前面的分列方式都不是最优解,所以我们考虑直接从最优解算起,找到最长的名称和最短的名称并假设它们在一列,如果不满足则减少一列。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 先取最理想的列数
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来实现。

1
2
3
4
5
6
7
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
}

具体代码:

http://jsbin.com/zipabig/1/edit?html,css,js,output


一部由众多技术专家推荐, 帮你成为具有全面能力和全局视野工程师的进阶利器—— 《了不起的JavaScript工程师》出版了! 点击下方链接即刻踏上进阶之路!


亚里士朱德 wechat
更多WEB技术分享请订阅微信公众号“WEB学习社”