原生图片预览实现及由此引出的图片自适应宽高问题探索

→→推荐一套B站新鲜出炉的web前端教程←←

看到手机相册,突发奇想:能不能用实现一个原生的页面“相册”?或者说,传统网页中怎么实现图片预览功能?

如果在原生网页中使用插件,那必不可少的要引入一堆东西(并不是鄙视插件,只是为了引入下文,见谅嘿嘿);但又不是说所有的页面都要用框架…

经过一番探索,终于大概实现了想要的功能:

大概流程就是:可以点开大图观看、可以左右滑动切换、进入预览时可以从你当前点击的那一张开始浏览。


实现相册初始展示页

如上所示,我们可以在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动态计算图片宽高:

  1. 容器宽高比例 > 图片宽高比例:说明图片比较高,以高度为准,宽度适应
  2. 容器宽高比例 < 图片宽高比例:说明图片比较宽,以宽度为准,高度适应
<!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>
(0)

相关推荐