動效乾貨——滑動手勢的複雜動畫,多任務界面的滑動布局(使用Framer,進階教程)

現在的多任務界面以 iOS 的水平滑動,Android 的垂直滑動為主。相信你都體驗過了。

n

這個設計運用了「深度」「疊層」的概念,在多任務界面正確的區分和傳達了「最近使用過的 App」「之前打開過的 App」這類信息。並且結合滑動交互手勢,每一個 App 的界面在屏幕中央的時候都能得到大面積的展示。相比於幾年前,「只顯示 icon」 或者「顯示了界面,但界面是水平排列,沒有深度」的多任務界面設計,這是一個很大的提升。這次我將 dirty hack 一個多任務界面的滑動效果。

n

這篇文章有一定難度。首先你需要入門 Framer。

n

需要了解交互事件動效乾貨--交互事件入門(使用Framer) - 知乎專欄

n動效乾貨--交互事件drag,scroll(使用Framer) - 知乎專欄n

還需要懂代碼基礎操作,包括變數、 for 循環等等。

n

還有貝塞爾曲線原理動效乾貨--貝塞爾曲線插值器原理(進階準備) - 知乎專欄

n

這已經是一個進階教程了。是我之前好幾篇文章里所講解的知識的一次綜合運用。

n

首先分析界面的布局,每個卡片是按順序排列的。通過滑動來改變每個卡片的位置和大小。

n

所謂的 dirty hack 就是繞過複雜的滑動機制,直接用來一個 ScrollComponent 來進行滑動操作。監聽它的水平位置的值,並映射到每個卡片上,實現卡片的移動。下面有GIF圖展示。

card_num = 9 #卡片數量nn# 弄個背景nbg = new Layerntwidth: 1080ntheight: 1920ntimage: "images/bg.png" #準備好的背景圖片nn# 創建所有的卡片ncards = new Layerntwidth: 1080ntheight: 1920ntbackgroundColor: nullnfor i in [0..card_num]ntcard = new Layernttparent: cardsnttwidth: 1080nttheight: 1920nttscale: 0.8nttx: -1080/2ntt# 隨機選擇準備好的圖片,名字分別為 app1 app2 app3,png格式nttimage: "images/app"+ Utils.randomChoice(["1","2","3"]) + ".png"nttborderRadius: 16nttshadowX: -24nttshadowColor: rgba(0,0,0,0.2)nttshadowBlur: 30nttopacity: 1ntcard.progress = 0 #假設出各自的滑動進度,從0~1表示整個完成度ntn#讓最後一個卡片不顯示,這個點不重要,不要關注它ncards.children[card_num].image = nullncards.children[card_num].backgroundColor = nullncards.children[card_num].shadowColor = nullnn# 創建用來滑動的組件n#設置一個足夠大的可滑動距離nscrool_layer = new ScrollComponentntwidth: 1080*6ntheight: 1920ntmaxX: 1080nthorizental: falsentbackgroundColor: nullnscrool_layer.centerX()nscrool_layer.content.draggable.vertical = falsenscrool_layer.content.backgroundColor = null #不需要看見它nscrool_layer.content.width = 1080*4 #讓content比父級小一點,一定程度避免邊界回彈,可滑動區域大一點nscrool_layer.content.draggable.speedX = 0.5*0.72 #設一個滑動的阻力,不然滑得太快了n#讓滑動組件在一開始的時候做一個動畫,快速檢查下面寫的參數映射有沒有效果nscrool_layer.content.x = 0nscrool_layer.content.animatentproperties: nttx: 189-12nttime: 0.5nn# 設置參數映射nprogress = 0 #建立一個總的進度nnscrool_layer.content.on Events.Move, ->nt#取到總進度的值nt#這行是說滑動組件每移動一整個屏幕的寬度時,總進度會產生 1 的變化。並保證滑動組件在0的位置時,總進度已經到1了。方便觀察而已ntprogress = Utils.modulate(scrool_layer.content.x, [-1080,0], [0,1])ntntfor i in [0..card_num]ntt#按卡片各自的順序分配各自的不同的進度ntt#其實這裡是設置成「總進度每變化1,各個卡片的進度也同樣變化1」但是卡片們是錯開的,每個錯開了0.2。比如第一個是1,下一個會是1.2,再下一個是1.4。之所以設計成0.2,是要控制界面內同時存在的卡片數量,0.2的話就是同時能存在5個卡片。之所以採用倒序,是因為在這裡靠後的卡片層級是在上面的。nttcards.children[card_num-i].progress = Utils.modulate(progress, [i*0.2,i*0.2+1], [0,1])nttntt#按各自不同的進度,映射各自的狀態ntt#位置。當它自己的進度從 0~1 時,它自己的位置也跟著變化一個整個屏幕的距離nttcards.children[card_num-i].x = Utils.modulate(cards.children[card_num-i].progress, [0,1], [0,1080], true) - 200n

(GIF圖)無法播放的話可以點擊鏈接 pic2.zhimg.com/v2-02b46

現在界面已經動起來了,有些細節先暫時不要管。因為接下來要處理一個大問題:貝塞爾曲線。

可以看到卡片都是平均排布的(這樣還沒有達到目的),因為現在的映射是線性的,需要改成貝塞爾的。Android N版本里用的就是這種貝塞爾曲線的方法來控制所有的卡片。

具體是:用4段二階貝塞爾曲線拼合成一個完整的曲線。每個點的位置是需要原型搭建出來以後再去細調的。但這裡我已經調好了。把下面這段加在代碼的最前面。

# 擬合好一條完整的曲線nbezeir_subdivs = 999 #每一段的二階曲線有1000個分段數np0 = [0,1]np0_5 = [0.1, 0.9988]#handlenp1 = [0.175,0.9955]np1_5 = [0.4, 0.99]#handlenp2 = [0.575,0.92]np2_5 = [0.7, 0.88]#handlenp3 = [0.775,0.71]np3_5 = [0.9,0.4]#handlnp4 = [1,0]n#一共4段二階曲線,每段都有3個控制點,首位相連nbezeir_pints = [nttt[p0, p0_5, p1],nttt[p1, p1_5, p2],nttt[p2, p2_5, p3],nttt[p3, p3_5, p4]nttt]nn# 用貝塞爾公式把點的坐標都算出來,放到數組裡nbezeir_x = []nbezeir_y = []nfor j in [0..3]ntfor i in [0..bezeir_subdivs]nttx_ = Math.pow(1-i/bezeir_subdivs,2)*bezeir_pints[j][0][0] + 2*i/bezeir_subdivs*(1-i/bezeir_subdivs)*bezeir_pints[j][1][0] + Math.pow(i/bezeir_subdivs,2)*bezeir_pints[j][2][0]ntty_ = Math.pow(1-i/bezeir_subdivs,2)*bezeir_pints[j][0][1] + 2*i/bezeir_subdivs*(1-i/bezeir_subdivs)*bezeir_pints[j][1][1] + Math.pow(i/bezeir_subdivs,2)*bezeir_pints[j][2][1]nttbezeir_x.push(x_)nttbezeir_y.push(y_)n

曲線的形態大概是這樣:

開始非常慢,中間快了一點,後面更快。

擬合好曲線以後,還需要去使用這條曲線。最後一步:取到曲線映射

獲得映射。用各個卡片的進度值作為曲線的x值,去查找到對應的曲線y值,並輸出,完成貝塞爾曲線映射。

getMapping = (_progress) ->ntfor i in [0..4000] #4條1000分段的曲線,完整的數組裡總共就有4000個值nttif _progress < bezeir_x[i]ntttreturn bezeir_y[i]ntttbreakn

這個是一個對比的方法,相當於拿著已經有的「進度值」去從小到大挨個對比一遍「曲線x值」,直到卡到某一個「曲線x值」比它還大,那麼就算是選中這個點了。

把 getMapping() 這個方法放到代碼的最前面。並把設置參數映射完善:

# 設置參數映射nprogress = 0nnscrool_layer.content.on Events.Move, ->ntprogress = Utils.modulate(scrool_layer.content.x, [-1080,0], [0,1])ntntfor i in [0..card_num]nttcards.children[card_num-i].progress = Utils.modulate(progress, [i*0.2,i*0.2+1], [0,1])nttntt#按各自不同的進度,映射各自的狀態ntt#透明度保持線性即可。參數也是我調整過的了。nttcards.children[card_num-i].opacity = Utils.modulate(cards.children[card_num-i].progress, [0.2,0.54], [0,1], true)ntt#尺寸保持線性即可。參數也我調整過的了。nttcards.children[card_num-i].scale = Utils.modulate(cards.children[card_num-i].progress, [0,1], [0.61, 0.72], true)ntt#位置。用 getMapping 的方法去取對應的貝塞爾曲線的y點值,並乘以尺寸係數,並加上原始位置參數nttcards.children[card_num-i].x = 1080*0.93 - getMapping(cards.children[card_num-i].progress)*1080*1.11n

(GIF圖)無法播放的話可以點擊鏈接 pic1.zhimg.com/v2-5aa4a

這個例子到這裡就結束了。如果要細化的話,可以去把每個 App 的 icon、名字都加上,調整曲線的控制點,調整 layout 等等。

還有放手後的「自動吸附」效果。放手後讓某一個 App 的界面自動吸附到屏幕中心。這裡已經不需要再深入介紹了。相信你已經可以想到怎麼去控制吸附過程的動畫過渡。在工程師做 Demo 的時候和他們深入溝通,讓專業的人來做完整的 Demo。你只需要明白用戶體驗的關鍵環節,能提供調整方案,而不是亂猜亂指揮。

而這個例子的核心點就是:

  1. 靠滑動組件把自己的坐標映射到其他物體。hack 出滑動手勢來做原型。

  2. 貝塞爾曲線映射,做非線性變化。
  3. 使用進度值來控制多個規律物體,把他們的進度值錯開,形成先後順序。

===============================================================

到此,掌握這些知識已經可以做出很多新奇的設計。技術手段的文章可以告一個段落了。之後會介紹一些動畫設計上的乾貨。


推薦閱讀:

給「非設計師」的移動設計指南(大量實例)
生活的全部就是工作
【譯文】設計進化論
設計師該如何做好設計決策
快樂設計的8個方法

TAG:原型设计 | Framer | 设计师 |