看到手机相册,突发奇想:能不能用实现一个原生的页面“相册”?或者说,传统网页中怎么实现图片预览功能?
如果在原生网页中使用插件,那必不可少的要引入一堆东西(并不是鄙视插件,只是为了引入下文,见谅嘿嘿);但又不是说所有的页面都要用框架…
经过一番探索,终于大概实现了想要的功能:
大概流程就是:可以点开大图观看、可以左右滑动切换、进入预览时可以从你当前点击的那一张开始浏览。
实现相册初始展示页
如上所示,我们可以在Header头中添加Viewport配置 —— 移动端页面常备:
<meta name="viewport" content="width=device-width, initial-scale=1">
然后在Body元素中添加小相片列表,其HTML如下:
<div class="gallery">
<div class="item">
<img src="images/39.jpg" alt="1">
</div>
<div class="item">
<img src="images/download.png" alt="2">
</div>
<div class="item">
<img src="images/nan.png" alt="3">
</div>
<div class="item">
<img src="images/nan2.png" alt="4">
</div>
<div class="item">
<img src="images/timg.jpg" alt="5">
</div>
</div>
现在页面上是一张张大小不一的图片凌乱排列,然后我们给它添加样式:
.gallery{
width:100vw;
display: flex;
flex-flow: row wrap;
}
.gallery .item{
width:calc(100vw / 3);
overflow: hidden;
}
.gallery .item img{
width: 100%;
height: 100%;
}
熟悉笔者的都知道:笔者提倡“尽可能的使用CSS去解决问题!”。所以:我们这里就要考虑这样一个问题:预览时页面的排布和原来有什么不同之处?
我的思路是:开始时总的宽度是100vw,子元素(相册图片)以flex排列;点击某一个图片预览时动态给父元素添加一个类名,这个类的作用是:将父元素下(单张)图片的最大宽度设为100vw(子元素从始至终都不设宽)。然后在js中根据子元素的长度计算其“真正宽度”(.style.width
)。并根据当前点击的是第几个子元素计算应该transform偏移多少距离:
.gallery.preview{
background-color: gray;
}
.gallery.animation{
-webkit-transition: 1s ease;
-moz-transition: 1s ease;
-o-transition: 1s ease;
transition: 1s ease;
}
.gallery.preview .item{
display: flex;
margin: auto;
align-items: center;
width: 100vw;
height: 100vh;
overflow: hidden;
}
.gallery.preview .item img {
max-width: 100vw;
max-height: 100vh;
width: initial;
height: initial;
}
根据上面的思路,在HTML页面加入脚本,监听Click事件:用户单击相片时,将相册切换为预览模式:
var $gallery = document.querySelector(".gallery");
$gallery.addEventListener("click", function (e) {
// 监听单击事件,切换相册的CSS class来实现预览和普通模式的切换
var classList = $gallery.classList,
css_preview = "preview";
if (classList.contains(css_preview)) {
classList.remove(css_preview)
// 在非预览模式下,相册的宽度为100vw
$gallery.style.width = 100 + "vw";
//【1】
} else {
classList.add(css_preview);
// 进入在预览模式,所有的图片横着拍成一排,相册的总宽度为每一个项目长度总和。
$gallery.style.width = 100 * itemsLength + "vw";
//【2】
}
});
在预览模式下,通过设置gallery样式类元素的宽度,让相册图片排成一排,然后通过CSS3的transform属性,设置元素的偏移量,移动整个元素位置,使得需要展示的图片出现在屏幕主区域。
在开始移动之前,我们要先禁止掉浏览器默认的触摸行为 —— 用CSS来做:
//为类“.gallery.preview”添加属性
touch-action: none;
然后去监听touchstart、touchmove及touchend事件来实现手势滑动功能
这三个事件很是常用,但其实他们的原理很简单,总结来说就是:在start(刚按下)时记录此时的手指位置——作为初始值;在move(触摸滑动)时根据实时的手指位置和初始手指位置变量实现要求判断;在end(手指离开)时(也有直接在move时进行的)进行收尾工作——比如:图片滑动完全划过去、元素跑到结束位置、将事件监听取消;
var isTtouchstart = false,
startOffsetX,
currentTranX = 0,
width = $gallery.offsetWidth,
$items = $gallery.querySelectorAll(".item"),
itemsLength = $items.length,
move = function (dx) {
$gallery.style.transform = "translate(" + dx + "px, 0)";
};
$gallery.addEventListener("touchstart", function (e) {
// 触摸开始时,记住当前手指的位置
startOffsetX = e.changedTouches[0].pageX;
//
$gallery.classList.remove("animation");
});
$gallery.addEventListener("touchmove", function (e) {
isTtouchstart = true;
// 计算手指的水平移动量
var dx = e.changedTouches[0].pageX - startOffsetX;
// 调用move方法,设置galley元素的transform,移动图片
move(currentTranX + dx);
});
$gallery.addEventListener("touchend", function (e) {
if (isTtouchstart) {
// 在移动图片的时候,需要动画,动画采用CSS3的transition实现。
$gallery.classList.add("animation");
// 计算偏移量
var dx = e.changedTouches[0].pageX - startOffsetX;
// 如果偏移量超出gallery宽度的一半
if (Math.abs(dx) > width / 2) {
// 处理临界值
if (currentTranX <= 0 && currentTranX > -width * itemsLength) {
// 如果手指向右滑动
if (dx > 0) {
// 如果图片不是显示第一张
if (currentTranX < 0) {
currentTranX = currentTranX + width;
}
// 如果手指向右滑动,并且当前图片不是显示最后一张
} else if (currentTranX > -width * (itemsLength - 1)) {
currentTranX = currentTranX - width;
}
}
}
// 如果未超出图片宽度的一半,上述条件不会执行,而这个时候,手指在移动的时候,图片随着手指移动了,通过下面的代码,将图片的位置还原
// 如果超出了图片宽度的一半,将切换到上一张/下一张图片
move(currentTranX);
}
isTtouchstart = false;
});
到此为止,基本功能是实现了,不过感觉少了点什么:
啊,知道了:点击进入预览时始终是从第一张开始的,而且如果结束预览时不是第一张它会消失!
这显然是没有做“变量恢复”:
//在上面代码中【1】的位置添加:
currentTranX = 0;
$gallery.style.transform = "translate(0, 0)";
//在上面代码【2】的位置添加:
for(let i in $items){
if($items[i]==e.target.parentNode){
currentTranX=-(+i)*width
move(currentTranX)
break
}
}
于是就出现了文章开头所示效果!
这个小demo是结束了,但是有一点却引起了我的关注:上面图片排列时为了防止展示问题都是让外层父容器指定宽高,然后给img元素一个宽高100%。
有没有可能让img固定宽高比呢?
能不能在所有外部宽高下都保持此宽高比呢?
padding-bottom实现比例固定图片自适应
calc()
和 background-size: cover;
都是个不错的想法,但是往兼容性、清晰度和特殊情况一看就会坏菜——比如cover在缩到一定范围时就会有部分遮盖。
但是如果padding出马,就像这样:
<div class="banner">
<img src="./images/nan.png">
</div>
.banner {
padding-bottom: 60%;
position: relative;
}
.banner > img {
position: absolute;
width: 100%;
height: 100%;
left: 0; top: 0;
}
Look:如此丝滑!
可以看到,无论屏幕宽度多宽,图片比例都是固定的——不会有任何剪裁,不会有任何区域缺失,布局就显得非常有弹性,也更健壮。
对于这种图片宽度100%容器,高度按比例的场景,padding-bottom的百分比值大小就是图片元素的高宽比,就这么简单。
重要的来了: 有时候图片宽度并不是容器的100%,例如,图片宽度50%容器宽度,图片高宽比4:3,此时用padding-bottom来实现就显得666了:
padding: 0 50% 66.66% 0;
object-fit实现图片自适应容器
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>宽高自适应</title>
<style>
.img-container{
width:688px;
height:204px;
background: black;
}
.img-container img{
width: 100%;
height: 100%;
object-fit: contain;
}
</style>
</head>
<body>
<div class="img-container">
<img src="images/download.png" alt="4">
</div>
</body>
</html>
object-fit
似乎一定程度上解决了width:100%
带来的一些图片宽高比问题,又结合了background-size:cover
的固定宽高比伸缩,也是比较秀的了。
“图片预览”应用场景下的js应用
其实更多场景下,看的是“图片完全、优雅地展示出来”。这时候,其实可以用JavaScript动态计算图片宽高:
- 容器宽高比例 > 图片宽高比例:说明图片比较高,以高度为准,宽度适应
- 容器宽高比例 < 图片宽高比例:说明图片比较宽,以宽度为准,高度适应
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<style>
div + div {
margin-top: 30px;
}
.img-container{
width:688px;
height:304px;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
box-sizing: border-box;
background: black;
}
.width {
width: 668px;
height: 404px;
}
</style>
</head>
<body>
<div class="img-container"></div>
<script>
const container = document.querySelector(".img-container")
container.classList.add("width")
const url = 'images/nan.png';
const img = document.createElement("img")
img.src = url;
img.onload=function(){
let { width,height} = getRealSize(container.offsetWidth,container.offsetHeight,img.width,img.height, 1)
img.width = width
img.height = height
}
container.appendChild(img)
function getRealSize(parentWidth, parentHeight, imgWidth, imgHeight, radio){
let real = { width:0,height:0}
let scaleC = parentWidth / parentHeight;
let scaleI = imgWidth / imgHeight;
if(scaleC > scaleI){ //说明图片比较高 以高度为准
real.height = radio * parentHeight;
real.width = parentHeight * scaleI;
}else if(scaleC < scaleI){ //说明图片比较宽 以宽度为准
real.width = radio * parentWidth;
real.height = parentWidth / scaleI;
}else{
real.width = radio * parentWidth;
real.height = parentWidth / scaleI;
}
return real
}
</script>
</body>
</html>