筆者所在的前端團隊主要從事移動端的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