閱讀943 返回首頁    go 阿裏雲 go 技術社區[雲棲]


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 動畫效果:

3D Animation

方案

針對上麵的問題,分別使用下述方案進行優化。

解析 transform

為了應對 transform 字段的變化並提高解析性能,Weex 使用了 LL Parser 的方式來解析 transform 字段。

形式文法

LL Parser是一種解析形式語言的方式。按照Chomsky hierarchy,形式語言的表達能力從弱到強可劃分為下麵4類:

  1. Regular Grammars,如正則表達式,缺陷是無法表達遞歸這個概念。
  2. Context-free Grammars,如 Java/C/Python 等常見的編程語言。
  3. Context-sensitive Grammars,如HTML,同 Java/C/Python 相比,Context-sensitive Grammars 允許 HTML 支持下麵的語法:對於標簽<a>,無論是否存在對應的閉標簽</a>,均符合語法。
  4. 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:

G(\mathbf{t},\mathbf{a^T}, \mathbf{b^T}) = \begin{bmatrix}g_1(f_1(t_1), a_1, b_1) & g_1(f_1(t_2), a_1, b_1) & ... & g_1(f_1(t_m), a_1, b_1)\\ g_2(f_2(t_1), a_2, b_2) & g_2(f_2(t_2), a_2, b_2) & ... & g_2(f_2(t_m), a_2, b_2)\\ ... & ... & ... & ...\\ g_n(f_n(t_1), a_n, b_n) & g_n(f_n(t_2), a_n, b_n) & ... & g_n(f_n(t_m), a_n, b_n)\end{bmatrix}

公式中變量的意義如下:

  • taT, 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軸,表示物體在彈跳方向上的高度:

EaseBounceOutInterpolator

TimeInterpolator

在經典物理學中,時間是一個單調的線性函數。但在動畫場景下,一些變化可能是非線性乃至非單調的,例如加速運動或彈跳效果。

函數fj在 Property Animations 中以 TimeInterpolator 的形式存在,可以視其為一個 篡改時間的函數。通過這個函數,可以把物理上的真實時間映射到[0,1]區間上,映射後的值表示動畫完成的比例。下圖展示了函數fj的幾種可能情況。

TimeInterpolator

在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)仍然保持原座標不變,如下圖所示:

2d identity matrix

該矩陣對角線上的值表示scale,下圖中的線性變換將點P(x,y)的座標放到大了3/2倍:

2d scale matrix

對於rotate,可以用下圖的線性變換表示:

2d rotate matrix

使用線性變換表示 rotate, scale有兩個優點:

  1. 可以方便的對物體進行上述變換,下圖中等式左邊第一個矩陣仍表示線性變換,等式左邊第二個矩陣表示圖中白色五邊形的頂點,通過下麵的矩陣乘法,可以輕鬆將原物體放大至3/2倍(白色物體變為黃色物體)。

  2. 由於多個線性變換可以用乘法相連接,因此用一個矩陣就可以表示多個線性變換。對於有數十萬乃至數百萬個頂點的物體,進行數十個線性變換後,求物體頂點座標的問題,可以簡化乘兩個矩陣相乘問題,這樣在計算時間和存儲空間消耗上都有很大節省。

2D Affine Transformation 與 Homogeneous coordinates

然而,translation 並不是一個線性變換,當需要為二維向量做 translation 時,在2維向量空間中隻能使用加法實現,即如下圖所示:

2d translation matrix

當數十個矩陣加法/乘法混合後,計算複雜度和空間複雜度相比之前的線性變換都會有顯著增加,數學形式上也會變得很複雜。下圖所示的矩陣運算僅表示兩個線性變換和一個 translation 組合的情況,計算已經很複雜,當變換數量和頂點數量增加後,形式會變得更加複雜。

2d translation matrix & rotate matrix

2D translation在二維向量空間上其實是一個 Affine Transformation ,即一個線性變換連接上一個向量平移,形式如下圖所示:

\mathbf{y} = a\mathbf{x}+\mathbf{b}

為了將上述 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)。

3d translation matrix

Projection

對於3D建模後生成的物體而言,由於目前手機屏幕是二維的,觀察者最終看到的是三維空間的物體在二維屏幕上的投影。

常見的投影方式有兩種,Parallel ProjectionPerspective Prjection,下麵將詳細介紹。

Parallel Projection

Parallel Projection又可分為兩種,Orthographic Projection 和 Oblique Projection:

下麵為 Orthographic Projection,投影線與投影平麵垂直:

e1268f83a7b9eb3484cddddcefeaf775.png

下圖為Oblique Projection,投影線與投影平麵不垂直,存在一定的夾角:

Oblique projection

無論哪種情況,在Parallel Projection中,投影線之間總是相互平行。

Perspective Projection

在Perspective Projection中,投影線聚焦於一點,該點被稱為Vanishing Point。

b45b728068e794bc0455526ba847944a.png

Perspective Projection 同 Parallel Projection 相比,更符合人眼對現實世界的觀察,離觀察者近的物體看起來大,離觀察者遠的物體看起來小,下圖展示了Perspective Projection中的一些基本概念:

Perspective Projection Concept

  • 觀察者(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中,開發者可以通過設置 rotateXrotateY 獲得 Perspective Projection 的效果,使用 perspective 屬性控製 Camera 到 Projection Plane的距離,當不設置 perspective 時,weex 會把 perspective 設置為正無窮,以達到 Orthographic Projection 的效果。

效果展示

經過上述多種方案的協同優化,Weex動畫的幀率同未優化(未使用 Parser, GPU)時相比,得到了極大的提升。

優化前的幀率和動畫效果如下,可以看到運行一段時間後,每幀渲染時間遠大於17ms:

Before Optimization

優化後的幀率和動畫效果如下,保長期運行後,每幀渲染時間依然保持在17ms左右,動畫無明顯卡頓:

After Optimization

下圖展示了 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.' })
})

Weex Image Rotate 3D

參考資料

最後更新:2017-08-17 13:33:33

  上一篇:go  《可穿戴創意設計:技術與時尚的融合》一一1.5 眼鏡類可穿戴設備
  下一篇:go  移動 H5 首屏秒開優化方案探討