[譯]玩轉 Android Paths

IllllllIIl發表於2018-02-08

玩轉 Paths

我最近幫別人實現了一個 app 裡面英雄人物的動畫。然而,我現在還不能把這個動畫分享給你們。但我想分享在實現它的過程中學到的東西。在這篇文章中,我將回顧如何重現這些由 Dave ‘beesandbombs’ Whyte 展示的迷人動畫,其中演示了很多一樣的實現技巧。

[譯]玩轉 Android Paths

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 軸上離原點的距離,來定義圖形形狀。而另一個我們可選用的極座標系,則是定義離原點的角度和半徑長度。

[譯]玩轉 Android Paths

笛卡爾座標系(左邊)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
}
複製程式碼

[譯]玩轉 Android Paths

所以為了生成想要的多邊形組合,我們建立了一個有不同邊數、半徑和顏色的多邊形 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()),
  ...
)
複製程式碼

[譯]玩轉 Android Paths

有效的 path 繪製

繪製一個 Path 只需簡單地呼叫 Canvas.drawPath(path, paint) 但是 Paint 類的引數支援 PathEffect,藉助這個我們可以去更改 path 被繪製時的效果。 例如我們可以使用 CornerPathEffect 去把我們的多邊形的各個角圓滑化處理或者是用 DashPathEffect 去分段地畫出 Path(虛線效果,譯者注)(關於這個技巧的更多細節,請閱讀前面提到的那篇 Path tracing 文章 ):

[譯]玩轉 Android Paths

另外一種畫分段 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 過程的不同動畫效果:

[譯]玩轉 Android Paths

吸附在 path 上

為了繪製某個沿著 path 的物體,我們可以使用 PathDashPathEffect. 這會把另一個 Path 沿著某條 path “點印”在它上面,例如像這樣以藍色圓形形狀沿著一個多邊形的邊點印在上面:

[譯]玩轉 Android Paths

PathDashPathEffect 接收 advancephase 兩個引數 —— 分別對應每個 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 Paths

通過 Android drawable 實現原本 gif 的效果

你可以通過下面的連結獲得這個 drawable 的原始碼: gist.github.com/nickbutcher…

展示不同的效果

你們可能已經注意到 PathDashPathEffect 構造方法中最後的引數:Style。這個列舉類控制在 path 上面的 stamp 在每個位置上是如何被繪製的。為了展示這個引數的使用,下面的例子使用了一個三角形 stamp 代替圓形,去展示平移(translate)旋轉(rotate)的效果差別:

[譯]玩轉 Android Paths

比較平移旋轉效果的異同

注意到使用 translate 效果時,三角形 stamp 方向總是相同的(箭頭方向指向左)而如果是 rotate 效果的話,三角形會旋轉自身保持在處於 path 的切線方向上。

還有一種 型別 叫做 morph,能讓 stamp 平穩變換。為了展示這個效果,我把 stamp 變成了如下的一條線段。請觀察當經過角落時,線段是如何彎曲的:

[譯]玩轉 Android Paths

當PathDashPathEffect.Style的型別為 MORPH

有趣的是,某些情況下,在 path 的開頭或緊密的角落,stamp 的形狀有點扭曲。

提醒一點你可以使用 ComposePathEffect 去組合多種 PathEffect 在一起,通過將 PathDashPathEffectCornerPathEffect 一起組合使用,可以實現讓 stamp 在有圓滑角落的 path 上運動。

使用正切

上面我們所討論的是關於如何生成多邊形的繞圈組合,而我最初的需求實際上還要麻煩點。使用 PathDashPathEffect 的缺點是隻能應用一種單一的形狀和顏色。我自己的作品需要有更精巧的標記(marker,即 stamp,譯者注),所以我用一種比點印在 path 上更好的辦法。我使用了 Drawable 並且計算給定一個進度的話,沿著 Path 標記需要在哪個地方繪製出來。

[譯]玩轉 Android Paths

沿著 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 的時候就已經可以使用了。


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章