原生图片预览实现及由此引出的图片自适应宽高问题探索
看到手机相册,突发奇想:能不能用实现一个原生的页面“相册”?或者说,传统网页中怎么实现图片预览功能?
如果在原生网页中使用插件,那必不可少的要引入一堆东西(并不是鄙视插件,只是为了引入下文,见谅嘿嘿);但又不是说所有的页面都要用框架…
经过一番探索,终于大概实现了想要的功能:
大概流程就是:可以点开大图观看、可以左右滑动切换、进入预览时可以从你当前点击的那一张开始浏览。
实现相册初始展示页
如上所示,我们可以在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; /* 相册采用flex布局 */ display: flex; /* 相册每一项为横向排列,并且换行 */ flex-flow: row wrap; } .gallery .item{ /* 每一项平均排列 */ /* flex: 1; */ /* 图片宽度设置为1/3屏幕宽度 */ 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{ /* 对子项设置为flex布局 */ display: flex; /* 设置margin为auto实现图片居中显示 */ 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了:
/**右50%表示宽度,下66.66%表示“高宽比4:3”**/ 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) /** * [获取自适应图片的宽高] * @param {[number]} parentWidth [父容器宽] * @param {[number]} parentHeight [父容器高] * @param {[number]} imgWidth [图片实际宽] * @param {[number]} imgHeight [图片时间高] * @param {[number]} radio [撑开比例] * @return {[Obejt]} [图片真实宽高] */ 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>