CSS Sprites

What is CSS Sprites

Web 是个很神奇的地方,很多时候会给你“OMG! The old things is new again!”的错觉。 比如初衷是数据表格的 <table/> 标签,会被用来取得更有弹性的布局效果; 比如 <h1/> 还是 <p class="title"/> 作为网站的标题,都可以成为争辩的话题; 比如 <canvas/> 和 Flash 的比较。

CSS Sprites 也是如此。

我们先把 CSS Sprites 这名字拆分一下,CSS 和 Sprites,CSS 么大家都知道,Cascading Styles Sheets,层叠样式表。 只不过它的层叠算法有的时候很奇怪,代码结构看着也不想表。但这不妨碍你把它去繁取简, 理解为样式(Style)。事实上在 Flex 里头,Adobe 就是这么做的。

而 Sprites 呢?查下字典,意为小精灵。在计算机图形的世界, 这个词的意思 是一张二维的图片,用来嵌入到场景当中。通常它是由多张小图拼成的。同样,在 ActionScript 里面, 有个 Class 唤作 Sprite。 采取这种做法的原因,跟现在一样,是 速度。 把一张大图载入内存,每次重绘的时候只改变它的显示位置,比每次重绘都把图片数据搞来搞去要经济很多。

而两个词结合起来,就是本文要说的,使用 CSS 应用古老的 Sprites 技术。在开始之前,我们先了解一下为什么。

在 YUI 的 Yslow 网站优化工具评分标准里头,有一项重要的评分标准是 减少资源请求次数。 网页上各种菜单的图标,布局里的背景图片, 按钮等等,可以将这些图片弄到一张大图上,再在 CSS 中通过 background-position 制定需要显示的图片位置。 也就是让浏览器自己去调整显示结果,取得“抠”图的效果。它是一项网页优化的技术。

是否要使用这项技术已经不是个问题了,它无处不在。

】有趣的是,在笔者写本文的时候, CSS-Tricks 也发了篇博文。在“为什么”的部分说的比较清楚。

CSS

CSS 的中文意思,前面介绍过了。对于它的意义、简单语法等,可以参考我的 HTML & CSS

先说基本,background 属性的语法。示例声明(来自鹿木)。 这是一个简写。在 CSS 中这种简写很多,使用最多的就是 padding 和 margin 的语法。

background: #000 url("/picture/3003_m.jpg") no-repeat scroll center top;

接着说背景声明。详细属性分为:

  • background-color 各种颜色;
  • background-image 背景图,可以是 url(),也可以是 BASE64 编码的图片;
  • background-repeat 重复模式,no-repeat, repeat, repeat-x, repeat-y
  • background-attachment 是否跟随页面滚动滚动,scroll, fixed
  • background-position

上述简写便是以这个顺序。详细语法可以参考 w3school。 我们要讲的重头是 background-position。它的值的模式为 xpos yposxpos 可以是 left, center, right, x% 或者实际的像素单位的坐标; ypos 则是 top, middle, bottom, y% 或者实际坐标。

了解这个之后,我们就可以做个示例了。

Demo 1

这个示例来自于 A List Apart。 我们要做的事情就是,把上一节中的图片作 Sprites,做一个看起来还不错的导航栏。最终效果如右图所示:

先把 HTML 搞起来。通常写 HTML 的原则有两个:简介其一、语义化其二。把语义化作为重要准则的原因也有两个, SEO 和可用性。因为搜索引擎的爬虫和屏幕阅读器都不会识别通过 CSS 后加的图片、文字,它们只认 HTML。 代码如右图下方所示。

接下来,配上 CSS。首先,将这个菜单的高宽设置为背景图大小,配上背景图;把 <li/> 的默认属性重置掉。 关于 position 的部分说起来容易话长, 对这方面不熟的童鞋建议看看《十步走学会 CSS Position》。 简单地说这里的代码就是让每个菜单条目使用绝对定位,方便布置鼠标悬停的效果。

#skyline {
    width: 400px; height: 200px;
    background: url(test-3.jpg);
    margin: 10px auto; padding: 0;
    position: relative;
}
#skyline li {
    margin: 0; padding: 0; list-style: none;
    position: absolute; top: 0;
}
#skyline li, #skyline a {
    height: 200px; display: block;
}
#panel1b {left: 0; width: 95px;}
#panel2b {left: 96px; width: 75px;}
#panel3b {left: 172px; width: 110px;}
#panel4b {left: 283px; width: 117px;}

布置好菜单之后,通过 a:hover 加上特效:

#panel1b a:hover {
    background: transparent url(test-3.jpg) 0 -200px no-repeat;
}
#panel2b a:hover {
    background: transparent url(test-3.jpg)
    -96px -200px no-repeat;
}
#panel3b a:hover {
    background: transparent url(test-3.jpg)
    -172px -200px no-repeat;
}
#panel4b a:hover {
    background: transparent url(test-3.jpg)
    -283px -200px no-repeat;
}

这个例子应用 CSS Sprites 的方式有点特别,经典应用方式是背景图片只指定一次, 需要的时候再去改 background-position。这个做法是个 Workaround,而非 Fix,主要原因是 IE6 里头的一个 bug:如果 IE 的缓存控制设置为不缓存,每次都去服务器取资源的话, 背景图会闪(解决方式)。

下面,我们通过求是设计的博客,来做一个更为纯粹的示例。

Demo 2 - Blog of Qiushi Design

求是博客的导航栏,是一个比上例更为经典的 CSS Sprites 应用。 它的背景图片 是一张包括常态、鼠标悬停之后、disable 之后三个效果的图片,并对应求是设计各个模块。

HTML 如前文所述,遵循简明扼要、语义化的规则。注意这里虽然最终效果里头并不需要文字, 但是依然写明,并通过 text-indent 将它设为不可见。实际的代码中,还含有 titletarget 等属性。title 用来鼠标悬停时提示说明,target="_blank" 则告诉浏览器要新开一个窗口或者标签。 此处为了简明,略去了。有 Firebug 的童鞋,可以查看右侧的示例元素。

关于 #menu li a 的 CSS 声明:

#menu li a {
    background:url("http://blog.qiushid.com/wp-content/themes/qiushid/images/navitems.gif") no-repeat scroll 0 -468px transparent;
    display:block;
    float:left;
    height:26px;
    text-indent:-999em;
    width:85px;
}

因为每个模块对应的图标都不一样,所以实际位置通过 #menu li a.class 指定。以首页(home)为例:

#menu li.home a {
    background-position: 0 -182px;
}
#menu li.home a:hover {
    background-position: 0 0;
}

其余代码可以参考求是博客的 CSS 文件

Demo 3 - (Blink) With JavaScript

A List Apart 在几年后又发一文, 加入了 JavaScript 的部分。 使用 jQuery 的 .fadeIn(), slideDown() 等 UI 方面的方法,来模拟淡入淡出等效果。 原文的示例代码不很清爽,就不再转用。下面是参考 hover 文档 做的求是博客导航栏的基础上制作的示例。

有没有 jQuery is the new <blink/> 的错觉?廉价的特效,廉价的代码:

    $("#qiushid-menu").find("a").hover(function() {
        $(this).fadeOut(500, function() {
            $(this).fadeIn(500);
        });
    }, function() {
        $(this).fadeIn(100);
    });

这么做的问题也很明显。如果用户玩导航栏,在那上面划来划去,之前的淡入淡出效果并不会消失, 而是慢慢做完。后果就是,鼠标甩过去之后导航栏闪个不停。修正的方法是每次 mouseentermouseleave 的时候判断是否正在特效进行中,如果是就清除。 下面的例子思路跟这个是一样的,所以这个例子的方法我就不再说了。

Demo 4 - (Marquee) With JavaScript

下面,我们做个高级一点的,jQuery 版本的 <marquee/>。 我们知道,求是设计的导航栏的背景图是在鼠标悬停(hover)的时候,通过调整 background-position 得到效果。现在呢,就要用 JavaScript 让这个过程慢一点。

实际执行代码请查看本页面源代码末尾的 JavaScript 部分,这里就伪代码介绍一下。

    var yPosMap = {     // 最独立的办法自然是从 CSS 里头取,但是感觉费力
        home: [-182, 0],
        camp: [-260, -78],
        bookstore: [-286, -104],
        microblog: [-338, -156],
        network: [-312, -130]
    };
    var timerMap = {};  //  每个动画的定时器应该是独立的

    $marqueemenu.find('a').hover(function() {
        animate(this, 'show');
    }, function() {
        animate(this, 'hide');
    });

    function animate(block, mode) {
        clearTimers(clsName);     // 清除正在进行的动画
        if (reachTargetPosition()) {
            reset();
            return;               // 已经到了
        }
        timerMap[clsName] = setInterval(function() {
            pos = getYPos($a);
            /*
             * 最关键的语句,CSS Sprites 就是这么回事:
             */
            $a.css('background-position', '0 '+(pos+delta)+'px');
            if (reachTargetPostion()) {
                clearInterval(timerMap[clsName]);
                reset();
            }
        }, 10);
    }

Demo 5 - jQuery.animate()

我们的示例到这还没结束。

jQuery 自带了个 .animate(),但是对个别 CSS 属性的动画支持并不好。有达人做了个 jQuery Bg Pos 补丁 让 jQuery 可以以 background-position 做动画。实际效果可以看右方示例。 有没有觉得示例四很有重复发明轮子的嫌疑?

详细内容,不妨移步 Snook 的博文他的示例。这里简单讲述一下用法。

首先要在页面上引入 jQuery 与 jQuery.bgPos 插件。后者的内容并不多, 不妨把它并入你的网站的主 JavaScript 文件里头去。

lt;script type="text/javascript" src="jquery-1.4.2.min.js">lt;/script>
lt;script type="text/javascript" src="jquery.bgpos.js">lt;/script>

然后是使用:

    $('#snook-menu').find('a')
        .css( {backgroundPosition: "-20px 35px"} )
        .mouseover(function(){
            $(this).stop().animate({backgroundPosition:"(-20px 94px)"},
                                   {duration:500})
        })
        .mouseout(function(){
            $(this).stop().animate({backgroundPosition:"(40px 35px)"},
                                   {duration:200, complete:function(){
                $(this).css({backgroundPosition: "-20px 35px"})
            }})
        });

为需要特效的空间注册鼠标移入移出的响应函数,也可以使用前面示例的 .hover() 简写。 重点代码是 .animate() 的参数部分。第一个就是特效的结束值,.animate() 会根据这个值计算应该如何动画,详细的过程与示例四的 animate() 函数相仿。

第二个({duration:200, complete:function(){}})是其他参数,前者是动画的持续时间, 后者是当动画完成之后的回调。示例中的回调将控件重置,主要是针对动画结束背景图位置有偏差的情况。

Summary

看完本文,了解了这四个例子之后,我想你对 CSS Sprites 应该有很清楚的认知了。

最后广告一下:做前端开发,目前为止最好用的工具依然是 Firefox + Firebug 组合。 Google Chrome 的开发者工具也已经越来越完善,但是小细节总有不愉处,可能是我习惯了前者的缘故。 IE9 如今成了 Underdog,但是也在慢慢赶上。此外,最好整个 IE Tester。 当然,如果你的机器上还有 IE6,直接测也行。不过,

你的电脑上还有 IE6