隨著Google Material Design的出現,一種旨在跨平臺和裝置建立統一體驗的視覺語言由此橫空出世。Google通過“Material Guidelines”動畫部分描述的例子是如此地擬真,以致於許多人將這些互動視為Google品牌的一部分。
在本教程中,我們將向大家展示如何在Google Material Design規範的Radial Action下構建波紋效果,並結合SVG和GreenSock功能。
響應式動作
Google使用Radial Action定義Responsive Interaction如下:
Radial action is the visual ripple of ink spreading outward from the point of input.
The connection between an input event and on-screen action should be visually represented to tie them together. For touch or mouse, this occurs at the point of contact. A touch ripple indicates where and when a touch occurs and acknowledges that the touch input was received.
Transitions, or actions triggered by input events, should visually connect to input events. Ripple reactions near the epicenter occur sooner than reactions further away.
Google非常清楚地表述了輸入反饋應從原點出發,向外擴散。例如,如果使用者直接在中心點選按鈕,則紋波將從初始接觸點向外擴充套件。這就是我們如何指出觸控發生的地點和時間的方式,以便向使用者確認接收到的輸入。
SVG中的徑向動作
有許多開發人員創作紋波技術,主要使用CSS技術,如@keyframes,transitions,transforms偽技巧,border-radius以及甚至額外的標記,如span或div。不使用CSS,讓我們來看看如何通過GreenSock的TweenMax庫用SVG來建立這個徑向動作。
建立SVG
不管你信不信,其實我們並不需要如Adobe Illustrator或甚至Sketch這樣花哨的應用程式來創作這個效果。SVG的標記可以使用我們可能已經熟悉並用到工作中的幾個XML標籤來編寫。
1 2 3 |
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> <symbol viewbox="0 0 100 100"/> </svg> |
對於使用SVG精靈圖示的使用者,你會注意到的使用。symbol元素允許在單個symbol例項中匹配相關的XML,並隨後例項化它們,或者換句話說——就像蓋章一樣在整個應用程式中使用它們。每個蓋章的例項與其唯一的建立者相同:它所在的symbol。
symbol元素接受諸如viewBox和preserveAspectRatio之類的屬性,這些屬性可以在引用use元素定義的矩形視口中提供符合縮放比例的能力。Sara Soueidan寫了一篇精彩的文章,並建立了一個互動式工具,以幫助你瞭解viewBox座標系統。簡單地說就是,定義初始的x和y座標值(0,0),然後定義SVG畫布的寬度和高度(100,100)。
這個XML拼圖的下一個部分是新增我們打算動畫化為波紋的形狀。這是放入circle元素的地方。
1 2 3 4 5 |
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> <symbol viewbox="0 0 100 100"> <circle/> </symbol> </svg> |
circle需要一些更多的資訊,然後它才能在SVG的viewBox內正確地顯示。
1 |
<circle cx="1" cy="1" r="1"/> |
屬性cx和cy是相對於SVG viewBox的座標位置;我們的例子中就是symbol。為了使點選的時候感覺更自然,我們需要確保在接收到輸入時觸發點直接放在使用者手指下方。
上圖中間那個例子,其屬性建立了一個半徑為1px大小為2px × 2px的圓。這將確保我們的圓不會像最後那個示例中所看到的那樣裁剪。
1 2 |
<div style="height: 0; width: 0; position: absolute; visibility: hidden;" aria-hidden="true"> <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" focusable="false"><symbol id="ripply-scott" viewbox="0 0 100 100"><circle id="ripple-shape" cx="1" cy="1" r="1"/></symbol></svg></div> |
對於最後的觸控,我們將用包含內聯CSS的div來包裝它,以簡潔地隱藏sprite。這樣可以防止在渲染時佔用頁面中的空間。
在撰寫本文時,SVG精靈包含symbol塊引用它自己的漸變定義——正如你在演示中將看到的——通過ID找不到漸變和正確地渲染;使用visibility 屬性代替display的原因:none在Firefox和其他大多數瀏覽器上作為整個漸變都會失敗。
所有IE直到IE11都需要使用focusable=”false” ;除了Edge,因為它還沒有測試過。這是來自SVG 1.2規範的一個提案,描述了鍵盤焦點控制應該如何工作。IE實現了這一點,其他的瀏覽器則不行。為了與HTML一致,並且為了更好的控制,SVG 2將轉而採用tabindex。
編寫標記
讓我們寫一個語義的button元素作為我們的物件,以顯示此波紋。
1 |
<button>Click for Ripple</button> |
大多數我們熟悉的button的標記結構是直截了當的,包括一些填充文字。
1 2 3 4 5 6 |
<button> Click for Ripple <svg> <use xlink:href="#ripply-scott"></use> </svg> </button> |
為了利用先前建立的symbol元素,我們需要方法來引用它,通過使用按鈕的SVG中的use元素來引用符號的ID屬性值。
1 2 3 4 5 6 |
<button id="js-ripple-btn" class="button styl-material"> Click for Ripple <svg class="ripple-obj" id="js-ripple"> <use width="100" height="100" xlink:href="#ripply-scott" class="js-ripple"></use> </svg> </button> |
最終標記具備了CSS和JavaScript hooks的附加屬性。以“js-”開頭的屬性值表示僅存在於JavaScript中的值,因此刪除它們將阻礙互動,但不會影響樣式。這有助於區分CSS選擇器和JavaScript hooks,以避免在將來需要刪除或更新時相互混淆。
use元素必須有定義的寬度和高度,否則將不會對檢視者可見。你也可以在CSS中定義,如果你直接在元素本身上決定不要的話。
聯結點樣式
當編寫CSS的時候,要達到預期的效果你所要做的並不多。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
.ripple-obj { height: 100%; pointer-events: none; position: absolute; top: 0; left: 0; width: 100%; z-index: 0; fill: #0c7cd5; } .ripple-obj use { opacity: 0; } |
這就是在刪除用於一般樣式的宣告時,還留下的內容。pointer-events的使用消除了SVG紋波成為滑鼠事件的目標,因為我們只需要父物件反應:button元素。
紋波最初必須是不可見的,因此要將不透明度值設定為零。我們還將波紋物件定位在button的左上方。我們可以使波紋形狀居中,但是由於此事件是基於使用者互動而發生的,所以擔心位置沒有意義。
賦予它生機
賦予生機正是這個互動所有的意義。
1 2 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/1.17.0/TweenMax.min.js"/> <script src="js/ripple.js"/> |
為了動畫化波紋,我們將使用GreenSock的TweenMax庫,因為它是使用JavaScript對物件進行動畫處理的最佳庫之一;特別是涉及與動畫SVG跨瀏覽器有關的問題。
1 2 3 4 5 |
var ripplyScott = (function() {} return { init: function() {} }; })(); |
我們將要使用的模式是所謂的模組模式,因為它有助於隱藏和保護全域性名稱空間。
1 2 3 4 5 6 |
var ripplyScott = (function() {} var circle = document.getElementById('js-ripple'), ripple = document.querySelectorAll('.js-ripple'); function rippleAnimation(event, timing) {…} })(); |
為了解決問題,我們將抓取一些元素並將它們儲存在變數中;特別是use元素,它包含button內的svg。整個動畫邏輯將駐留在rippleAnimation函式中。該函式將接受動畫序列和事件資訊的時序引數。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
var ripplyScott = (function() {} var circle = document.getElementById('js-ripple'), ripple = document.querySelectorAll('.js-ripple'); function rippleAnimation(event, timing) { var tl = new TimelineMax(); x = event.offsetX, y = event.offsetY, w = event.target.offsetWidth, h = event.target.offsetHeight, offsetX = Math.abs( (w / 2) - x ), offsetY = Math.abs( (h / 2) - y ), deltaX = (w / 2) + offsetX, deltaY = (h / 2) + offsetY, scale_ratio = Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2)); } })(); |
我們定義了大量的變數,所以讓我們一個一個地討論這些變數所負責的內容。
1 |
var tl = new TimelineMax(); |
此變數建立動畫序列的時間軸例項以及所有時間軸在TweenMax中例項化的方式。
1 2 |
var x = event.offsetX; var y = event.offsetY; |
事件偏移量是一個只讀屬性,它將滑鼠指標的偏移值報告給目標節點的填充邊。在這個例子中,就是我們的button。x的事件偏移量從左到右計算,y的事件偏移量從上到下計算;都從零開始。
1 2 |
var w = event.target.offsetWidth; var h = event.target.offsetHeight; |
這些變數將返回按鈕的寬度和高度。最終計算結果將包括元素邊框和填充的大小。我們需要這個值才能知道我們的元素有多大,這樣我們才可以將波紋傳播到最遠的邊緣。
1 2 |
var offsetX = Math.abs( (w / 2) - x ); var offsetY = Math.abs( (h / 2) - y ); |
偏移值是點選距離元素中心的偏移距離。為了填滿目標的整個區域,波紋必須足夠大,可以從接觸點覆蓋到最遠的角落。使用初始x和y座標將不會再次將其從零開始,對於x,是從左到右的值,對於y,是從上到下的值。這種方法讓我們使用這些值的時候無論目標的中心點點選在哪一邊,都會檢測距離。
注意圓將如何覆蓋整個元素的過程,無論輸入的起始點何處發生。根據起始點的互動來覆蓋整個表面,我們需要做一些數學。
以下是我們如何使用464 x 82作為寬和高,391和45作為x和y座標來計算偏移量的過程:
1 2 |
var offsetX = (464 / 2) - 391 = -159 var offsetY = (82 / 2) - 45 = -4 |
通過將寬度和高度除以2來找到中心,然後減去由x和y座標檢測到的報告值。
Math.abs()方法返回數字的絕對值。使用上面的算術得到值159和4。
1 2 |
var deltaX = 232 + 159 = 391; var deltaY = 41 + 4 = 45; |
三角計算點選的整個距離,而不是距離中心的距離。選擇三角的原因是x和y總是從零開始從左到右,所以當相反方向(從右到左)點選的時候,我們需要方法來檢測點選。
學過基礎數學課程的小夥伴應該都知道勾股定理。公式為:高(a)的平方加底(b)的平方,得到斜邊(c)的平方。
a2 + b2 = c2
1 |
var scale_ratio = Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2)); |
使用這個公式讓我們來看一下計算:
1 |
var scale_ratio = Math.sqrt(Math.pow(391, 2) + Math.pow(45, 2)); |
Math.pow()方法返回第一個引數的冪;在這個例子中增加了一倍。391的2次方為152881。後面45的2次方等於2025。將這兩個值相加並取結果的平方根將留下393.58099547615353,這就是我們需要的波紋比例。
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 |
var ripplyScott = (function() { var circle = document.getElementById('js-ripple'), ripple = document.querySelectorAll('.js-ripple'); function rippleAnimation(event, timing) { var tl = new TimelineMax(); x = event.offsetX, y = event.offsetY, w = event.target.offsetWidth, h = event.target.offsetHeight, offsetX = Math.abs( (w / 2) - x ), offsetY = Math.abs( (h / 2) - y ), deltaX = (w / 2) + offsetX, deltaY = (h / 2) + offsetY, scale_ratio = Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2)); tl.fromTo(ripple, timing, { x: x, y: y, transformOrigin: '50% 50%', scale: 0, opacity: 1, ease: Linear.easeIn },{ scale: scale_ratio, opacity: 0 }); return tl; } })(); |
使用TweenMax中的fromTo方法,我們可以傳遞目標——波紋形狀——並設定包含整個運動序列方向的物件文字。鑑於我們想要從中心向外形成動畫,SVG需要將轉換原點設定為中間位置。考慮到我們想要之後要進行動畫處理,需要設定opacity 為1,因此縮放也需要調整到最小的位置。不知道你回想起了沒有,之前我們在CSS中設定了opacity為0的use元素以及我們從值1開始並返回到零的原因。最後部分是返回時間軸例項。
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 35 36 37 38 39 40 41 |
var ripplyScott = (function() { var circle = document.getElementById('js-ripple'), ripple = document.querySelectorAll('.js-ripple'); function rippleAnimation(event, timing) { var tl = new TimelineMax(); x = event.offsetX, y = event.offsetY, w = event.target.offsetWidth, h = event.target.offsetHeight, offsetX = Math.abs( (w / 2) - x ), offsetY = Math.abs( (h / 2) - y ), deltaX = (w / 2) + offsetX, deltaY = (h / 2) + offsetY, scale_ratio = Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2)); tl.fromTo(ripple, timing, { x: x, y: y, transformOrigin: '50% 50%', scale: 0, opacity: 1, ease: Linear.easeIn },{ scale: scale_ratio, opacity: 0 }); return tl; } return { init: function(target, timing) { var button = document.getElementById(target); button.addEventListener('click', function(event) { rippleAnimation.call(this, event, timing); }); } }; })(); |
返回的物件字面值將控制我們的波紋,方法是通過將事件偵聽器附加到所需的目標,呼叫rippleAnimation,以及最後傳遞我們將在下一步討論的引數。
1 |
ripplyScott.init('js-ripple-btn', 0.75); |
最後通過使用模組並傳遞init函式來對按鈕進行呼叫,init函式傳遞按鈕和序列的時序。看,就是這樣!
希望你喜歡這篇文章,並從中受到啟迪!歡迎使用不同的形狀來檢查演示,並檢視原始碼。不妨嘗試新的形狀、新的圖層形狀,最重要的是發揮你的想象力,放飛你的創意。
注意:其中一些技術是試驗性的,只能在現代瀏覽器中執行。
瀏覽器支援:Chrome Firefox Internet Explorer Safari Opera
在Github上檢視這個專案
譯文連結:http://www.codeceo.com/article/svg-material-design-ripple-button.html
英文原文:Creating Material Design Ripple Effects with SVG
翻譯作者:碼農網 – 小峰
[ 轉載必須在正文中標註並保留原文連結、譯文連結和譯者等資訊。]