The cause of

A few days ago, I saw a h5 more gorgeous 3D sphere text effect on the Internet, I feel very interesting, and I’m going to reproduce it on the Android side. Without any nonsense, let’s take a look at the effect (GIF looks like some cards, but actually it won’t).

card

Core principles

Text coordinates

The first thing TO do is to determine a coordinate for each text. Android uses left-handed coordinates, and our effect is a sphere, so I used spherical coordinates to calculate the coordinates for each text.

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))
Copy the code

Where radius is the length of the line from the center of the circle to the sphere, as well as the radius of the sphere; upDegree is the included Angle between the line and the positive direction of the Y-axis, ranging from [0,180]; bottomDegree is the included Angle between the projection of the line on the plane determined by the xz axis and the positive direction of the X-axis, ranging from [0,360].

Text color and size

When the Angle between the text and the positive direction of the X axis is 90 degrees, the text is the largest and the color is the darkest, 270 degrees is the smallest and the color is the lightest, 270 degrees to 360 degrees is the reverse process of the above process. For this purpose, we define a variable, factor, to describe how much the color and size of the text will change, in the range of [minFactor, 1]. MinFactor can be passed in as an external variable.

According to the previous description, we can determine that the function of factor is

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} else -> {-1.0 / math.pi * math.toradians (bottomDegree) + 1.5}})Copy the code

By constructing three piecewise linear functions at different angles.

Calculate literal coordinates

Define the WordItem class to represent each text, coordinate, and its corresponding factor. Calculate the corresponding coordinate for all text when onMeasure, and store it in the wordItemList member variable.

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)
    }
}

Copy the code
 private fun genWordItemList(a): 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
    }
Copy the code

Rendering text

Firstly, set the size of brush text and the corresponding alpha value according to factor, and then calculate the corresponding position according to the size of the text to draw, and constantly increase the bottomDegreeOffset, modify the coordinates of each text, to achieve rotation.

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()
        }
Copy the code

warehouse

The code has been uploaded to Github

Welcome to my official account, “Old arsenic on a skateboard.”