Weex Android 動畫揭秘
背景
在目前常見的交互方式中,動畫扮演了一個重要的角色。
在 Weex 框架下,Weex 的動畫需要屏蔽 CSS/JS 動畫與 Android 動畫係統的差異,並盡可能的達到60FPS。
本文闡述了在 Android 上實現高性能CSS/JS動畫過程中所遇到的問題/相關數學知識及解決方案。本文使用的前端 DSL 為 Weex vue 1.0或 Weex Vue 2.0。
現狀與問題
在 Weex 環境下, 一個典型的動畫在前端DSL中的寫法如下:
animation = weex.requireModule('animation')
animation.transition(testEl, {
styles: {
color: '#FF0000',
transform: 'translate(250px, 100px) rotate(60deg)',
transformOrigin: 'center center'
},
duration: 800, //ms
timingFunction: 'ease',
delay: 0 //ms
}, function () {
modal.toast({ message: 'animation finished.' })
})
對於上述代碼片段,Weex Android需要處理下述問題。
transform 字段的解析
為了符合傳統意義上的前端的書寫習慣,transform 字段沒有使用JSON表示,而是使用了一個字符串表示。在 transform 裏,逗號前後可能沒有空格,也可能有多個空格,transform裏的函數名稱和參數的數據類型也不確定,且麵臨後期需求變更的可能性。
對於複雜字符串的解析與處理,常見的方式是正則表達式。然而在此場景下使用正則表達式,麵臨如下困難:
- 正則表達式在 Android 下性能較差,對於每秒60幀,每幀對數百個元素做動畫的場景,正則表達式將會成為整個動畫模塊的性能瓶頸。
- 正則表達式的可維護性很差,對於需求變更很不友好,經過需求變更及人員調整後,複雜的正則表達式往往無法維護,隻能推導重寫。
Android 動畫方案的選擇
在Android係統層麵,存在Property Animation, View Animation, Drawable Animation三種動畫體係,且三個體係互不兼容。Weex需要選擇一個動畫體係達到以下目的:
- 將前端指定的 styles(如transform,color)和 timing-function 以合理的方式映射到 Android 端。
- style 和 timing-function 對修改友好。
- 可以使用 Android 手機的 GPU 能力提高動畫幀率。
3D動畫的實現
支持 rotateX, rotateY 屬性,實現如下的 3d 動畫效果:
方案
針對上麵的問題,分別使用下述方案進行優化。
解析 transform
為了應對 transform 字段的變化並提高解析性能,Weex 使用了 LL Parser 的方式來解析 transform 字段。
形式文法
LL Parser是一種解析形式語言的方式。按照Chomsky hierarchy,形式語言的表達能力從弱到強可劃分為下麵4類:
- Regular Grammars,如正則表達式,缺陷是無法表達遞歸這個概念。
- Context-free Grammars,如 Java/C/Python 等常見的編程語言。
- Context-sensitive Grammars,如HTML,同 Java/C/Python 相比,Context-sensitive Grammars 允許 HTML 支持下麵的語法:對於標簽
<a>
,無論是否存在對應的閉標簽</a>
,均符合語法。 - Recursively enumerable Grammars,圖靈機識別形式語言的能力上限,一般隻存在於理論中。
可以將形式語言中的符號的劃分為下麵兩類,終結符號和非終結符號,下麵使用EBNF的方式,給出了整數(integer)在形式語言中的定義。在這個定義中,integer和digit是非終結符,雙引號中的0,1,2,3,4,5,6,7,8,9,-
均為終結符號。非終結符號可以由推導規則進行推導,而終結符號則無法進行推導。
integer = ["-"], digit, {digit} ;
digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" ;
在更複雜的推導規則中,非終結符可以被遞歸推導。
LL Parser
LL Parser 是一種解析Context-free Grammars的方式。在常見的編程語言中實現 LL Parser 時,一般會把非終結符號用該語言中的函數表示,Context-free Grammar中的遞歸可以映射為編程語言中函數的遞歸;終結符號則一般使用字符串處理技術來處理。
transform 的定義、解析及擴展
對於transform,用下述 ENBF 形式進行定義:
definition = {function};
function = name, "(", value, { ",", value } , ")";
name = character, {character};
value = identifier, {identifier};
identifier = character | "." | "%" | "+" | "-";
character = digit | letter;
digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" ;
letter = "A" | "B" | "C" | "D" | "E" | "F" | "G"
| "H" | "I" | "J" | "K" | "L" | "M" | "N"
| "O" | "P" | "Q" | "R" | "S" | "T" | "U"
| "V" | "W" | "X" | "Y" | "Z" | "a" | "b"
| "c" | "d" | "e" | "f" | "g" | "h" | "i"
| "j" | "k" | "l" | "m" | "n" | "o" | "p"
| "q" | "r" | "s" | "t" | "u" | "v" | "w"
| "x" | "y" | "z" ;
Weex 對 transform 解析的解析使用了 LL Parser 的方式,代碼參見 FunctionParser。
實際上,使用上述文法,不僅定義了 transform, 還定義了 rgb(244, 23, 400)
等模式。所以上述 FunctionParser 具有較強的通用性,不僅適用於 transform ,還可以應用於其他字段上。
動畫方案的選擇
Android在係統層麵,提供了三種動畫機製,分別是 Drawable Animation, View Animation, Property Animation.
Drawable Animation 與 View Animation
Drawable Animation最簡單,但一般用於動畫類型和持續時間已經在編譯時確定的場景,並不適用於 Weex 這樣的動態化方案。
View Animation 的複雜度適中,但擴展性差,隻能將動畫應用於下述View的屬性上:
- rotate
- scale
- translate
- alpha
基於擴展性考慮,Weex 的動畫方案選擇了 Property Animation。
Property Animation
在狹義上,動畫可以被視為為某個對象的一個或多個屬性隨著時間變化的過程。動畫的這種表示形式與數學上的函數很相似,在Android中,可以用如下函數描述Property Animation:
公式中變量的意義如下:
- t,aT, bT 分別代表時間序列、屬性起始值序列、屬性終止值序列,三者均為向量。函數G產生了一個 n * m 的矩陣
- tj 為指定的時間點,ai 為動畫起始時某個屬性的值,bi為動畫終止時某個屬性的值。
下麵具體闡述上麵的函數。
ObjectAnimator
在Android中,每一次屏幕刷新,會產生一個 VSync 硬件中斷。當係統收到 VSync時,會調用Choreographer
的回調函數,在回調函數中,ObjectAnimator會被觸發。
ObjectAnimator首先根據當前的硬件時鍾,確定tj的取值,之後求出該時間點對應的列向量。然後根據列向量中每一行的取值,依次更新對應的屬性。
因此,tj可以視為插值時間,插值時間序列 t 與屬性變換函數序列 g 的外積為函數G,即ObjectAnimator。
由於Choreographer
的回調函數每一次被調用,可以確定一個tj,故tj是離散的,所以 t 是一個離散變量,G是一個離散函數。
TypeEvaluator 與 Property
當ObjectAnimator依次更新對象的屬性時,由於Java語言缺少函數指針的概念,ObjectAnimator無法更新複雜的屬性值,隻能對基本數據類型進行更新。
為了解決這個問題,可以使用 Property 對複雜對象的setter/getter進行封裝,ObjectAnimator使用封裝後的 Property 即可完成複雜屬性的更新操作。
對於下麵的這些屬性,如果使用Property的方式更新它們的值,Android係統將自動啟用 GPU 硬件加速:
- rotate
- rotateX
- rotateY
- scaleX
- scaleY
- translateX
- translateY
- alpha
當麵對需求變更,需要增加新的屬性時,編寫新的 Property 即可。
函數gi是插值時間、起始值、終止值三個變量的函數,在Android 中,用TypeEvaluator 表示 gi。ObjectAnimator 會使用 TypeEvaluator 的值來更新對應的 Property。
gi可能為非單調函數,下圖為一個彈跳效果的函數曲線,a,b為某個確定的值,f(tj)為x軸,表示插值時間;gi為y軸,表示物體在彈跳方向上的高度:
TimeInterpolator
在經典物理學中,時間是一個單調的線性函數。但在動畫場景下,一些變化可能是非線性乃至非單調的,例如加速運動或彈跳效果。
函數fj在 Property Animations 中以 TimeInterpolator 的形式存在,可以視其為一個 篡改時間的函數。通過這個函數,可以把物理上的真實時間映射到[0,1]
區間上,映射後的值表示動畫完成的比例。下圖展示了函數fj的幾種可能情況。
在Weex Android的動畫中,transform/style 被映射到了TypeEvaluator上,仍使用簡單的線性函數;timing-function 映射到了 TimeInterpolator 上,該函數可能為來實現非單調函數,如 Bézier_curve。
3D Animation
目前 Weex 的 3D Animation特指 rotateX, rotateY, perspective 這三個屬性,前端可以利用這三個屬性實現一些3D效果。
Mathematics
下麵首先闡述動畫在2D空間上遇到的一些數學問題及解決方案,之後再擴展到3D空間。
2D Linear Transformation
2D空間上的點可以視為一個2維向量空間上的向量。rotate,scale 可以視為線性變換(Linear Transformation)矩陣。
當該矩陣是單位矩陣,點P(x,y)仍然保持原座標不變,如下圖所示:
該矩陣對角線上的值表示scale,下圖中的線性變換將點P(x,y)的座標放到大了3/2倍:
對於rotate,可以用下圖的線性變換表示:
使用線性變換表示 rotate, scale有兩個優點:
-
可以方便的對物體進行上述變換,下圖中等式左邊第一個矩陣仍表示線性變換,等式左邊第二個矩陣表示圖中白色五邊形的頂點,通過下麵的矩陣乘法,可以輕鬆將原物體放大至3/2倍(白色物體變為黃色物體)。
由於多個線性變換可以用乘法相連接,因此用一個矩陣就可以表示多個線性變換。對於有數十萬乃至數百萬個頂點的物體,進行數十個線性變換後,求物體頂點座標的問題,可以簡化乘兩個矩陣相乘問題,這樣在計算時間和存儲空間消耗上都有很大節省。
2D Affine Transformation 與 Homogeneous coordinates
然而,translation 並不是一個線性變換,當需要為二維向量做 translation 時,在2維向量空間中隻能使用加法實現,即如下圖所示:
當數十個矩陣加法/乘法混合後,計算複雜度和空間複雜度相比之前的線性變換都會有顯著增加,數學形式上也會變得很複雜。下圖所示的矩陣運算僅表示兩個線性變換和一個 translation 組合的情況,計算已經很複雜,當變換數量和頂點數量增加後,形式會變得更加複雜。
2D translation在二維向量空間上其實是一個 Affine Transformation ,即一個線性變換連接上一個向量平移,形式如下圖所示:
為了將上述 Affine Transformation 轉變為Linear Transformation ,在計算機圖形學中經常在3D Homogeneous coordinates 下表示2D空間上的點。
對於m維向量空間上的 Affine Transformation,可以通過添加一個額外的維度,轉變為m+1維上的Linear Transformation,m+1的向量空間被稱為 Homogeneous coordinates 。
由於2D空間內的 translation, rotate, scale 均是二維向量空間內的 Affine Transformation,因此在3D Homogeneous coordinates 下,上述變換將變為 Linear Transformation.
下麵的例子中為點 P(x,y) 增加了一個額外的維度後(即點 P 位於平麵z=1上),使用線性變換即可完成translation,亦將點 P(x,y) 移動到點 P(x+3, y+2)。
Projection
對於3D建模後生成的物體而言,由於目前手機屏幕是二維的,觀察者最終看到的是三維空間的物體在二維屏幕上的投影。
常見的投影方式有兩種,Parallel Projection 和 Perspective Prjection,下麵將詳細介紹。
Parallel Projection
Parallel Projection又可分為兩種,Orthographic Projection 和 Oblique Projection:
下麵為 Orthographic Projection,投影線與投影平麵垂直:
下圖為Oblique Projection,投影線與投影平麵不垂直,存在一定的夾角:
無論哪種情況,在Parallel Projection中,投影線之間總是相互平行。
Perspective Projection
在Perspective Projection中,投影線聚焦於一點,該點被稱為Vanishing Point。
Perspective Projection 同 Parallel Projection 相比,更符合人眼對現實世界的觀察,離觀察者近的物體看起來大,離觀察者遠的物體看起來小,下圖展示了Perspective Projection中的一些基本概念:
- 觀察者(viewer)或 Camera,即圖中的人眼。由於所有的光線匯聚於人眼,因此人眼所在位置是 Vanishing Point。
- Objects,圖中虛線圓,即被投影的物體。
- Projection Plane,即圖中的 Drawing Surface,物體將會被投影到此平麵上。觀察者看不到Object,隻能看到 Object 在 Projection Plane 上的投影。
- 圖中的d是觀察者離投影平麵的距離,d越大時,投影線之間的夾角越小,投影效果越接近於Orthographic Projection。當d為正無窮時,投影線之間互相平行,此時Perspective Projection 變成了 Orthographic Projection。因此Orthographic Projection是Perspective Projection的一個特例。
在一個典型3D渲染模型中,Projection Plane一般為屏幕,Camera為開發者設置的一個點,Objects是開發者對於物體的建模,用戶最終隻能看到 Objects 在屏幕上的投影。
Implementation
在Weex中,開發者可以通過設置 rotateX
、rotateY
獲得 Perspective Projection 的效果,使用 perspective
屬性控製 Camera 到 Projection Plane的距離,當不設置 perspective 時,weex 會把 perspective 設置為正無窮,以達到 Orthographic Projection 的效果。
效果展示
經過上述多種方案的協同優化,Weex動畫的幀率同未優化(未使用 Parser, GPU)時相比,得到了極大的提升。
優化前的幀率和動畫效果如下,可以看到運行一段時間後,每幀渲染時間遠大於17ms:
優化後的幀率和動畫效果如下,保長期運行後,每幀渲染時間依然保持在17ms左右,動畫無明顯卡頓:
下圖展示了 3D rotation 的效果,關鍵代碼片段如下,可以看到由於 perspective 屬性的存在,圖片呈現出了 離觀察者近的部分較大,離觀察者遠的部分較小 的效果,目前 perspective 隻在 Weex 0.16 以上支持:
animation.transition(testEl, {
styles: {
color: '#FF0000',
transform: 'rotateY(45deg) perspective(1800px)',
transformOrigin: 'center center'
},
duration: 3000, //ms
timingFunction: 'ease',
delay: 0 //ms
},
function () {
modal.toast({ message: 'animation finished.' })
})
參考資料
- https://www.gcssloop.com/customview/matrix-3d-camera
- https://en.wikipedia.org/wiki/Formal_grammar
- https://javayhu.me/blog/2016/05/26/when-math-meets-android-animation-1/
- https://javayhu.me/blog/2016/05/27/when-math-meets-android-animation-2/
- https://github.com/ssloy/tinyrenderer/wiki/Lesson-4:-Perspective-projection
- https://www.zhihu.com/question/20666664/answer/157400568
- https://drafts.csswg.org/css-transforms-2/
最後更新:2017-08-17 13:33:33