- 原文地址:Playing with Paths
- 原文作者:Nick Butcher
- 譯文出自:掘金翻譯計劃
- 本文永久連結:github.com/xitu/gold-m…
- 譯者:IllllllIIl
- 校對者:LeeSniper
玩轉 Paths
我最近幫別人實現了一個 app 裡面英雄人物的動畫。然而,我現在還不能把這個動畫分享給你們。但我想分享在實現它的過程中學到的東西。在這篇文章中,我將回顧如何重現這些由 Dave ‘beesandbombs’ Whyte 展示的迷人動畫,其中演示了很多一樣的實現技巧。
beesandbombs 展示的多邊形繞圈
當我看到這個時(對熟悉我工作的人來說可能不是很驚訝),第一想法是使用 AnimatedVectorDrawable (下文會簡稱為 AVD
)。AVD
很好用,但不是適用所有的情況 —— 特別是我們有如下的需求的話:
- 我知道我們需要畫一個多邊形,但還沒卻確定具體要畫哪個形狀。
AVD
是需要預先設定引數的動畫,即改變形狀需要重新設定動畫。 - 關於動畫進度追蹤的問題,我們只想要繪製多邊形的一部分。
AVD
是“義無反顧”地執行任務,如果動畫開始後,它會完整地執行完整個動畫,換句話說你不能取消它。 - 我們想要使另一個物體繞著多邊形運動。這個當然也可以通過
AVD
實現。但它還是需要很多事前工作去計算想生成的軌跡。 - 我們想把繞多邊形物體的運動進度與多邊形的顯示分離開來,獨立控制。
因此我選擇用自定義 Drawable 來實現,其中包含多個 Path 物件。Path
是對圖形形狀的基本描繪(AVD 中實際也使用了 Path!),而且 Android Canvas 的 API也是藉助 Path 來生成各種有趣的效果。在實現一些效果之前,我想強烈推薦 Romain Guy 這篇寫得很好的文章,裡面展示的很多技巧就是我在本文所用到的:
Android Recipe #4, path tracing
極座標系
當定義 2d 形狀的時候,我們通常在笛卡爾座標系 (x,y) 中進行定義。通過指定 x 軸和 y 軸上離原點的距離,來定義圖形形狀。而另一個我們可選用的極座標系,則是定義離原點的角度和半徑長度。
笛卡爾座標系(左邊)vs 極座標系(右邊)
我們可以通過這兩條公式進行極座標系和笛卡爾座標系之間的轉換:
val x = radius * Math.cos(angle);
val y = radius * Math.sin(angle);
複製程式碼
我強烈推薦讀下面這篇文章以瞭解更多關於極座標系的內容:
為了能生成規則的多邊形(例如每個內角的度數相同),極座標系能起到非常大的作用。為了生成想要的邊數,你可以通過計算求出對應的度數(因為內角度數和是 360 度),然後藉助同一個半徑,再利用這個度數的多個倍數關係去描繪出每個點。 你可以用圖形 API 將這些點座標轉化為笛卡爾座標。下面是一個通過給定的邊數和半徑生成多邊形 Path
的函式:
fun createPath(sides: Int, radius: Float): Path {
val path = Path()
val angle = 2.0 * Math.PI / sides
path.moveTo(
cx + (radius * Math.cos(0.0)).toFloat(),
cy + (radius * Math.sin(0.0)).toFloat())
for (i in 1 until sides) {
path.lineTo(
cx + (radius * Math.cos(angle * i)).toFloat(),
cy + (radius * Math.sin(angle * i)).toFloat())
}
path.close()
return path
}
複製程式碼
所以為了生成想要的多邊形組合,我們建立了一個有不同邊數、半徑和顏色的多邊形 list 集合。Polygon
是一個持有這些資訊和計算相應 Path
的類:
private val polygons = listOf(
Polygon(sides = 3, radius = 45f, color = 0xffe84c65.toInt()),
Polygon(sides = 4, radius = 53f, color = 0xffe79442.toInt()),
Polygon(sides = 5, radius = 64f, color = 0xffefefbb.toInt()),
...
)
複製程式碼
有效的 path 繪製
繪製一個 Path 只需簡單地呼叫 Canvas.drawPath(path, paint) 但是 Paint 類的引數支援 PathEffect,藉助這個我們可以去更改 path 被繪製時的效果。 例如我們可以使用 CornerPathEffect 去把我們的多邊形的各個角圓滑化處理或者是用 DashPathEffect 去分段地畫出 Path
(虛線效果,譯者注)(關於這個技巧的更多細節,請閱讀前面提到的那篇 Path tracing 文章 ):
另外一種畫分段 path 的方法是使用 PathMeasure#getSegment,它能複製 path 的某一部分到一個新的 Path 物件。我是直接使用了能畫出虛線的方法,就像自己改變了繪製的時間間隔和分段繪製實現的效果一樣。
通過暴露這些控制 drawable 特性的引數,我們可以很容易地生成動畫:
object PROGRESS : FloatProperty<PolygonLapsDrawable>("progress") {
override fun setValue(pld: PolygonLapsDrawable, progress: Float) {
pld.progress = progress
}
override fun get(pld: PolygonLapsDrawable) = pld.progress
}
...
ObjectAnimator.ofFloat(polygonLaps, PROGRESS, 0f, 1f).apply {
duration = 4000L
interpolator = LinearInterpolator()
repeatCount = INFINITE
repeatMode = RESTART
}.start()
複製程式碼
例如,這是繪製同心圓多邊形 path 過程的不同動畫效果:
吸附在 path 上
為了繪製某個沿著 path 的物體,我們可以使用 PathDashPathEffect. 這會把另一個 Path
沿著某條 path “點印”在它上面,例如像這樣以藍色圓形形狀沿著一個多邊形的邊點印在上面:
PathDashPathEffect
接收 advance
和 phase
兩個引數 —— 分別對應每個 stamp(繪製在 path 上面的物體,譯者注)之間的間距和繪製第一個 stamp 在 path 上的偏移量。通過把每個 stamp 的間距設定為和整個 path 的長度一樣(通過 PathMeasure#getLength 獲取), 我們就可以只繪製出一個 stamp。然後再通過不斷改變偏移量,(偏移量是由 dotProgress
範圍 [0, 1] 控制)我們就可以實現只有一個 stamp 沿著 path 在運動的動畫效果。
val phase = dotProgress * polygon.length
dotPaint.pathEffect = PathDashPathEffect(pathDot, polygon.length,
phase, TRANSLATE)
canvas.drawPath(polygon.path, dotPaint)
複製程式碼
我們現在有生成我們圖形的所有要素。通過新增另一個引數,就是每個點在每個多邊形上所對應的第幾“圈”的圈數,每個點會完成對應的繞圈動畫。能生成像這樣的效果:
通過 Android drawable 實現原本 gif 的效果
你可以通過下面的連結獲得這個 drawable 的原始碼: gist.github.com/nickbutcher…
展示不同的效果
你們可能已經注意到 PathDashPathEffect 構造方法中最後的引數:Style。這個列舉類控制在 path 上面的 stamp 在每個位置上是如何被繪製的。為了展示這個引數的使用,下面的例子使用了一個三角形 stamp 代替圓形,去展示平移(translate)
和旋轉(rotate)
的效果差別:
比較平移
和旋轉
效果的異同
注意到使用 translate
效果時,三角形 stamp 方向總是相同的(箭頭方向指向左)而如果是 rotate
效果的話,三角形會旋轉自身保持在處於 path 的切線方向上。
還有一種 型別
叫做 morph
,能讓 stamp 平穩變換。為了展示這個效果,我把 stamp 變成了如下的一條線段。請觀察當經過角落時,線段是如何彎曲的:
當PathDashPathEffect.Style的型別為 MORPH
有趣的是,某些情況下,在 path 的開頭或緊密的角落,stamp 的形狀有點扭曲。
提醒一點你可以使用
ComposePathEffect
去組合多種PathEffect
在一起,通過將PathDashPathEffect
和CornerPathEffect
一起組合使用,可以實現讓 stamp 在有圓滑角落的 path 上運動。
使用正切
上面我們所討論的是關於如何生成多邊形的繞圈組合,而我最初的需求實際上還要麻煩點。使用 PathDashPathEffect
的缺點是隻能應用一種單一的形狀和顏色。我自己的作品需要有更精巧的標記(marker,即 stamp,譯者注),所以我用一種比點印在 path 上更好的辦法。我使用了 Drawable
並且計算給定一個進度的話,沿著 Path 標記需要在哪個地方繪製出來。
沿著 path 移動 VectorDrawable
為了實現這個效果,我再次使用 PathMeasure
類,它提供了 getPosTan 方法獲取位置座標,和沿著某個 Path 給定長度時的正切值。通過這樣(涉及到一點數學),我們可以平移和旋轉畫布,從而讓我們的標記
繪製在正確的位置和方向上。
pathMeasure.setPath(polygon.path, false)
pathMeasure.getPosTan(markerProgress * polygon.length, pos, tan)
canvas.translate(pos[0], pos[1])
val angle = Math.atan2(tan[1].toDouble(), tan[0].toDouble())
canvas.rotate(Math.toDegrees(angle).toFloat())
marker.draw(canvas)
複製程式碼
找到你的 path
希望這篇文章能夠說明自定義 drawable 的同時去建立和操作 path 對於生成有趣的圖形效果是多麼有用。 編寫一個自定義 drawable,在單獨更改各部分的動畫效果這方面有很靈活的控制。這個方法也能讓你動態更改數值,而不用需要預先就設定好整個動畫。期待你們通過 Android 的 Path API 和其他內建效果實現更多新奇的效果,而這些工具早在 API 1 的時候就已經可以使用了。
掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。