图片的横向瀑布流,其实简单地按顺序排列就可以了
但要实现每行中各图片都等高(各行不一定等高,但每行里面等高),且每行都占满,就需要用到flex的特性了
控制每行图片高度都一致,可能会影响图片的比例,所以不能简单暴力地设置高度,需要按比例来动态计算
另外,如要限制图片展示的行数,则只需判断好每行总高度与容器总高度的关系即可
这里就来实现一下这个小功能
因为都是假数据的关系,图片的宽高值是随机数,并非原图宽高值,仅作参考
看完上面那张大大的图,先想一下可以怎么实现..
要实现每行都能够占满,需要用到 flex-grow 这个属性
flex-grow基于flex-basis基准值来计算,而flex-basis则基于项目的width、min|max-width相关的值来计算,或者手动定义
使用flex-grow可以分配按比例分配主轴的剩余空间
如果有10张图片需要放置,第一行仅可以放置四张图片,剩余100px的空间,那么各图片的flex-grow可以直接配置成图片的宽度width值,即可很方便精准地分配好这剩余的空间
第二行可以放五张图片,剩余N px的空间... 按照这种计算方式来铺满每一行
<h1 class="get-latest-update"> <a href="javascript:;">获取最近更新</a> </h1> <div class="img-items"></div> <script type="text/template" id="img-item-tpl"> <div class="img-item" style="flex-grow: {{width}}; width: {{width}}px;"> <a href="#/img/{{id}}" style="padding-top: {{paddingTop}}%;"> <img data-src="{{src}}" src="{{src}}" width="100%" height="100%"> </a> </div> </script>
上面页面模板中,flex-grow 与 width的值一致,用以按比例分配每行剩余空间
另外可以看到这里有个 padding-top 的百分比值
我们都知道 padding-top 的百分比值是基于父元素的宽度来计算的,根据盒模型,一般这种计算方式是为了获取固定宽高比
当父元素有宽度,但高度为0时,整体高度则由padding-top值来撑开,则父元素就有了一个设定的宽高比,
同时我们将子元素(这里是图片)position值设置为absolute,宽高占满父元素,则子元素图片也有了一定的宽高比,实现按比例的图片缩放
来看看对应的样式设置
body { background-color: #f2f2f2; } .get-latest-update { font-size: 20px; cursor: pointer; > a { color: #0183fd; text-decoration: none; } } .img-items { display: flex; flex-wrap: wrap; overflow: hidden; } .img-item { margin-right: 10px; margin-bottom: 10px; background-color: #fff; box-shadow: 0 0 10px #ddd; > a { position: relative; display: block; width: 100%; } img { position: absolute; top: 0; left: 0; width: 100%; height: 100%; } }
那么,这个width和padding-top的该怎么计算出来呢
核心代码是
// 图片预定义的高度 var baseHeight = 200; for (var i = 1; i <= num; ++i) { var w = getRandom(width.min, width.max); var h = getRandom(height.min, height.max); imgs.push({ id: i, src: imgSrcBase[Math.floor(i / 10)] + (i % 10 + 1) + '.jpg', // 设置图片的宽度,需根据预定义的高度值来做好比例处理 // 为了让每行各图片按自身宽度自动flex-glow,同时利用这个比例处理保证每行图片的高度一致 width: w * baseHeight / h, height: h, // padding-top的百分比,用以基于父元素宽度设置该元素的高度 // 为了保证图片宽高按比例 paddingTop: h / w * 100 }); }
paddingTop的值,按照以下这个映射关系来看就好
容器高度 == 容器宽度 * paddingTop %
最终会形成
容器高度 == 图片高度
容器宽度 == 图片宽度
所以
图片高度 == 图片宽度 * paddingTop %
width值的计算可能比较绕
假设这里 width直接取 图片宽度w值,就会出现一行中图片高度不一致的情况
因为最终的图片高度即为容器的高度,而容器的高度是由容器宽度决定的(注意这里的paddingTop值已经确定),而容器宽度就是由这里的width来决定的。图片宽度的不同,就直接导致了最终高度的不同
所以,为了确保图片高度一致,假设有三张图片 50*50 100*100 50*150 放在了同一行中,flex布局会将三张图片所在容器的高度自适应为最高的那个150,如果flex-grow值起作用了,这个最高值还会再多一些
我们可以考虑最简单的情况,正好放满一行。那么最终三张图片的高度都应该为150,按照各自的图片比例来调整,则最终第一张图片宽度的计算 50 / 50 === width / 150 , 则 width = 50 / 50 * 150
可能有些行最高的图片还是不够高,为了也能够显示出比较大的图片,我们还可以定义好这个基准高度值,比如 baseHeight设置为 200
所以,最终每一张图片的宽度width值为 w / h * baseHeight
还要一个问题,如何实现只显示三行
显示三行,每行的图片数量不固定,这是通过flex布局自动排列每一行的,都会经过 基本排列 -> 分配剩余空间 的步骤
目前想到的方法是对每一行的容器所占位置进行累加,最后对比即可
不过这种方式会有比较大的性能损耗,看还能不能有更优雅的做法吧
// 设置显示的图片行数 function setLineLimit(num) { // 内容区宽度 var contentWidth = $('.img-items').outerWidth(); // 定义的外边距 var marginWidth = 10; // 每行宽度 var curWidth = 0; // 行标识 var lineIndex = 1; // 初始需将图片设置为可见,否则flex无法自适应排版 $('.img-item').show() .each(function() { var $item = $(this); var itemWidth = $item.outerWidth(); // 隐藏多余的行 if (lineIndex > num) { $item.hide(); return; } $item.show(); // 某一行 if (curWidth + itemWidth + marginWidth <= contentWidth + marginWidth) { curWidth += itemWidth + marginWidth; } // 下一行 else { ++lineIndex; curWidth = itemWidth; if (lineIndex > num) { $item.hide(); } } }); }
主要注意的点是,为了兼顾视窗缩放的过程中,自动排列也能照常进行,在计算的时候需要将每个项先显示出来,再进入计算环节
// 视窗缩放时处理可视的图片 $(window).resize(throttle(setLineLimit.bind(this, 3), 200));
完整JS代码
1 // 事件绑定 2 function addEvent(elem, type, handler) { 3 elem.addEventListener(type, handler, false); 4 } 5 6 function qs(selector) { 7 return document.querySelector(selector); 8 } 9 10 function qsa(selectors) { 11 return document.querySelectorAll(selectors); 12 } 13 14 // 函数节流,频繁操作中间隔 delay 的时间才处理一次 15 function throttle(fn, delay) { 16 delay = delay || 200; 17 18 var timer = null; 19 // 每次滚动初始的标识 20 var timestamp = 0; 21 22 return function () { 23 var arg = arguments; 24 var now = Date.now(); 25 26 // 设置开始时间 27 if (timestamp === 0) { 28 timestamp = now; 29 } 30 31 clearTimeout(timer); 32 timer = null; 33 34 // 已经到了delay的一段时间,进行处理 35 if (now - timestamp >= delay) { 36 fn.apply(this, arg); 37 timestamp = now; 38 } 39 // 添加定时器,确保最后一次的操作也能处理 40 else { 41 timer = setTimeout(function () { 42 fn.apply(this, arg); 43 // 恢复标识 44 timestamp = 0; 45 }, delay); 46 } 47 } 48 } 49 50 // 获取随机数 51 function getRandom(min, max) { 52 return Math.round(Math.random() * (max - min + 1) + min); 53 } 54 55 // 构造图片数据 56 function createMockImgs(num) { 57 var imgs = []; 58 59 // 图片宽高数据范围 60 var width = { 61 min: 50, 62 max: 200 63 }; 64 65 var height = { 66 min: 150, 67 max: 300 68 }; 69 70 // 图片源 71 var imgSrcBase = [ 72 'http://www.deskcar.com/desktop/movietv/2009/2009227225145/', 73 'http://www.deskcar.com/desktop/fengjing/2017418153624/', 74 'http://www.deskcar.com/desktop/fengjing/2017418153624/', 75 'http://www.deskcar.com/desktop/fengjing/2017418153624/', 76 'http://www.deskcar.com/desktop/fengjing/2017418153624/', 77 'http://www.deskcar.com/desktop/else/20161228125639/', 78 'http://www.deskcar.com/desktop/fengjing/2017418153446/' 79 ]; 80 81 // 图片预定义的高度 82 var baseHeight = 200; 83 84 for (var i = 1; i <= num; ++i) { 85 var w = getRandom(width.min, width.max); 86 var h = getRandom(height.min, height.max); 87 88 imgs.push({ 89 id: i, 90 src: imgSrcBase[Math.floor(i / 10)] + (i % 10 + 1) + '.jpg', 91 // 设置图片的宽度,需根据预定义的高度值来做好比例处理 92 // 为了让每行各图片按自身宽度自动flex-glow,同时利用这个比例处理保证每行图片的高度一致 93 width: w * baseHeight / h, 94 height: h, 95 // padding-top的百分比,用以基于父元素宽度设置该元素的高度 96 // 为了保证图片宽高按比例 97 paddingTop: h / w * 100 98 }); 99 } 100 101 return imgs; 102 } 103 104 // 视窗缩放时处理可视的图片 105 $(window).resize(throttle(setLineLimit.bind(this, 3), 200)); 106 107 // 设置显示的图片行数 108 function setLineLimit(num) { 109 // 内容区宽度 110 var contentWidth = $('.img-items').outerWidth(); 111 // 定义的外边距 112 var marginWidth = 10; 113 // 每行宽度 114 var curWidth = 0; 115 // 行标识 116 var lineIndex = 1; 117 118 // 初始需将图片设置为可见,否则flex无法自适应排版 119 $('.img-item').show() 120 .each(function() { 121 var $item = $(this); 122 var itemWidth = $item.outerWidth(); 123 124 // 隐藏多余的行 125 if (lineIndex > num) { 126 $item.hide(); 127 return; 128 } 129 130 $item.show(); 131 132 // 某一行 133 if (curWidth + itemWidth + marginWidth <= contentWidth + marginWidth) { 134 curWidth += itemWidth + marginWidth; 135 } 136 // 下一行 137 else { 138 ++lineIndex; 139 curWidth = itemWidth; 140 141 if (lineIndex > num) { 142 $item.hide(); 143 } 144 } 145 }); 146 } 147 148 var mockImgs = createMockImgs(60); 149 150 console.log(mockImgs); 151 152 // 点击渲染 153 addEvent(qs('.get-latest-update'), 'click', function() { 154 renderList(mockImgs); 155 setLineLimit(3); 156 }); 157 158 var itemTpl = qs('#img-item-tpl').innerHTML; 159 var itemsDOM = qs('.img-items'); 160 161 /** 162 * 渲染数据 163 * @param {[type]} data [description] 164 * @return {[type]} [description] 165 */ 166 function renderList(data) { 167 var html = ''; 168 var fragment = document.createDocumentFragment(); 169 170 data.forEach(function(item) { 171 var divTemp = document.createElement('div'); 172 173 // 模板替换 174 divTemp.innerHTML = itemTpl.replace(/{{(\w+)}}/g, function(input, match) { 175 return match ? item[match] || '' : ''; 176 }); 177 178 fragment.appendChild(divTemp.firstElementChild); 179 }); 180 181 // 渲染 182 itemsDOM.appendChild(fragment); 183 }