筆者所在的前端團隊主要從事移動端的H5頁面開發,而團隊使用的適配方案是: viewport units + rem
。具體可以參見凹凸實驗室的文章 – 利用視口單位實現適配布局 。
筆者目前(2017.08.12)接觸到的移動端適配方案中,「利用視口單位實現適配布局」是最好的方案。不過使用 rem
作為單位會遇到以下兩個難點:
- 微觀尺寸(20px左右)定位不准
- 逐幀動畫容易有抖動
第一個難點的通常出現在 icon
繪制過程,可以使用圖片或者 svg-icon 解決這個問題,筆者強烈建議使用 svg-icon,具體理由可以參見:「擁抱Web設計新趨勢:SVG Sprites實踐應用」。
第二個難點筆者舉個例子來分析抖動的原因和尋找解決方案。
一個抖動的例子
做一個8幀的逐幀動畫,每幀的尺寸為:360×540。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
.steps_anim {
position: absolute;
width: 9rem;
height: 13.5rem;
background: url(//misc.aotu.io/leeenx/sprite/m.png) 0 0 no-repeat;
background-size: 45rem 13.5rem;
top: 50%;
left: 50%;
margin: -5.625rem 0 0 -5.625rem;
animation: step 1.2s steps(5) infinite;
}
@keyframes step {
100% {
background-position: -45rem;
}
}
|
觀察在主流(手機)分辨率下的播放情況:
iPhone 6 (375×667) |
iPhone 6+ (414×736) |
iPhone 5 (320×568) |
Android (360×640) |
---|---|---|---|
![]() |
![]() |
![]() |
![]() |
四種分辨率下,可以看到除了 ip6
其它的三種分辨率都發生了抖動。(ip6
不抖動的原因是適配方案是基本於 ip6
的分辨率訂制的。)
分析抖動
圖像由終端(屏幕)顯示,而終端則是一個個光點(物理像素)組成的矩陣,換句話說圖片也一組光點矩陣。為了方便描述,筆者假設終端上的一個光點代表css中的1px。
以下是一張 9px * 3px
的sprite:
把 sprite 的 background-size 的寬度取一半,那么終端會怎么處理?
9 / 2 = 4.5
終端的光點都是以自然數的形式出現的,這里需要做取整處理。取整一般是三種方式:round/ceil/floor
。假設是 round ,那么 background-size: 5px
,sprite 會是以下三種的一個:
情況一 | 情況二 | 情況三 |
---|---|---|
![]() |
![]() |
![]() |
理論上,5 / 3 = 1.666...
。但實際上光點取整后,三個幀的寬度都不可能等於 1.666...
,而是有一個幀的寬度降級為 1px
(虧),另外兩個寬度升級為 2px
(盈),筆者把這個現象稱作「盈虧互補」。
再看一下盈虧互補后,逐幀的取位過程:
情況一 | 情況二 | 情況三 |
---|---|---|
![]() |
![]() |
![]() |
可以看到由於盈虧互補導致了三個幀的寬度不一致,虧的那一幀在動畫中的表示就是抖動。
筆者總結抖動的原因是:sprite在尺寸縮放后,幀與幀之間的盈虧互補現象導致動畫抖動
附注:1px 由幾個光點表示是由以終端的 dpr 決定
解決方案
「盈虧互補」也可以說是「盈虧不一致」,如果尺寸在縮放后「盈虧一致」那么抖動現象可以解決。
解決構想一
筆者根據「盈虧一致」設計了「解決構想一」:
根據上圖,其實很容易就聯想到一個簡單的方案:不用雪碧圖(即一幀對應一張圖片)。
這個方案確實是可以解決抖問題,不過筆者並不推薦使用它,因為它有兩個負面的東西:
- KB變大與請求數增多
- 多余的 animation 代碼
這個方案很簡單,這里就不贅述了。
解決構想二
把逐幀取位與圖像縮放拆分成兩個獨立的過程,就是筆者的「解決構想二」:
實現「構想二」,筆者首先想到的是使用 transform: scale()
,於是整理了一個實現方案A:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
.steps_anim {
position: absolute;
width: 360px;
height: 540px;
background: url(//misc.aotu.io/leeenx/sprite/m.png) 0 0 no-repeat;
background-size: 1800px 540px;
top: 50%;
left: 50%;
transform-origin: left top;
margin: -5.625rem 0 0 -5.625rem;
transform: scale(.5);
animation: step 1.2s steps(5) infinite;
}
@keyframes step {
100% {
background-position: -1800px;
}
}
/* 寫斷點 */
@media screen and (width: 320px) {
.steps_anim {
transform: scale(0.4266666667);
}
}
@media screen and (width: 360px) {
.steps_anim {
transform: scale(0.48);
}
}
@media screen and (width: 414px) {
.steps_anim {
transform: scale(0.552);
}
}
|
這個實現方案A存在明顯的缺陷:scale 的值需要寫很多斷點代碼。於是筆者結全一段 js 代碼來改善這個實現方案B:
css:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
.steps_anim {
position: absolute;
width: 360px;
height: 540px;
background: url("//misc.aotu.io/leeenx/sprite/m.png") 0 0 no-repeat;
background-size: 1800 540px;
top: 50%;
left: 50%;
transform-origin: left top;
margin: -5.625rem 0 0 -5.625rem;
animation: step 1.2s steps(5) infinite;
}
@keyframes step {
100% {
background-position: -1800px;
}
}
|
javascript:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
// 以下代碼放到<head></head>中// <![CDATA[
document.write("
.steps_anim {scale(.5); }
");
function doResize() {
scaleStyleSheet.innerHTML = ".steps_anim {-webkit-transform: scale(" + (document.documentElement.clientWidth / 750) + ")}";
}
window.onresize = doResize;
doResize();
// ]]>
|
通過改善后的方案 CSS 的斷點沒了,感覺是不錯了,不過筆者覺得這個方案不是個純粹的構建方案。
我們知道<img> 是可以根據指定的尺寸自適應縮放尺寸的,如果逐幀動畫也能與 <img>
自適應縮放,那就可以從純構建角度實現「構想二」。
SVG
剛好可以解決難題!!!SVG
的表現與 <img>類似同時可以做動畫。以下是筆者的實現方案C。
html:
1
2
3
|
<svg viewBox="0, 0, 360, 540" class="steps_anim">
<image xlink:href="//misc.aotu.io/leeenx/sprite/m.png" width="1800" height="540" />
</svg>
|
css:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
.steps_anim {
position: absolute;
width: 9rem;
height: 13.5rem;
top: 50%;
left: 50%;
margin: -5.625rem 0 0 -5.625rem;
image {
animation: step 1.2s steps(5) infinite;
}
}
@keyframes step {
100% {
transform: translate3d(-1800px, 0, 0);
}
}
|
方案C的改良
實現方案C很好地解決了方案A和方案B的缺陷,不過方案C也有它的問題:不利於自動化工具去處理圖片。
自動化工具一般是怎么處理圖片的?
自動化工具一般是掃描 CSS 文件找出所有的 url(...)
語句,然后再處理這些語句指向的圖片文件。
如果 可以改用 CSS 的
background-image
就可以解決這個問題,不過 SVG
不支持 CSS 的 background-image
。但是,SVG
有一個擴展標簽:foreignObject
,它允許向 插入
html
代碼。在使用它前,先看一下它的兼容情況:
iOS 與 Android 4.3 一片草綠兼容情況算是良好,筆者實機測試騰訊 X5
內核的瀏覽器兼容仍舊良好。以下是改良后的方案。
html:
1
2
3
4
5
|
<svg viewBox="0, 0, 360, 540" class="steps_anim">
<foreignObject class="html" width="360" height="540">
<div class="img"></div>
</foreignObject>
</svg>
|
css:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
.steps_anim {
position: absolute;
width: 9rem;
height: 13.5rem;
top: 50%;
left: 50%;
margin: -5.625rem 0 0 -5.625rem;
}
.html {
width: 360px;
height: 540px;
}
.img {
width: 1800px;
height: 540px;
background: url(//misc.aotu.io/leeenx/sprite/m.png) 0 0 no-repeat;
background-size: 1800px 540px;
animation: step 1.2s steps(5) infinite;
}
@keyframes step {
100% {
background-position: -1800px 0;
}
}
|
改良后的方案DEMO: http://jdc.jd.com/fd/promote/leeenx/201708/svg-sprite.html
總結
感謝閱讀完本文章的讀者。本文是筆者的個人觀點,希望能幫助到有相關問題的朋友,如果本文有不妥之處請不吝賜教。
參考資料:
https://stackoverflow.com/questions/9946604/insert-html-code-inside-svg-text-element
https://www.w3.org/TR/SVG/extend.html
https://developer.mozilla.org/en-US/docs/Web/SVG/Element/foreignObject