乐趣区

关于后端:炫酷的3D球体文字云效果

起因

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

外围原理

文字坐标

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

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 的函数为

 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 成员变量中。

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)
    }
}
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,批改每个文字的坐标,实现旋转。

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

文末

您的点赞珍藏就是对我最大的激励!
欢送关注我,分享 Android 干货,交换 Android 技术。
对文章有何见解,或者有何技术问题,欢送在评论区一起留言探讨!

退出移动版