自创jQuery放大镜效果,主要运用offset()、width()、height()、e.pageX、e.pageY等关于位置的方法。
核心思想:缩略图的尺寸与放大图尺寸的比值 = 放大镜相对缩略图的坐标X(或Y)/放大图相对包裹它的外框坐标X(或Y)的比值
其中有几个关键点需要注意,还有两个bug,调试过程没少为此绞尽脑汁,算是从坑里爬出来,最终得到这样一套程序。问题值得思考、研究和总结,代码还需不断重构、优化。
关键点
包裹缩略图的元素和包裹放大图的元素宽高都通过js计算获得,以便程序可以复用,换成任意图片都有效。
function imgScale($big, $small) {
// 必须先把元素插入文档才能获取大小
var big_width = $big.find("img").width();
var small_width = $small.find("img").width();
var scaleX = big_width/small_width;
var big_height = $big.find("img").height();
var small_height = $small.find("img").height();
var scaleY = big_height/small_height;
return [scaleX, scaleY];
}
2. 缩略图宽高用outerWidth()和outerHeight(),从而计算放大镜的位置时,放大镜可以靠到缩略图的最边上。
3. 放大图预览区域要三层div,最外层是预览区内容框,设置position: absolute,使预览区域脱离文档流,不会遮盖缩略图旁边的产品介绍;最内层是图片元素img,因为里面的图片要根据放大镜的移动而移动,所以也需要设置position: absolute;问题来了,图片要相对预览区内容框移动,我们都知道absolute是相对于第一个设置了relative的父元素,但预览区内容框也是absolute,所以要在中间加一层div,设置position: realative。
var $zoom_window = $("<div id ='zoom-window'><div><img src='images/product_image_big.jpg'></div></div>");
/*放大展示区*/
#shop-details {
margin-left: 330px;
}
#zoom-window {
position: absolute; /* 不占用文字空间 */
overflow: hidden;
border: 1px solid #ccc;
}
#zoom-window>div {
position: relative;
}
#zoom-window img {
position: absolute;
}
4. offset()方法是获取元素相对文档的偏移,获取缩略图的位置是相对文档的。这个相对位置是会随着浏览器的放大缩小而变化的,所以我们要把获取缩略图的运算放在事件回调函数内。这样,每次触发事件时会同时计算缩略图、放大镜元素的位置以及鼠标的event.pageX和event.pageY(也是相对于文档),保持一致。即使改变浏览器窗口大小,反正每次都会计算,保证不会出错。如果放在全局作用域,在页面加载时就计算出缩略图的位置,定死了,不会响应浏览器窗口的变化。
优化:从性能上看,每次鼠标滑入事件都计算位置,性能降低。可以增加一个windown.onresize()事件,实时计算缩略图的位置。
debug
1. 放大镜在缩略图内移动,会不停地闪
经过排查,发现是与放大镜元素插入的节点及mouseleave事件绑定的元素有关。
放大镜元素插入在缩略图主div的下一层级,mouseleave事件要绑定在放大镜的父元素(本例中是缩略图img的最外层div),而不能绑定在放大镜的兄弟元素或子元素(比如绑定在a元素)。
<div id="product-zoom">
<a href="images/product_image_big.jpg" title="毛呢外套中长款过膝呢大衣">
<img src="images/product_image_small.jpg" alt="毛呢外套中长款过膝呢大衣">
</a>
</div>
$parent.mouseleave(function() {
$(this).find("a").attr("title", this.myTitle);
$m_glass.remove();
$zoom_window.remove();
});
表面上看,绑定在外层div、a、img都可以,因为它们尺寸一样大,鼠标离开触发放大镜消失的事件效果一样。
实际上,如果绑定在兄弟元素或子元素,鼠标每移动一下,指针还在放大镜元素之内,程序认为它离开了兄弟元素(例如a元素)或子元素(例如image)的,就要触发mouseleave, 放大镜消失,放大镜一消失,指针还处于缩略图之内,又触发了mouseenter事件,放大镜出现。如此死循环。
2. 放大区图片加载的问题
放大区图片的src链接根据缩略图的链接href获得(因为用户会随时更换查看缩略图),因此要在mouseenter事件内加入获取放大区图片src这条代码。
最初js内在全局作用域创建放大区元素:
var $zoom_window = $("<div id ='zoom-window'><div><img src='images/product_image_big.jpg'></div></div>");
图片是包含了src属性的。
后来想着反正每次mouseenter事件会从缩略图获取放大区图片的src,就把这里的src取消了。
问题就出现了,鼠标第一次滑入,放大区无显示。
又是debug时间:
打断点,鼠标第一次滑过,查看图片的大小和图片外层包裹框的大小,0! 第二次之后,恢复正常!
分析原因:
获取预览图链接,插入。如果此时才是第一次添加src加载图片,由于是异步加载,根据图片的大小计算预览窗口的尺寸会先于图片完成,此时图片尺寸为0,则计算出的预览窗口的尺寸为0,稍后图片会加载。所以第一次看不到放大区的原因是,计算出的预览窗口的尺寸为0,图片设置了overflow: hidden属性,就一点都看不到了。
第二次mouseenter时图片已加载完成,计算出了需要的预览窗口,图片正常显示。
针对JavaScript(Jquery)图片异步加载的问题,编写了一个测试代码,如下:
得到结论:
1. 如果jQuery对象创建了但还没插入DOM树,使用jQuery的width()获取不到尺寸;
2. jQuery对象(图片链接)插入DOM树之之后,因为图片异步加载的原因,jQuery的width()和原生js的offsetWidth、clientWidth不能立即获取尺寸;
3. 下一线程,jQuery的width()和原生js的offsetWidth、clientWidth可以获取到图片尺寸。
解决方法:事件需要获取图片尺寸,则在之前的页面加载时就创建图片的jQuery对象。
<!DOCTYPE html>
<html>
<head>
<meta chardelete=utf-8 />
<title>关于页面图片加载的分析</title>
<script src="http://code.jquery.com/jquery-1.7.2.min.js"></script>
</head>
<style>
li {
font-size: 14px;
line-height: 1.5;
}
#img-box1,
#img-box2,
#img-box3 {
position: absolute;
width: 300px;
height: 300px;
overflow: hidden;
}
#img-box1 {
left: 10%;
}
#img-box2 {
left: 40%;
}
#img-box3 {
left: 70%;
}
#img-box1 > div,
#img-box2 > div,
#img-box3 > div {
position: relative;
}
#img-box1 > img,
#img-box2 > img,
#img-box3 > img {
position: absolute;
}
.point1,
.point2,
.point3 {
height:100px;
width: 100px;
position: absolute;
bottom: 5%;
background: red;
white-space: pre-line;
color: #000;
cursor: pointer;
}
.point1 {
left: 10%;
background: #b2d;
}
.point2 {
left: 40%;
background: #cd6;
}
.point3 {
left: 70%;
background: #fcc;
}
</style>
<body>
<h3>按以下要求执行$img_box~$img_box10相关的$(function(){})函数</h3>
<ol>
<li>只执行第1个函数(浏览器加载时创建一个临时的div,加载待显示的本地大图),注释掉其余,刷新网页,鼠标滑过3个块,查看图片显示和控制台</li>
<li>只执行第2个函数(浏览器加载时创建一个临时的div,加载待显示的本地小图),注释掉其余,刷新网页,鼠标滑过3个块,查看图片显示和控制台</li>
<li>只执行第3个函数(浏览器加载时创建一个临时的div,加载待显示的网络图片),注释掉其余,刷新网页,鼠标滑过3个块,查看图片显示和控制台</li>
<li>只执行第4个函数(浏览器加载时创建与待显示图片不同路径的图片),注释掉其余,刷新网页,鼠标滑过3个块,查看图片显示和控制台</li>
<li>只执行第5个函数(浏览器加载时创建一个无src路径的img元素),注释掉其余,刷新网页,鼠标滑过3个块,查看图片显示和控制台</li>
<li>都不执行</li>
</ol>
<div id="container"></div>
<div class='point1'>
要显示的图片为相对路径,超大尺寸
</div>
<div class='point2'>
要显示的图片为相对路径,小尺寸
</div>
<div class='point3'>
要显示的图片为网络路径
</div>
<script>
$(function () {
var $img_box1,
$img_box2
// mouseenter事件创建并插入相对路径的图片元素__超大尺寸
$('.point1').mouseenter(function () {
$img_box1 = $("<div id ='img-box1'><div><img src='images/superbig-image.jpg'></div></div>");
// 如果jQuery对象创建了但还没插入DOM树,使用jQuery的width()获取不到尺寸
console.log('插入DOM树之前jQuery对象本地大图的宽度:' + $img_box1.width());
// 插入DOM树之之后,jQuery的width()和原生js的offsetWidth、clientWidth获取尺寸
$img_box1.appendTo('#container');
console.log('插入DOM树之后本地大图的宽度' + document.querySelector('#img-box1 img').clientWidth + '\n');
}).mouseleave(function () {
$img_box1.remove();
});
// mouseenter事件创建并插入相对路径的图片元素--小尺寸
$('.point2').mouseenter(function () {
$img_box2 = $("<div id ='img-box2'><div><img src='images/small-image.jpg'></div></div>");
console.log('插入DOM树之前jQuery对象本地小图的宽度:' + $img_box2.width());
$img_box2.appendTo('#container');
console.log('插入DOM树之后本地小图的宽度:' + document.querySelector('#img-box2 img').clientWidth + '\n');
}).mouseleave(function () {
$img_box2.remove();
});
// mouseenter事件创建并插入网络url的图片元素
$('.point3').mouseenter(function () {
$img_box3 = $("<div id ='img-box3'><div><img src=''></div></div>");
$img_box3.find('img').attr('src','http://www.baidu.com/img/bdlogo.gif');
console.log('插入DOM树之前jQuery对象网络图片的宽度:' + $img_box3.width());
$img_box3.appendTo('#container')
console.log('插入DOM树之后网络图片的宽度:' + document.querySelector('#img-box3 img').clientWidth + '\n');
}).mouseleave(function () {
$img_box3.remove();
});
});
// 浏览器加载时创建一个临时的div,加载待显示的本地大图
$(function () {
var $img_box6 = $("<div id ='img-box6'><img src='images/superbig-image.jpg'></div>");
console.log('that big size local image loaded');
// 第一次插入DOM之后大图有宽度,插入之前没有;其它图都没宽度
// 第二次所有图插入之前无宽度,插入之后有
})
// 浏览器加载时创建一个临时的div,加载待显示的本地小图
$(function () {
var $img_box7 = $("<div id ='img-box7'><img src='images/small-image.jpg'></div>");
console.log('that small size local image loaded');
// 第一次插入DOM之后小图有宽度,插入之前没有;其它图都没宽度
// 第二次所有图插入之前无宽度,插入之后有
})
// 浏览器加载时创建一个临时的div,加载待显示的网络图片*/
$(function () {
var $img_box8 = $("<div id ='img-box8'><img src='http://www.baidu.com/img/bdlogo.gif'></div>");
console.log('that url image loaded');
// 第一次插入DOM之后网络图有宽度,插入之前没有;其它图都没宽度
// 第二次所有图插入之前无宽度,插入之后有
})
// 浏览器加载时创建与待显示图片不同路径的图片
$(function () {
var $img_box9 = $("<div id ='img-box9'><img src='images/another-image.jpg'></div>");
console.log('element of image with another src created');
// 第一次插入DOM之后、之前所有图都没宽度
// 第二次所有图插入之前无宽度,插入之后有
})
// 浏览器加载时创建一个无src路径的img元素
$(function () {
var $img_box10 = $("<div id ='img-box10'><img src=''></div>");
console.log('element of image with empty src created');
// 第一次插入DOM之后、之前所有图都没宽度
// 第二次所有图插入之前无宽度,插入之后有
})
</script>
</body>
</html>
最后是放大镜demo程序:
HTML如下:
<!DOCTYPE HTML>
<html>
<head>
<meta charset=utf-8 />
<title>zoom effects</title>
<link rel="stylesheet" href="style/zoom.css" />
<script type="text/javascript" src="scripts/jquery.js"></script>
</head>
<body>
<div id="product-zoom">
<a href="images/product_image_big.jpg" title="毛呢外套中长款过膝呢大衣">
<img src="images/product_image_small.jpg" alt="毛呢外套中长款过膝呢大衣">
</a>
</div>
<div id="shop-details">
</div>
<script type="text/javascript" src="scripts/zoom.js"></script>
</body>
</html>
CSS如下:
a img {
display: block;
}
/*产品缩略图浏览*/
#product-zoom {
float: left;
width: 320px;
border: 1px solid #bbb;
cursor: crosshair;
}
#product-zoom img {
width: 100%;
}
#m-glass {
width: 120px;
height: 120px;
opacity: 0.5;
background: #e5e4e2;
position: absolute;
}
/*放大展示区*/
#shop-details {
margin-left: 330px;
}
#zoom-window {
position: absolute; /* 不占用文字空间 */
overflow: hidden;
border: 1px solid #ccc;
}
#zoom-window>div {
position: relative;
}
#zoom-window img {
position: absolute;
}
JavaScript如下:
$(function () {
var $parent = $("#product-zoom");
//现有展示栏的位置、宽高,不能只是内容的高度
var dpWidth = $parent.outerWidth();
var dpHeight = $parent.outerHeight();
//放大镜的大小
var mSize = 120;
//创建放大镜
var $m_glass = $("<div id ='m-glass'></div>");
//创建预览区域,双层div。因为图片的父元素必须relative,但relative会占据原来位置,所以再在外面加一层div,设为absolute
//要先把src引入,否则第一次不能出现预览框,为什么????
var $zoom_window = $("<div id ='zoom-window'><div><img src='images/product_image_big.jpg'></div></div>");
//可不可以不设为全局变量?
var dpPos = null;
var scale = [];
$("#product-zoom a").mouseenter(function (e) {
//获取光标位置
this.myTitle = this.title;
this.title = "";
//不能放在全局变量,不然浏览器加载后就成为固定值,窗口缩小再mouseenter位置不对
dpPos = $parent.offset();
//改变放大镜的位置
showGlass(e, dpPos, dpWidth, dpHeight);
//插入放大镜
$m_glass.appendTo("#product-zoom");
//获取预览图链接,插入
// 获取预览图链接,插入。如果此时才是第一次添加src加载图片
// 由于是异步加载,根据图片的大小计算预览窗口的尺寸会先于图片完成
// 此时图片尺寸为0,则计算出的预览窗口的尺寸为0
// 第二次mouseenter时图片已加载完成,计算出需要的预览窗口
$zoom_window.find('img').attr('src',this.href);
$zoom_window.appendTo("#shop-details");
// 先插入文档,才能获得图片大小。设置预览图的大小
scale = imgScale($zoom_window, $parent);
$zoom_window.width(scale[0] * mSize + "");
$zoom_window.height(scale[1] * mSize + "");
});
$("body").on("mousemove", "#m-glass", function(e) {
//改变放大镜的位置
showGlass(e, dpPos, dpWidth, dpHeight);
var left = dpPos.left;
var top = dpPos.top;
//设置预览图片的位置
var absLeft = -(e.pageX - mSize/2 - left)*scale[0];
var absTop = -(e.pageY - mSize/2 - top)*scale[1];
var ultRight = -($zoom_window.find("img").width() - $zoom_window.width());
var ultBottom = -($zoom_window.find("img").height() - $zoom_window.height());
$zoom_window.find("img").css({
"left": (Math.max(Math.min(0, absLeft), ultRight)) + "px",
"top": (Math.max(Math.min(0, absTop), ultBottom)) + "px"
});
});
// mouseleave事件要绑定在放大镜的父元素,不能绑定在兄弟元素或子元素。
// 这样的话,鼠标每移动一下,程序认为它在放大镜$m_glass之内,
// 是离开了兄弟元素(例如a元素)或子元素(例如image)的,就要触发mouseleave
// 放大镜消失,放大镜一消失,就触发了mouseenter事件,放大镜出现。如此死循环。
// 由于用函数给框框设置了界限,鼠标怎么动框框都在图片范围内,这样鼠标指针就可以脱离框框,实现解除绑定
$parent.mouseleave(function() {
$(this).find("a").attr("title", this.myTitle);
$m_glass.remove();
$zoom_window.remove();
});
// 改变放大镜位置的函数
function showGlass(e, dpPos, dpWidth, dpHeight) {
var mLeft = Math.min(Math.max((e.pageX - mSize/2), dpPos.left),
(dpPos.left + dpWidth - mSize));
var mTop = Math.min(Math.max((e.pageY - mSize/2), dpPos.top),
(dpPos.top + dpHeight - mSize));
$m_glass.css({
"left": mLeft + "px",
"top": mTop + "px"
});
}
//预览区大小:大图/小图*放大镜尺寸
function imgScale($big, $small) {
// 必须先把元素插入文档才能获取大小
var big_width = $big.find("img").width();
var small_width = $small.find("img").width();
var scaleX = big_width/small_width;
var big_height = $big.find("img").height();
var small_height = $small.find("img").height();
var scaleY = big_height/small_height;
return [scaleX, scaleY];
}
});