炫酷的3D球体文字云效果!

起因

前些日子在网上看到了一个h5的比较炫的3D球体文字效果,感觉挺有意思,就准备在Android侧进行一下复现,废话少说,先看一下效果(gif看上去有些卡,实际不会)

核心原理

文字坐标

首先要做的就是为每个文字确定一个坐标,Android采用的是左手坐标系,而且我们的效果又是一个球体,所以我采用了球面坐标系计算每个文字的坐标。

代码语言:javascript
复制
y = radius * cos(Math.toRadians(this.upDegree))
z = -radius * sin(Math.toRadians(this.upDegree)) * sin(Math.toRadians(this.bottomDegree))
x = radius * sin(Math.toRadians(this.upDegree)) * cos(Math.toRadians(this.bottomDegree))

其中radius为圆心到球面的连线长度,也就是球体的半径,upDegree为连线与y轴正方向的夹角,范围为[0,180],bottomDegree为连线在xz轴确定的平面上的投影与x轴正方向的夹角,范围为[0,360].

文字颜色与大小

当文字转到与x轴正方向夹角为90度的时候,此时文字最大,颜色最深,270度时最小,颜色最浅,270度到360度则是上述过程的逆过程。为此我们定义一个变量factor用于描述文字颜色和大小的改变程度,范围为【minFactor,1】minFactor可以通过外部变量传入。

根据前面的描述,我们可以确定factor的函数为

代码语言:javascript
复制
 factor = minFactor.coerceAtLeast(
            when (bottomDegree) {
                in 0.0..90.0 -> {
                    1.0 / Math.PI * Math.toRadians(bottomDegree) + 0.5
                }
                in 270.0..360.0 -> {
                    1.0 / Math.PI * Math.toRadians(bottomDegree) - 1.5
                }
                else -> {
                    -1.0 / Math.PI * Math.toRadians(bottomDegree) + 1.5
                }
            }
        )

通过在不同的角度我构建了三个分段的线性函数来表示。

计算文字坐标

定义类WordItem用以表示每个文字,坐标以及其对应的factor,在onMeasure的时候为所有文字计算相应的坐标,并存储在wordItemList成员变量中。

代码语言:javascript
复制
class WordItem(
    var text: String,
    var upDegree: Double = 0.0,
    var bottomDegree: Double = 0.0,
    var x: Double = 0.0,
    var y: Double = 0.0,
    var z: Double = 0.0,
    var factor: Double = 0.0
) {
fun cal(radius: Double, upDegree: Double, bottomDegree: Double, minFactor: Double) {
    this.upDegree = upDegree % 180
    this.bottomDegree = bottomDegree % 360
    y = radius * cos(Math.toRadians(this.upDegree))
    z = -radius * sin(Math.toRadians(this.upDegree)) * sin(Math.toRadians(this.bottomDegree))
    x = radius * sin(Math.toRadians(this.upDegree)) * cos(Math.toRadians(this.bottomDegree))
    factor = minFactor.coerceAtLeast(
        when (bottomDegree) {
            in 0.0..90.0 -> {
                1.0 / Math.PI * Math.toRadians(bottomDegree) + 0.5
            }
            in 270.0..360.0 -> {
                1.0 / Math.PI * Math.toRadians(bottomDegree) - 1.5
            }
            else -> {
                -1.0 / Math.PI * Math.toRadians(bottomDegree) + 1.5
            }
        }
    )
}

fun move(radius: Double, upOffset: Double, bottomOffset: Double, minFactor: Double) {
    cal(radius, upDegree + upOffset, bottomDegree + bottomOffset, minFactor)
}

}

代码语言:javascript
复制
private fun genWordItemList(): MutableList<WordItem>? {
wordList?.let { list ->
val wordItemList = mutableListOf<WordItem>()
var upDegree = 0.0
for (row in 0 until circleRowNum) {
upDegree += upDegreeGap
upDegree %= 180.0
var bottomDegree = 0.0
for (col in 0 until perNumInCircle) {
val index = row * perNumInCircle + col
if (index < wordList?.size ?: 0) {
bottomDegree += bottomDegreeGap
bottomDegree %= 360.0
val wordItem = WordItem(list[index])
wordItem.cal(radius, upDegree, bottomDegree, minFactor)
wordItemList.add(wordItem)
}
}
}
return wordItemList
}
return null
}

绘制文字

首先根据factor设置画笔文字的大小以及相应的alpha值,然后在根据文字大小计算其相应的位置,进行绘制,并且不断增加bottomDegreeOffset,修改每个文字的坐标,实现旋转。

代码语言:javascript
复制
canvas?.let { canvas ->
wordItemList?.forEach { wordItem ->
wordItem.move(radius, 0.0, 1.0, minFactor)
paint.textSize = (wordItem.factor * maxTextSize).toFloat()
paint.alpha = 30.coerceAtLeast((wordItem.factor * 255).toInt())
textRect.setEmpty()
paint.getTextBounds(wordItem.text, 0, wordItem.text.length, textRect)
canvas.drawText(
wordItem.text,
((width - paddingLeft - paddingRight) / 2 + wordItem.x - textRect.width() / 2).toFloat(),
((height - paddingTop - paddingBottom) / 2 + wordItem.y - textRect.height() / 2).toFloat(),
paint
)
}
postInvalidate()
}

Android高级开发系统进阶笔记、最新面试复习笔记PDF,我的GitHub

文末
对文章有何见解,或者有何技术问题,欢迎在评论区一起留言讨论!