The source address

Douyin Internet celebrity text Clock -TextClockView

The origin of

When I was surfing Douyin at home over the weekend, I saw this Internet celebrity clock, which is on Android platform, so I thought WHY not make it my own. According to the videos posted on Tiktok, there are basically two types of clocks, one is displayed on the “wallpaper” and the other is displayed on the “lock screen”.

Showing “wallpaper” can be done through the LiveWallPaper API, which is how this topic will be implemented.

To display the “lock screen” visual inspection is to use the relevant API of each ROM manufacturer, the development of lock screen theme can be done.

However, the basic way to achieve both is to draw the clock in Canvas Paint, etc. So in the last part, I first drew the clock in the custom View mode and displayed the effect in the Activity. In the next post, I will combine this View with the LiveWallPaper Settings.

Thinking analysis

This is the reference of the screenshot I took at that time. First, let’s analyze the elements and style expression involved:

  1. “Information in a Circle” the digital time + digital date + text day in the center of the circle is always white
  2. “Hour circle” a circle of text hour, one, two… At twelve o ‘clock, the current number is white, the other number is white + transparency, and ten o ‘clock is white.
  3. One minute, one minute, two minutes… Fifty-nine minutes, sixty minutes are empty, and the current minute is white, and the other minutes are white + transparency.
  4. “Second circle” a circle of text seconds, one second, two seconds.. Fifty-nine seconds. Sixty seconds is empty. Same thing.

Then analyze the animation effect:

  1. One “second turn” is made every second, and the Angle of rotation is360 ° / 60 = 6 °And there was one on the wayLinear rotationPast animation effects.
  2. The rotation Angle and animation effect are the same as that of the “second circle”.
  3. Every hour “time circle”, rotation Angle is12 = 30 ° 360 ° /, the animation effect is the same as above.

Draw a static diagram

1. Prepare canvas

Basically fill the background of the canvas with black, and then move the origin of the canvas to the center of the size of the View for easy thinking and drawing.


// Calculate the width and height of the View without the padding in the onLayout method
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
    super.onLayout(changed, left, top, right, bottom)
    mWidth = (measuredWidth - paddingLeft - paddingRight).toFloat()
    mHeight = (measuredHeight - paddingTop - paddingBottom).toFloat()
    
    // This will be covered later
    // Use the View width * coefficient to handle the size, which can be linked to the style
    mHourR = mWidth * 0.143f
    mMinuteR = mWidth * 0.35f
    mSecondR = mWidth * 0.35f
}

// Shift the canvas origin to the center in the onDraw method
override fun onDraw(canvas: Canvas?). {
    super.onDraw(canvas)
    if (canvas == null) return
    canvas.drawColor(Color.BLACK)// Fill the background
    canvas.save()
    canvas.translate(mWidth / 2, mHeight / 2)// The origin moves to the center
    
    // Draw each component, as described later
    drawCenterInfo(canvas)
    drawHour(canvas, mHourDeg)
    drawMinute(canvas, mMinuteDeg)
    drawSecond(canvas, mSecondDeg)

    // Draw an auxiliary line to the right from the origin
    canvas.drawLine(0f, 0f, mWidth, 0f, mHelperPaint)

    canvas.restore()
}
Copy the code

2. Draw “Message in circle”

After the first step, in the Xml Preview of AS, you can see a screen of black + a red line from the center of the screen to the right border. (At first glance, still pretty)

/** * Draw the information in the circle */
private fun drawCenterInfo(canvas: Canvas) {
    Calendar.getInstance().run {
        // Draw the numeric time
        val hour = get(Calendar.HOUR_OF_DAY)
        val minute = get(Calendar.MINUTE)

        mPaint.textSize = mHourR * 0.4f// The font size is calculated according to the "time circle" radius
        mPaint.alpha = 255
        mPaint.textAlign = Paint.Align.CENTER
        canvas.drawText("$hour:$minute".0f, mPaint.getBottomedY(), mPaint)

        // Draw month and week
        val month = (this.get(Calendar.MONTH) + 1).let {
            if (it < 10) "0$it" else "$it"
        }
        val day = this.get(Calendar.DAY_OF_MONTH)
        val dayOfWeek = (get(Calendar.DAY_OF_WEEK) - 1).toText()// A private extension method that converts an Int number to one, eleven, twenty, etc. This method is used to draw three text circles

        mPaint.textSize = mHourR * 0.16f// The font size is calculated according to the "time circle" radius
        mPaint.alpha = 255
        mPaint.textAlign = Paint.Align.CENTER
        canvas.drawText("$month.$dayweek$dayOfWeek".0f, mPaint.getTopedY(), mPaint)
    }
}

/** * The extension gets the vertically centered y coordinate on the x axis */ when drawing text
private fun Paint.getCenteredY(a): Float {
    return this.fontSpacing / 2 - this.fontMetrics.bottom
}

/** * The extension gets the y coordinate of the upper edge of the X-axis when drawing the text */
private fun Paint.getBottomedY(a): Float {
    return -this.fontMetrics.bottom
}

/** * The extension gets the y coordinate on the X-axis close to the lower edge of the X-axis */
private fun Paint.getToppedY(a): Float {
    return -this.fontMetrics.ascent
}
Copy the code

Among them is mPaint. GetBottomedY () mPaint. GetToppedY (), which are two Kotlin methods that extend to Paint brushes. Their purpose is to handle the alignment of text with the x axis. The third argument to the canvas.drawtext () method is the y-coordinate, but this refers to the y-coordinate of the Baseline text, so the utility method is written to get the corrected y-coordinate. (I’ll just throw out this dot here. Check out the Paint API for details. I’ll link to the article I read at the end of this article.)

Take drawing digital time as an example to show the different effects:

Replace mPaint. GetBottomedY () with 0f(y = 0, Baseline = 0) and use 15:67 ABC JQK to see the difference. (The red line is the beautiful auxiliary line drawn above)

canvas.drawText("15:67 Test text ABC JQK".0f, 0f, mPaint)

canvas.drawText("15:67 Test text ABC JQK".0f, mPaint.getBottomedY(), mPaint)
Copy the code

Ok, “Message in circle” will look like this when drawn:

3. Draw “hour circle”, “minute circle” and “second circle”

The idea is to loop for 12 times, rotate the canvas 30° times I each time, and draw the text in the specified position. After 12 times, it makes a circle.

This method receives an degrees: Float parameter that controls the overall rotation of the “time circle”, which is animated by changing this value. And because the animation direction of the three circles is counterclockwise, this degree will always be a negative value.

/** ** draw hours */
private fun drawHour(canvas: Canvas, degrees: Float) {
    mPaint.textSize = mHourR * 0.16f

    // Handle the overall rotation
    canvas.save()
    canvas.rotate(degrees)

    for (i in 0 until 12) {
        canvas.save()

        // Rotate from the X-axis, draw the "points" every 30°, complete the "time circle" 12 times.
        val iDeg = 360 / 12f * i
        canvas.rotate(iDeg)
        
        // This deals with the transparency of the current point in time because degrees controls the overall rotation counterclockwise
        //iDeg controls the drawing clockwise, so when the sum of the two is 0, it is exactly on the positive x axis, which is the starting drawing position.
        mPaint.alpha = if (iDeg + degrees == 0f) 255 else (0.6f * 255).toInt()
        mPaint.textAlign = Paint.Align.LEFT

        canvas.drawText("${(i + 1).toText()}Point", mHourR, mPaint.getCenteredY(), mPaint)
        canvas.restore()
    }

    canvas.restore()
}
Copy the code

Draw “minute circle” and “second circle” in the same way

/** * draw minutes */
private fun drawMinute(canvas: Canvas, degrees: Float) {
    mPaint.textSize = mHourR * 0.16f

    // Handle the overall rotation
    canvas.save()
    canvas.rotate(degrees)

    for (i in 0 until 60) {
        canvas.save()

        val iDeg = 360 / 60f * i
        canvas.rotate(iDeg)

        mPaint.alpha = if (iDeg + degrees == 0f) 255 else (0.6f * 255).toInt()
        mPaint.textAlign = Paint.Align.RIGHT

        if (i < 59) {
            canvas.drawText("${(i + 1).toText()}Points", mMinuteR, mPaint.getCenteredY(), mPaint)
        }
        canvas.restore()
    }

    canvas.restore()
}

/** ** draw seconds */
private fun drawSecond(canvas: Canvas, degrees: Float) {
    mPaint.textSize = mHourR * 0.16f

    // Handle the overall rotation
    canvas.save()
    canvas.rotate(degrees)

    for (i in 0 until 60) {
        canvas.save()

        val iDeg = 360 / 60f * i
        canvas.rotate(iDeg)

        mPaint.alpha = if (iDeg + degrees == 0f) 255 else (0.6f * 255).toInt()
        mPaint.textAlign = Paint.Align.LEFT

        if (i < 59) {
            canvas.drawText("${(i + 1).toText()}SEC. "", mSecondR, mPaint.getCenteredY(), mPaint)
        }
        canvas.restore()
    }

    canvas.restore()
}
Copy the code

DuangDuang!!!!! The effect has come out

4. Turn the clock

So how do you make the clock go round? Taking a look at the code in onDraw(), the method that draws all three circles receives a corresponding degrees: Float parameter, which controls the overall rotation of a circle and rotates counterclockwise, so it always has to be negative.

So that’s it, you just control these three angles, and you can make the clock move.

override fun onDraw(canvas: Canvas?). {
    super.onDraw(canvas)
    .../ / to omit
    
    // Draw each component, as described later
    drawCenterInfo(canvas)
    drawHour(canvas, mHourDeg)
    drawMinute(canvas, mMinuteDeg)
    drawSecond(canvas, mSecondDeg)

    .../ / to omit
}
Copy the code

Define the global variables for the three angles and associate them with the actual time, then trigger a redraw of the View every second.

// Define global variables for three angles
private var mHourDeg: Float by Delegates.notNull()
private var mMinuteDeg: Float by Delegates.notNull()
private var mSecondDeg: Float by Delegates.notNull()

/** * draw method */
fun doInvalidate(a) {
    Calendar.getInstance().run {
        val hour = get(Calendar.HOUR)
        val minute = get(Calendar.MINUTE)
        val second = get(Calendar.SECOND)

        // Here, the three angles are associated with the actual time. The current time is a few minutes and a few seconds, and the corresponding circle is rotated counterclockwise
        mHourDeg = - 360. / 12f * (hour - 1)
        mMinuteDeg = - 360. / 60f * (minute - 1)
        mSecondDeg = - 360. / 60f * (second - 1)

        invalidate()
    }
}
Copy the code

Then simply use the timer in the Activity to refresh the View every second. The effect is shown below. You can see that the rotation is rotating, but it is a hop per second. Take a look at our analysis:

One “second turn” per second, 360°/60=6°, and a linear rotation animation.

So we still need a linear rotation effect.

// Code in the Activity
private var mTimer: Timer? = null
private fun caseTextClock(a) {
    setContentView(R.layout.activity_stage_text_clock)

    mTimer = timer(period = 1000) {
        runOnUiThread {
            stage_textClock.doInvalidate()
        }
    }

}

override fun onDestroy(a) {
    super.onDestroy() mTimer? .cancel() }Copy the code

5. Let the clock run gracefully

Based on what we already know, the essence of the clock is to constantly change the value of the parameter Degrees: Float over a period of time (say 150ms) and trigger the redraw method, thus creating an animation effect as seen by human eyes.

So, we wanted to make the turn of the second (the representation of the three turns) a little more linear and elegant, so that we could make a linear turn of 6° for the first 150ms before starting to draw the new second.

init {
    // Handle the animation and declare the global handler
    mAnimator = ValueAnimator.ofFloat(6f, 0f)// From 6 to 1
    mAnimator.duration = 150
    mAnimator.interpolator = LinearInterpolator()// Set the interpolator to linear
    doInvalidate()
}

/** * start drawing */
fun doInvalidate(a) {
    Calendar.getInstance().run {
        val hour = get(Calendar.HOUR)
        val minute = get(Calendar.MINUTE)
        val second = get(Calendar.SECOND)

        mHourDeg = - 360. / 12f * (hour - 1)
        mMinuteDeg = - 360. / 60f * (minute - 1)
        mSecondDeg = - 360. / 60f * (second - 1)

        // Record the current Angle, then rotate the second by 6° linearly
        val hd = mHourDeg
        val md = mMinuteDeg
        val sd = mSecondDeg

        // Handle animations
        mAnimator.removeAllUpdateListeners()// The previous listener needs to be removed
        mAnimator.addUpdateListener {
            val av = (it.animatedValue as Float)

            if (minute == 0 && second == 0) {
                mHourDeg = hd + av * 5// The rotation Angle of time circle is 5 times of minute second, linear rotation is 30°
            }

            if (second == 0) {
                mMinuteDeg = md + av// Linear rotation 6°
            }

            mSecondDeg = sd + av// Linear rotation 6°

            invalidate()
        }
        mAnimator.start()
    }
}
Copy the code

Let’s end with this beautiful and elegant clock

At the end of the article

My personal ability is limited. If there is something wrong, I welcome you to criticize and point it out. I will accept it humbly and modify it in the first time so as not to mislead you.

Read the article

  • View 1-3 drawText()
  • Everything you know and Don’t Know about Android Paint

My other articles

  • [Custom View] Douyin popular text clock – Part 1
  • 【 custom View】 onion math same style ShadowLayout -ShadowLayout
  • 【 custom View】 Onion math with the same radar map in-depth analysis -RadarView
  • 【 custom View】 Onion mathematics with the same Banner evolution -BannerView