-
Notifications
You must be signed in to change notification settings - Fork 1
Description
瀑布流,又叫瀑布流式布局。是一些图片网站常用的布局,主要的特点是:图片等宽,竖直方向上参差不齐。
静态效果如下:
要想实现这个效果,只用 CSS 是很难的,但不是不可以。纯 CSS 实现这里先不讨论。
开始之前,需要准备好一些图片,图片要求:宽度要相等,高度随意。
实现原理是:给每个图片设置绝对定位,然后动态改变这些图片的 left 和 top 即可实现自动排列。这里重点是如何动态改变这些图片的 left 和 top。
计算列数
第一行图片是瀑布流的基础,也是实现的关键。所以从第一行开始说起:
计算图片列数:图片列数 = 可视区的宽度 / 图片的宽度:
再加上图片之间的间隙:图片列数 = 可视区宽度 / (图片宽度 + 间隙宽度):
排列行
第一行图片 top 都是 0,所以只要动态设置每张图片的 left 就可以排列好:left = (imgWidth + gap) * i
第一行排好后,排列第二行:
- 首先准备一个数组
arr用于储存每一列的总高度。 - 将第一行
图片的高度(通过img.offsetHeight获取)存入数组:arr[h1, h2, h3, h4, h5, h6]。 - 找出数组中高度最小的那一项,并记录它的索引
index。然后把第二行的第一张图片放到这张图片后面。 - 第二行第一张图片位置为:
left = arr[index].offsetLeft。(因为同一列中的图片left是一样的)
top = arr数组中的最小高度 + 间隙大小
这时候会发现,后面所有的图片都叠在了第二行的第一张图片那里:
这是因为,在第二行加入第一张图片后,并没有更新数组中的数值。现在,只需要更新一下最小列的高度即可。
每次向高度最小的列后面添加一张图片后,更新一下数组中储存的这列的高度值,就实现了图片的自动排列,从而实现瀑布流效果。
全部代码如下:
HTML:
View Content
<div id="imgs-wrapper" class="imgs_wrapper">
<img src="./imgs/1.jpg" alt="water_fall_img" />
<img src="./imgs/2.jpg" alt="water_fall_img" />
<img src="./imgs/3.jpg" alt="water_fall_img" />
<img src="./imgs/4.jpg" alt="water_fall_img" />
<img src="./imgs/5.jpg" alt="water_fall_img" />
<img src="./imgs/6.jpg" alt="water_fall_img" />
<img src="./imgs/7.jpg" alt="water_fall_img" />
<img src="./imgs/8.jpg" alt="water_fall_img" />
<img src="./imgs/9.jpg" alt="water_fall_img" />
<img src="./imgs/10.jpg" alt="water_fall_img" />
<img src="./imgs/11.jpg" alt="water_fall_img" />
<img src="./imgs/12.jpg" alt="water_fall_img" />
<img src="./imgs/13.jpg" alt="water_fall_img" />
<img src="./imgs/14.jpg" alt="water_fall_img" />
<img src="./imgs/15.jpg" alt="water_fall_img" />
<img src="./imgs/16.jpg" alt="water_fall_img" />
<img src="./imgs/17.jpg" alt="water_fall_img" />
<img src="./imgs/18.jpg" alt="water_fall_img" />
<img src="./imgs/19.jpg" alt="water_fall_img" />
<img src="./imgs/20.jpg" alt="water_fall_img" />
</div>CSS:
View Content
* {
margin: 0;
padding: 0;
}
body {
background: #eee;
}
.imgs_wrapper img {
position: absolute;
width: 236px;
}JavaScript:
View Content
window.onload = function() {
var oWrapper = document.getElementById("imgs-wrapper");
var aImgs = oWrapper.getElementsByTagName("img");
waterFall(); // 初始化
function waterFall() {
var nGap = 10; // 图片间隙
var nImgWidth = aImgs[0].offsetWidth; // 图片宽度
var nClientWidth = getClient().width; // 可视区宽度
var nColumns = parseInt(nClientWidth / (nImgWidth + nGap)); // 列数
var aImgHeights = []; // 储存每列图片的总高度
for (var i = 0; i < aImgs.length; i++) {
var nImgHeight = aImgs[i].offsetHeight; // 当前图片的高度
if (i < nColumns) {
// 第一行图片
aImgs[i].style.top = 0;
aImgs[i].style.left = (nImgWidth + nGap) * i + "px";
aImgHeights.push(nImgHeight); // 储存当前图片的高度
} else {
var nMinHeight = getMinNum(aImgHeights); // 获取数组中最小高度
var nMinIndex = aImgHeights.indexOf(nMinHeight); // 最小高度的索引(即图片所在的列)
var nImgLeft = aImgs[nMinIndex].offsetLeft;
var nImgTop = aImgHeights[nMinIndex] + nGap;
// 执行动画
startMove(aImgs[i], {
left: nImgLeft,
top: nImgTop
});
// 添加新的图片后,更新数组中的最小高度
aImgHeights[nMinIndex] = nMinHeight + nGap + nImgHeight;
}
}
}
/**
* 获取数组中的最小数
* @param {Array} arr 数字数组
*/
function getMinNum(arr) {
return Math.min.apply(Math, arr);
}
// 获取页面可视区的宽高
function getClient() {
return {
width:
window.innerWidth ||
document.documentElement.clientWidth ||
document.body.clientWidth ||
0,
height:
window.innerHeight ||
document.documentElement.clientHeight ||
document.body.clientHeight ||
0
};
}
/**
* 获取某个属性的值
* @param {Object} elem 当前元素
* @param {String} attr 属性
*/
function getStyle(elem, attr) {
if (window.getComputedStyle) {
return window.getComputedStyle(elem, null)[attr];
} else {
return elem.currentStyle[attr];
}
}
/**
* 运动函数
* @param {Object} elem 当前元素
* @param {Object} json 要操作的属性 // 传透明度属性时,单位为100
* @param {Function} fn 回调函数
* @param {Number} slow 动画的缓慢程度 // 数值越大,动画越迟缓。
* @param {Number} time 每次动画执行时间
*/
function startMove(elem, json, fn = null, slow = 10, time = 15) {
clearInterval(elem.timer);
elem.timer = setInterval(function() {
var bStop = true;
for (var attr in json) {
var iCur = 0;
var target = json[attr];
if (attr === "opacity") {
iCur = getStyle(elem, attr) * 100 || 1;
} else {
iCur = parseInt(getStyle(elem, attr)) || 0;
}
var iSpeed = (target - iCur) / slow;
iSpeed = iSpeed > 0 ? Math.ceil(iSpeed) : Math.floor(iSpeed);
iCur += iSpeed;
if (attr === "opacity") {
elem.style.filter = "alpha(opacity=" + iCur + ")";
elem.style[attr] = iCur / 100;
} else if (attr === "zIndex") {
elem.style.zIndex = target;
} else {
elem.style[attr] = iCur + "px";
}
iCur != target && (bStop = false);
}
if (bStop) {
clearInterval(elem.timer);
fn && fn();
}
}, time);
}
};这里用了一个运动函数 startMove,这个函数使用很简单,我们只需要把 操作的对象 和 运动时要操作的属性与值 传给它。它就会自动完成这个动画。
效果如下:
响应浏览器窗口大小
到这里我们知道了,只要执行 waterFall 方法,图片就会自动去找自己的位置。所以如果想要图片的位置随着窗口大小的改变而改变,只需要加上这些代码:
window.addEventListener("resize", waterFall);效果如下:
但这样是有问题的,当浏览器窗口触发 resize 事件时,直接执行 waterFall 方法会造成性能问题(因为浏览器的 resize 事件会在短时间内触发很多次),所以需要使用防抖函数进行改进。这里涉及了 JS 中的节流、防抖的概念,具体实现如下:
/**
* 防抖函数
* @param {Object} func 要执行的函数
* @param {Number} wait 间隔时间
*/
function debounce(func, wait) {
var timer;
return function() {
var context = this;
var args = arguments;
clearTimeout(timer);
timer = setTimeout(function() {
func.apply(context, args);
}, wait);
};
}然后修改代码:
- window.addEventListener('resize', waterFall);
+ window.addEventListener('resize', debounce(waterFall, 1000));效果如下:
这里为了演示效果,我将防抖时间设置为 1 秒,实际使用时设置为 300 毫秒左右即可。
一个很好的线上例子:Pinterest 是一个瀑布流图片网站,同样也对响应浏览器窗口大小进行了防抖处理。
节流、防抖函数参考文章:JavaScript 专题之跟着 underscore 学节流
懒加载效果
这个例子中,我用了 20 张图片来举例,实际中的图片都是成百上千张,不可能一下子全部加载。所以就需要使用 懒加载,即:当页面显示最后一张图片时,再去加载一批新的图片。
那么如何判断页面有没有显示最后一张图片呢?
这里通过最后一张图片的 offsetTop 值来判断,当 页面滚动的距离 + 页面可视区的高度 > 最后一张图片的offsetTop 时,证明页面中显示了最后一张图片:
代码如下:
// 模拟获取到的 Ajax 数据
var newImgs = [
'./imgs/1.jpg',
'./imgs/2.jpg',
......
'./imgs/20.jpg'
];
window.onscroll = function() {
if (getClient().height + getScrollTop() >= aImgs[aImgs.length - 1].offsetTop) {
newImgs.forEach(function(item) {
var oImg = document.createElement('img');
oImg.src = item;
oImg.alt = "new_img";
oWrapper.appendChild(oImg);
});
}
// 延迟加载图片,否则图片还没显示出来时,获取不到图片的高度
setTimeout(function() {
waterFall();
}, 1000);
};
function getScrollTop() {
return window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0
}上面的代码还有一点问题:鼠标滚轮滚动时 onscroll 事件会被多次触发。试想一下,这里的代码如果多次被触发会怎样?页面中会瞬间加载很多很多图片(假如 onscroll 事件触发一次会加载 20 张图片,鼠标滚动一下触发了 10 次 onscroll 事件,那么一下就会加载 200 张图片,这样是很糟糕的)这个问题可以用上面提到的防抖函数进行处理。不过这里只是用布尔变量简单处理一下:
// 模拟获取到的 Ajax 数据
var newImgs = [
'./imgs/1.jpg',
'./imgs/2.jpg',
......
'./imgs/20.jpg'
];
+ var bFlag = false;
window.onscroll = function() {
+ if (bFlag) { return; }
if (getClient().height + getScrollTop() >= aImgs[aImgs.length - 1].offsetTop) {
+ bFlag = true;
newImgs.forEach(function(item) {
var oImg = document.createElement('img');
oImg.src = item;
oImg.alt = "new_img";
oWrapper.appendChild(oImg);
});
}
// 延迟加载图片,否则图片还没显示出来时,获取不到图片的高度
setTimeout(function() {
waterFall();
}, 1000);
};
function getScrollTop() {
return window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0
}至此,瀑布流效果已经基本实现。还有一些不足,可以自行尝试改进或者使用 Ajax 获取数据来模拟真实的场景。
Demo 体验地址:https://liuyib.github.io/blog/demo/blog/water-fall/
以上 🚀








