Out of the box source address

Onion math same radar map -RadarView

Support for XML custom attributes:

  • rv_webRadius: Radius of the radar net (this property determines the width and height of the View)
  • rv_webMaxProgress: Maximum progress of each attribute
  • rv_webLineColor: Color of the radar net
  • rv_webLineWidth: Line width of the radar net
  • rv_textArrayedColor: The color of each attribute text
  • rv_textArrayedFontPath: Font path for each property text and center name
  • rv_areaColor: The color of the central connection area
  • rv_areaBorderColor: Border color of the center connection area
  • rv_textCenteredName: Name of the centre
  • rv_textCenteredColor: Color of the center text
  • rv_textCenteredFontPath: Font path for the center numeric text
  • rv_animateTime: Animation execution time
  • rv_animateMode: Animation mode
    • TIME: The animation execution time isrv_animateTime
    • SPEED: The animation execution speed must beRv_webMaxProgress ➗ rv_animateTime

Support code to set the data source:

  • setTextArray(textList: List<String>): Sets the text array of each property,You can’t have less than 3 elements
  • setProgressList(progressList: List<Int>): Set the progress array corresponding to each attribute, the array element is 0 by default,The number of elements must be the same as the literal array
  • setOldProgressList(oldProgressList: List<Int>): Set the corresponding progress array of each property before animation execution, the array element is 0 by default,The number of elements must be the same as the literal array

Support code execution animation:

  • doInvalidate(): Animation of each property is executed together
  • doInvalidate(index: Int, block: ((Int) -> Unit)? = null): Specifies a property that executes the animation and can be passed in as a parameter to receive a callback at the end of the animation

The origin of

Recently, our company has been reconstructing a business on the product side, and the UI has also been changed, which involves radar map, so we have this encapsulated RadarView, its main features are:

  1. Have rich custom properties, you can set the appearance of the radar chart
  2. The number of attributes can be set freely
  3. Support two animation modes (fixed time, fixed speed)
  4. Support for specifying a property to perform an animation (to meet the individual needs of the UI draft, see header figure 3)

The first triad demonstrates the main features of the View, and one item for a comparison (99% reduction, ✧(≖ ≖✿)).

Thinking analysis

NOTE:

  1. We abstract the hexagon into N Angle, and use N Angle to express it
  2. Before drawing, we will move the origin of the coordinate system to the center of the radar chart, and the origin is used in the following text

Let’s consider the key technical points:

  • Draw N – Angle radar network
    • Draw a dotted line
      • The dotted line can be givenPaintSet up theDashPathEffectimplementation
      • With the center of the radar chart as the origin, the coordinate system is rotated counterclockwise every time360/NDegrees, drawn from the origin up with a length of zeroRadar radiusThe dotted line
    • Draw lines
      • After drawingA dotted lineAfter that, a solid line is drawn
      • Similarly, the center of the radar chart is taken as the origin, and the coordinate system is moved upward every timeRadar network radius /4And rotate clockwise360/N/2Degree (why this value? You can choose 🤔), the coordinate system at this timeThe x axisIt just corresponds to thetaThe solid linecoincidence
      • So, directly fromMove the rotated origin of the new coordinate systemAlong theThe x axisDraw a solid line
      • Solid line length throughRadar radiusAnd the trigonometric functions of the corresponding angles and so on
  • Draws N Angle property text
    • With the center of the radar chart as the origin and the Angle 12 points as the first Angle, its vertex coordinates can be known(0 - radius)
    • Rotate clockwise through a point on the circle about the center (the origin of the coordinates)Alpha radianTo get the coordinates of the other pointCoordinates formula, you can calculate the coordinates of the vertices at each Angle (this formula will be derived later!).
    • Combine the spacing between text and vertices,Paint setTextAlign (), and fine tuning when drawing textYWe’ll be done
  • Draw the central connection area
    • Similarly, the value of the 12-point direction attribute (Angle) is the first point, and the coordinate of the point is known as(0, -radius * progress ️)
    • Through the sameCoordinates formulaThe coordinate of the progress value of each attribute can be calculated
    • usePathIt’s easy to connect the points to build a path, then draw and stroke
  • Draw the center number & name
    • It is very basic to draw text, but need to pay attention to fine tuningYIn order to improve the degree of reduction with UI draft (details! The details! The details!
  • Add animation effects
    • The essence of animation is to adjust the coordinates of each property value and then redraw the View
    • The actual code we use property animationValueAnimatorIt can be done. It’s easy

Sort out the framework of thinking:

  1. Define and initialize properties
    1. Custom properties section
    2. Computed properties section
    3. Draws the property part of the dependency
    4. Define an API for setting properties
  2. Draw N – Angle radar network
  3. Draws N Angle property text
  4. Draw the central connection area
  5. Draw the center number & name
  6. Add animation effects

Technical points, ideas sorted out, according to the truth is about to start the code, but we first on the math problem warm up.

Warm-up math

Listen to the question: According to the 9 years of compulsory education, deduce the coordinate formula of another point by rotating a point on a circle clockwise about the center of the circle (the origin of the coordinate)

Let the radius of the circle be zerorI have A point on the circle A(.), rotating clockwise around the center of the circleAlpha radianThen the coordinate B(.), the formula is as follows:

=

=+

The derivation process is as follows:

Mathematically specifiedCounterclockwise rotation is positive

When a ray starts from the positive (to the right) direction of the X-axis and rotates counterclockwise to a new position (in the first, second, third, and fourth quadrants respectively), it gets an Angle. And just to make it easier to say that this is a positive Angle, the counterclockwise direction is also going to be positive.

So in order for us to have a positive clockwise direction in our code, we’re going toAs aPlug in the formula above

Encapsulation formula

Now we will package the above formula into tool code, which can be described as a “sharp tool”, we will often use in the future custom View!

First of all, we need to convert 360 degrees into radian, so that we can use the Angle system directly when drawing will be much more convenient.

/** ** ** ** /
private fun Float.degree2radian(a): Float {
    return (this / 180f * PI).toFloat()
}

/** * calculates the sine of an Angle */
fun Float.degreeSin(a): Float {
    return sin(this.degree2radian())
}

/** * computes the cosine of an Angle */
fun Float.degreeCos(a): Float {
    return cos(this.degree2radian())
}
Copy the code

Then, write code according to the formula to get our “weapon”. Here we need to pass in the PointF instance externally instead of creating it each time to improve performance

/** * calculates the coordinates of a point, rotated by an Angle about the origin */
fun PointF.degreePointF(outPointF: PointF, degree: Float) {
    outPointF.x = this.x * degree.degreeCos() - this.y * degree.degreeSin()
    outPointF.y = this.y * degree.degreeCos() + this.x * degree.degreeSin()
}
Copy the code

Drawing process

1. Define and initialize attributes

1. Custom properties section

This step is easy: define our attributes in attrs.xml, declare variables in Layout, and initialize them. Here we just post the code that declares variables.

/ / * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
//* Custom attributes section
/ / * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *

/** * Radar map radius */
private var mWebRadius: Float = 0f

/** * Maximum progress for the radius of the radar map */
private var mWebMaxProgress: Int = 0

/** * The color of the radar cable */
@ColorInt
private var mWebLineColor: Int = 0

/** * Width of radar line */
private var mWebLineWidth: Float = 0f

/ * * * radar map each fixed-point text color * /
@ColorInt
private var mTextArrayedColor: Int = 0

/** * radar map text array font path */
private var mTextArrayedFontPath: String? = null

/ * * * color * / radar map center connection area
@ColorInt
private var mAreaColor: Int = 0

/ * * * connection area border color * / radar map center
@ColorInt
private var mAreaBorderColor: Int = 0

/** * Center text name of radar map */
private var mTextCenteredName: String = default_textCenteredName

/** ** Center text color */
@ColorInt
private var mTextCenteredColor: Int = 0

/** * Center text path */
private var mTextCenteredFontPath: String? = null

/** * an array of characters, and the length of the array determines how many edges the radar map is */
private var mTextArray: Array<String> by Delegates.notNull()

/** * progress array, corresponding to TextArray */
private var mProgressArray: Array<Int> by Delegates.notNull()

/** * The array of progress before executing the animation, corresponding to TextArray */
private var mOldProgressArray: Array<Int> by Delegates.notNull()

/** * Animation time, 0 means no animation *NOTE:If it is in a certain speed mode, it represents the time from the center of the radar to the top of the animation */
private var mAnimateTime: Long = 0L

/** * animation mode, default time fixed mode */
private var mAnimateMode: Int = default_animateMode
Copy the code

2. Calculate the properties section

A calculated property is a property that we calculate based on a custom property. For example 🌰, the size of the text is described using the scale of the radius of the radar disk. The same goes for the other properties. The code is as follows:

/ / * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
//* Calculate the properties section
/ / * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *

/** * The width of the vertical text distance radar main graph */
private var mVerticalSpaceWidth: Float by Delegates.notNull()

/** * Horizontal text distance from the width of the radar main graph */
private var mHorizontalSpaceWidth: Float by Delegates.notNull()

/** * The font size in the literal array */
private var mTextArrayedSize: Float by Delegates.notNull()

/** * Text array Set the font size after the text width, take the most words */
private var mTextArrayedWidth: Float by Delegates.notNull()

/** * Text array sets the height of the text after the font size */
private var mTextArrayedHeight: Float by Delegates.notNull()

/** * The width of the View */
private var mWidth: Float by Delegates.notNull()

/** * The height of the View */
private var mHeight: Float by Delegates.notNull()
Copy the code
/** * Initializes the calculation properties, such as the basic width and height, font size, spacing, etc. * NOTE: based on the UI scale, calculate according to [mWebRadius] */
private fun initCalculateAttributes(a) {
    // Calculate the corresponding attributes according to the scale
    (mWebRadius / 100).let {
        mVerticalSpaceWidth = it * 8
        mHorizontalSpaceWidth = it * 10

        mTextArrayedSize = it * 12
    }

    // After setting the font size, calculate the width and height of the textmPaint.textSize = mTextArrayedSize mTextArray.maxBy { it.length }? .apply { mTextArrayedWidth = mPaint.measureText(this)
        mTextArrayedHeight = mPaint.fontSpacing
    }
    mPaint.utilReset()

    // Dynamically calculate the width and height of the view
    mWidth = (mTextArrayedWidth + mHorizontalSpaceWidth + mWebRadius) * 2.1f
    mHeight = (mTextArrayedHeight + mVerticalSpaceWidth + mWebRadius) * 2.1f
}
Copy the code

3. Draw the properties section of the dependency

The draw-dependent properties are the global properties that we need to use when we actually draw, and we initialize them ahead of time so that we can reuse them instead of opening up memory every time a new object is used in the draw() method.

If we create a new Paint object in the draw() method, AS will alert us.

Avoid objectallocations during draw/layout operations (preallocate and reuse instead) less... (⌘F1) To avoid creating objects during drawing and layout, Inspection info:You should avoid allocating objects during a drawing or layout operation. These are called frequently, so a smooth UI can be interruptedby garbage collection pauses caused by the objectAllocations. You should avoid creating objects during drawing and layout. They are frequently executed, so smooth UI is interrupted by garbage collection pauses caused by object allocation. The waythis is generally handled is to allocate the needed objects up front and to reuse them forEach drawing Operation. The usual approach is to pre-initialize the required objects and reuse them for each drawing operation. Some methods allocate memory on your behalf (suchas Bitmap.create), and these should be handled inMethods that allocate memory for you (such as bitmap.create) should be treated the same way. Issue id: DrawAllocationCopy the code

So we declare and initialize the brush, Path, array of coordinates, etc., as follows:

/ / * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
//* Draw the properties section used
/ / * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *

/** * global brush */
private val mPaint = createPaint()
private val mHelperPaint = createPaint()

/** * Global path */
private val mPath = Path()

/** * Radar net dotted line effect */
private var mDashPathEffect: DashPathEffect by Delegates.notNull()

/** * Coordinates of the vertices in the radar main graph */
private var mPointArray: Array<PointF> by Delegates.notNull()

/** * Text array Coordinate array of each text */
private var mTextArrayedPointArray: Array<PointF> by Delegates.notNull()

/** * coordinate array for each progress */
private var mProgressPointArray: Array<PointF> by Delegates.notNull()

/** * temporary variable */ used for conversion
private var mTempPointF: PointF = PointF()

/** * radar map text array font */
private var mTextArrayedTypeface: Typeface? = null

/** * Center text font */
private var mTextCenteredTypeface: Typeface? = null

/** * Animation processor array */
private varmAnimatorArray: Array<ValueAnimator? >by Delegates.notNull()

/** * Time array for each radar attribute animation */
private var mAnimatorTimeArray: Array<Long> by Delegates.notNull()
Copy the code
/** * Initializes the attributes associated with drawing */
private fun initDrawAttributes(a) {
    context.dpf2pxf(2f).run {
        mDashPathEffect = DashPathEffect(floatArrayOf(this.this), this)
    }

    mPointArray = Array(mTextArray.size) { PointF(0f, 0f) }
    mTextArrayedPointArray = Array(mTextArray.size) { PointF(0f, 0f) }
    mProgressPointArray = Array(mTextArray.size) { PointF(0f, 0f) }
    if(mTextArrayedFontPath ! =null) {
        mTextArrayedTypeface = Typeface.createFromAsset(context.assets, mTextArrayedFontPath)
    }
    if(mTextCenteredFontPath ! =null) {
        mTextCenteredTypeface = Typeface.createFromAsset(context.assets, mTextCenteredFontPath)
    }
}
Copy the code

4. Define an API for setting properties

Here we expose the three apis of setting the text array, the property progress array, and the progress array before executing the animation

/ / * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
//* Set the data properties section
/ / * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *

fun setTextArray(textList: List<String>) {
    this.mTextArray = textList.toTypedArray()
    this.mProgressArray = Array(mTextArray.size) { 0 }
    this.mOldProgressArray = Array(mTextArray.size) { 0 }
    initView()
}

fun setProgressList(progressList: List<Int>) {
    this.mProgressArray = progressList.toTypedArray()
    initView()
}

/** * Sets the progress before executing the animation */
fun setOldProgressList(oldProgressList: List<Int>) {
    this.mOldProgressArray = oldProgressList.toTypedArray()
    initView()
}
Copy the code

2. Draw the n-angle radar network

We move the origin of the coordinate system to the center of the View before drawing, which makes it easier to draw later. As follows:

override fun onDraw(canvas: Canvas?). {
    super.onDraw(canvas)
    if (canvas == null) return
    canvas.helpGreenCurtain(debug)

    canvas.save()
    canvas.translate(mWidth / 2, mHeight / 2)

    // Perform data verification here
    if (checkIllegalData(canvas)) {
        // Draw a mesh graph
        drawWeb(canvas)

        // Draw a literal array
        drawTextArray(canvas)

        // Draw the connection area
        drawConnectionArea(canvas)

        // Draw the center text
        drawCenterText(canvas)
    }

    canvas.restore()
}
Copy the code

Then it is time to draw the n-angle net, and the code is the embodiment of our previous thinking.

/** * draw mesh graph */
private fun drawWeb(canvas: Canvas) {
    canvas.save()

    val rDeg = 360f / mTextArray.size

    mTextArray.forEachIndexed { index, _ ->
        // Draw a dotted line, rotating the coordinate system counterclockwise (rDeg * index) degrees each time
        canvas.save()
        canvas.rotate(-rDeg * index)

        mPaint.pathEffect = mDashPathEffect
        mPaint.color = mWebLineColor
        mPaint.strokeWidth = mWebLineWidth
        canvas.drawLine(0f, 0f, 0f, -mWebRadius, mPaint)
        mPaint.utilReset()

        // Use the trigonometry function to find the longest edge of the net
        val lineW = mWebRadius * (rDeg / 2).degreeSin() * 2
        for (i in 1.4.) {
            // Draw the edges of the net, move the coordinate system up each time (mWebRadius / 4f)* I,
            // Rotate clockwise (rDeg / 2) degrees, then draw a solid line of length (lineW / 4f * I)
            canvas.save()
            canvas.translate(0f, -mWebRadius / 4f * i)
            canvas.rotate(rDeg / 2)

            mPaint.color = mWebLineColor
            mPaint.strokeWidth = mWebLineWidth
            canvas.drawLine(0f, 0f, lineW / 4f * i, 0f, mPaint)
            mPaint.utilReset()

            canvas.restore()
        }

        canvas.restore()
    }

    canvas.restore()
}
Copy the code

3. Draw N Angle attribute text

This step in addition to the code to achieve our previous ideas, but also on the overall processing and fine-tuning of the text position, such a fierce operation after our degree of reduction to the next level.

/** * draws a literal array */
private fun drawTextArray(canvas: Canvas) {
    canvas.save()

    val rDeg = 360f / mTextArray.size

    // Calculate the coordinates of each vertex of the radar chart
    mPointArray.forEachIndexed { index, pointF ->
        if (index == 0) {
            pointF.x = 0f
            pointF.y = -mWebRadius
        } else {
            mPointArray[index - 1].degreePointF(pointF, rDeg)
        }

        // Draw auxiliary dots
        if (debug) {
            mHelperPaint.color = Color.RED
            canvas.drawCircle(pointF.x, pointF.y, 5f, mHelperPaint)
            mHelperPaint.utilReset()
        }
    }

    // Based on the vertex coordinates, calculate the text coordinates and draw the text
    mTextArrayedPointArray.mapIndexed { index, pointF ->
        pointF.x = mPointArray[index].x
        pointF.y = mPointArray[index].y
        return@mapIndexed pointF
    }.forEachIndexed { index, pointF ->
        mPaint.color = mTextArrayedColor
        mPaint.textSize = mTextArrayedSize
        if(mTextArrayedTypeface ! =null) {
            mPaint.typeface = mTextArrayedTypeface
        }

        when {
            index == 0- > {// Adjust text y coordinates
                pointF.y += mPaint.getBottomedY()

                pointF.y = -(pointF.y.absoluteValue + mVerticalSpaceWidth)
                mPaint.textAlign = Paint.Align.CENTER
            }
            mTextArray.size / 2f == index.toFloat() -> {
                // Adjust text y coordinates
                pointF.y += mPaint.getToppedY()

                pointF.y = (pointF.y.absoluteValue + mVerticalSpaceWidth)
                mPaint.textAlign = Paint.Align.CENTER
            }
            index < mTextArray.size / 2f -> {
                // Adjust text y coordinates
                if (pointF.y < 0) {
                    pointF.y += mPaint.getBottomedY()
                } else {
                    pointF.y += mPaint.getToppedY()
                }

                pointF.x = (pointF.x.absoluteValue + mHorizontalSpaceWidth)
                mPaint.textAlign = Paint.Align.LEFT
            }
            index > mTextArray.size / 2f -> {
                // Adjust text y coordinates
                if (pointF.y < 0) {
                    pointF.y += mPaint.getBottomedY()
                } else {
                    pointF.y += mPaint.getToppedY()
                }

                pointF.x = -(pointF.x.absoluteValue + mHorizontalSpaceWidth)
                mPaint.textAlign = Paint.Align.RIGHT
            }
        }

        canvas.drawText(mTextArray[index], pointF.x, pointF.y, mPaint)
        mPaint.utilReset()
    }

    canvas.restore()
}
Copy the code

4. Draw the central connection area

This step is also an easy operation. According to the progress of each attribute and the coordinate formula, obtain the coordinates of each point, and then build the Path and draw it.

/** * Draw the radar connection area */
private fun drawConnectionArea(canvas: Canvas) {
    canvas.save()

    val rDeg = 360f / mTextArray.size

    // Calculate the progress coordinates according to the first coordinates of the radar chart
    val bPoint = mPointArray.first()
    mProgressPointArray.forEachIndexed { index, pointF ->
        val progress = mProgressArray[index] / mWebMaxProgress.toFloat()
        pointF.x = bPoint.x * progress
        pointF.y = bPoint.y * progress
        pointF.degreePointF(mTempPointF, rDeg * index)

        pointF.x = mTempPointF.x
        pointF.y = mTempPointF.y

        // Draw auxiliary dots
        if (debug) {
            mHelperPaint.color = Color.BLACK
            canvas.drawCircle(pointF.x, pointF.y, 5f, mHelperPaint)
            mHelperPaint.utilReset()
        }

        // Use paths to connect points
        if (index == 0) {
            mPath.moveTo(pointF.x, pointF.y)
        } else {
            mPath.lineTo(pointF.x, pointF.y)
        }
        if (index == mProgressPointArray.lastIndex) {
            mPath.close()
        }
    }
    // Draw the region path
    mPaint.color = mAreaColor
    canvas.drawPath(mPath, mPaint)
    mPaint.utilReset()

    // Draws the border of the region pathmPaint.color = mAreaBorderColor mPaint.style = Paint.Style.STROKE mPaint.strokeWidth = mWebLineWidth mPaint.strokeJoin =  Paint.Join.ROUND canvas.drawPath(mPath, mPaint) mPath.reset() mPaint.utilReset() canvas.restore() }Copy the code

5. Draw the center number & name

This step is also a normal operation, the only thing to pay attention to is to fine-tune the text position to improve the restore degree

/** * Draw the center text */
private fun drawCenterText(canvas: Canvas) {
    canvas.save()

    // Draw the numbers
    mPaint.color = mTextCenteredColor
    mPaint.textSize = mTextArrayedSize / 12 * 20
    mPaint.textAlign = Paint.Align.CENTER
    if(mTextCenteredTypeface ! =null) {
        mPaint.typeface = mTextCenteredTypeface
    }
    // Move the coordinate system slightly downward
    canvas.translate(0f, mPaint.fontMetrics.bottom)
    var sum = mProgressArray.sum().toString()
    // Add auxiliary text
    if (debug) {
        sum += Ajk "hello"
    }
    canvas.drawText(sum, 0f, mPaint.getBottomedY(), mPaint)
    mPaint.utilReset()

    // Draw the name
    mPaint.color = mTextCenteredColor
    mPaint.textSize = mTextArrayedSize / 12 * 10
    mPaint.textAlign = Paint.Align.CENTER
    if(mTextArrayedTypeface ! =null) {
        mPaint.typeface = mTextArrayedTypeface
    }
    canvas.drawText(mTextCenteredName, 0f, mPaint.getToppedY(), mPaint)
    mPaint.utilReset()

    // Draw an auxiliary line
    if (debug) {
        mHelperPaint.color = Color.RED
        mHelperPaint.strokeWidth = context.dpf2pxf(1f)
        canvas.drawLine(-mWidth, 0f, mWidth, 0f, mHelperPaint)
        mHelperPaint.utilReset()
    }

    canvas.restore()
}
Copy the code

6. Add animations

The essence of animation is to adjust the coordinates of each property value and then redraw the View

In our code, the coordinates of each property are based on the progress of the property, so constantly adjusting the progress of each property can generate animation.

The animation handler is initialized ahead of time during initialization operations

/** * Initializes the animation processor */
private fun initAnimator(a) {
    mAnimatorArray = Array(mTextArray.size) { null }
    mAnimatorTimeArray = Array(mTextArray.size) { 0L }
    mAnimatorArray.forEachIndexed { index, _ ->
        val sv = mOldProgressArray[index].toFloat()
        val ev = mProgressArray[index].toFloat()
        mAnimatorArray[index] = if (sv == ev) null else ValueAnimator.ofFloat(sv, ev)

        if (mAnimateMode == ANIMATE_MODE_TIME) {
            mAnimatorTimeArray[index] = mAnimateTime
        } else {
            // Calculate constant speed based on maximum progress and animation time
            val v = mWebMaxProgress.toFloat() / mAnimateTime
            mAnimatorTimeArray[index] = if (sv == ev) 0L else ((ev - sv) / v).toLong()
        }
    }
}
Copy the code
/** * All attributes are animated together */
fun doInvalidate(a) {
    mAnimatorArray.forEachIndexed { index, _ ->
        doInvalidate(index)
    }
}

/** * specifies a property to start animation */
fun doInvalidate(index: Int, block: ((Int) -> Unit)? = null) {
    if (index >= 0 && index < mAnimatorArray.size) {
        val valueAnimator = mAnimatorArray[index]
        val at = mAnimatorTimeArray[index]
        if(valueAnimator ! =null && at > 0) {
            valueAnimator.duration = at
            valueAnimator.removeAllUpdateListeners()
            valueAnimator.addUpdateListener {
                val av = (it.animatedValue as Float)
                mProgressArray[index] = av.toInt()

                invalidate()
            }
            // Set the animation end listener
            if(block ! =null) {
                valueAnimator.removeAllListeners()
                valueAnimator.addListener(object : Animator.AnimatorListener {
                    override fun onAnimationRepeat(animation: Animator?). {}

                    override fun onAnimationEnd(animation: Animator?). {
                        block.invoke(index)
                    }

                    override fun onAnimationCancel(animation: Animator?). {}

                    override fun onAnimationStart(animation: Animator?). {}
                })
            }
            valueAnimator.start()
        } else{ block? .invoke(index) } } }Copy the code

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.

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