Customize the View and draw the Android radar map with Kotlin

Posted by not_skeletor on Fri, 14 Jan 2022 21:46:18 +0100

stay Custom View and Radar chart Based on the two blogs, I have made some modifications. Here I summarize my own learning experience.

There are several ways to customize the View

typedefinition
Custom composite controlMultiple controls are combined into a new control to facilitate multiple reuse
Inherit system View controlIt inherits from TextView and other system controls and extends the basic functions of system controls
Inherit ViewDo not reuse system control logic, inherit View for function definition
Inherit system ViewGroupIt inherits from system controls such as LinearLayout and extends the basic functions of system controls
Inherit ViewGroupDo not reuse system control logic, inherit ViewGroup for function definition

This paper implements radar map by inheriting from View.

Layout file

Add the radar chart control in the layout file to display it. The control program is in the LeiDaMap class, and the layout is directly added to the XML file in the layout folder:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <com.example.demo.radarMap.LeiDaMap
        android:id="@+id/leiDaMap"
        android:layout_width="300dp"
        android:layout_height="280dp"
        android:layout_gravity="center_horizontal" />
</LinearLayout>

Control file

The complete code is as follows. The program flow is described in detail below.

package com.example.demo.radarMap

import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Path
import android.util.AttributeSet
import android.view.View
import kotlin.math.cos
import kotlin.math.min
import kotlin.math.sin

/**
 * @description: Radar chart
 */
class LeiDaMap(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) :
    View(context, attrs, defStyleAttr) {
    /**
     * Number of polygon points
     */
    private val count = 6

    /**
     * Number of radar layers
     */
    private val num = 4

    /**
     * Polygons are equally divided into angles, expressed in radians
     */
    private val angle = (Math.PI * 2 / count).toFloat()

    /**
     * Maximum radius of mesh
     */
    private var radius = 0f

    /**
     * Center x
     */
    private var centerX = 0

    /**
     * Center y
     */
    private var centerY = 0

    /**
     * Data Max
     */
    private var maxValue = 100f

    /**
     * Score of each dimension
     */
    private var data = doubleArrayOf(50.0, 60.0, 70.0, 80.0, 90.0, 100.0)
    private var titles = arrayOf("one by one", "two two", "three three", "four four", "five five", "six six")

    /**
     * Radar area brush
     */
    private var mMainPaint: Paint? = null

    /**
     * Text brush
     */
    private var mTextPaint: Paint? = null

    /**
     * Data area brush
     */
    private var mValuePaint: Paint? = null

    constructor(context: Context?) : this(context, null)
    constructor(context: Context?, attrs: AttributeSet?) : this(context, attrs, 0)

    private fun initPaint() {
        mMainPaint = Paint()
        mMainPaint?.isAntiAlias = true
        mMainPaint?.strokeWidth = 3F
        mMainPaint?.style = Paint.Style.STROKE
        mMainPaint?.color = Color.BLACK

        mTextPaint = Paint()
        mTextPaint?.isAntiAlias = true
        mTextPaint?.color = Color.BLUE
        mTextPaint?.textSize = 60F

        mValuePaint = Paint()
        mValuePaint?.isAntiAlias = true
        mValuePaint?.color = Color.RED
        mValuePaint?.style = Paint.Style.FILL_AND_STROKE
    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        //Maximum radius of mesh
        radius = min(h, w).toFloat() / 2 * 0.7f
        centerX = w / 2
        centerY = h / 2
        postInvalidate()
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        //Draw regular polygon
        drawPolygon(canvas)
        //Draw a line from center to end
        drawLines(canvas)
        //Draw text
        drawText(canvas)
        //Draw area
        drawRegion(canvas)
    }

    /**
     * Draw regular polygon
     */
    private fun drawPolygon(canvas: Canvas) {
        val path = Path()
        //Spacing between spider silk
        val r = radius / num
        for (i in 1..num) {
            //Current radius
            val curR = r * i
            path.reset()
            //Number of polygon points
            for (j in 0 until count) {
                if (j == 0) {
                    path.moveTo(centerX.toFloat(), centerY + curR)
                } else {
                    //According to the radius, calculate the coordinates of each point on the spider silk
                    val x = (centerX + curR * sin((angle * j).toDouble())).toFloat()
                    val y = (centerY + curR * cos((angle * j).toDouble())).toFloat()
                    path.lineTo(x, y)
                }
            }
            //Closed path
            path.close()
            mMainPaint?.let { canvas.drawPath(path, it) }
        }
    }

    /**
     * Draw a line from center to end
     */
    private fun drawLines(canvas: Canvas) {
        val path = Path()
        for (i in 0 until count) {
            path.reset()
            path.moveTo(centerX.toFloat(), centerY.toFloat())
            //Calculate the coordinates of each point on the outermost spider silk
            val x = (centerX + radius * sin((angle * i).toDouble())).toFloat()
            val y = (centerY + radius * cos((angle * i).toDouble())).toFloat()
            path.lineTo(x, y)
            mMainPaint?.let { canvas.drawPath(path, it) }
        }
    }

    /**
     * Draw text
     * First calculate the length of the text, and then offset the starting drawing coordinate to the left by this length.
     */
    private fun drawText(canvas: Canvas) {
        val fontMetrics: Paint.FontMetrics = mTextPaint!!.fontMetrics
        val fontHeight: Float = fontMetrics.descent - fontMetrics.ascent
        for (i in 0 until count) {
            //Calculate the coordinates of each point on the outermost spider silk
            val x =
                (centerX + (radius + fontHeight / 2) * sin((angle * i).toDouble())).toFloat()
            val y =
                (centerY + (radius + fontHeight / 2) * cos((angle * i).toDouble())).toFloat()
            // Text length to move text based on text length
            val dis: Float = mTextPaint!!.measureText(titles[i])
            //First quadrant, second quadrant
            if (i == 1 || i == 2) {
                canvas.drawText(titles[i], x, y, mTextPaint!!)
            }
            // Three quadrant, four quadrant
            else if (i == 4 || i == 5) {
                canvas.drawText(titles[i], x - dis, y, mTextPaint!!)
            }
//            Point on axis
            else if (i == 0) {
                canvas.drawText(titles[i], x - dis / 2, y + dis / 3, mTextPaint!!)
            } 
            else if (i == 3) {
                canvas.drawText(titles[i], x - dis / 2, y, mTextPaint!!)
            }
        }
    }

    /**
     * Draw area
     */
    private fun drawRegion(canvas: Canvas) {
        val path = Path()
        mValuePaint?.alpha = 255
        for (i in 0 until count) {
            val percent = data[i] / maxValue
            //Calculate the coordinates of each point on the outermost spider silk
            val x = (centerX + radius * sin((angle * i).toDouble()) * percent).toFloat()
            val y = (centerY + radius * cos((angle * i).toDouble()) * percent).toFloat()
            if (i == 0) {
                path.moveTo(centerX.toFloat(), y)
            } else {
                path.lineTo(x, y)
            }
            //Draw small dots
            mValuePaint?.let { canvas.drawCircle(x, y, 20F, it) }
        }
        mValuePaint?.alpha = 127
        //Draw filled area
        mValuePaint?.let { canvas.drawPath(path, it) }
    }

    /**
     * @param titles
     */
    fun setTitles(titles: Array<String>) {
        this.titles = titles
    }

    /**
     * Score of each dimension
     * @param data data
     */
    fun setData(data: DoubleArray) {
        this.data = data
    }

    /**
     * Data Max
     * @param maxValue maxValue
     */
    fun setMaxValue(maxValue: Float) {
        this.maxValue = maxValue
    }

    /**
     * Set spider web color
     * @param color
     */
    fun setMainPaintColor(color: Int) {
        mMainPaint?.color = color
    }

    /**
     * Set title color
     * @param color
     */
    fun setTextPaintColor(color: Int) {
        mTextPaint?.color = color
    }

    /**
     * @param color
     */
    fun setValuePaintColor(color: Int) {
        mValuePaint?.color = color
    }

    init {
        initPaint()
    }
}

After operation:

The previous steps are to initialize variables. The drawing process is as follows:
1. Find the center of the layout
2. Draw polygon
3. Draw a line from the center to the corner
4. Draw text (may involve text offset)
5. Draw area

1. Find the center of the layout

Rewrite the onSizeChanged function to find the center point and the maximum radius of the radar map. To make room for the text, you need to multiply it by a factor.

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        //Maximum radius of mesh
        radius = min(h, w).toFloat() / 2 * 0.7f
        centerX = w / 2
        centerY = h / 2
        postInvalidate()
    }

2. Draw polygon

    private fun drawPolygon(canvas: Canvas) {
        val path = Path()
        //Spacing between spider silk
        val r = radius / num
        for (i in 1..num) {
            //Current radius
            val curR = r * i
            path.reset()
            //Number of polygon points
            for (j in 0 until count) {
                if (j == 0) {
                    path.moveTo(centerX.toFloat(), centerY + curR)
                } else {
                    //According to the radius, calculate the coordinates of each point on the spider silk
                    val x = (centerX + curR * sin((angle * j).toDouble())).toFloat()
                    val y = (centerY + curR * cos((angle * j).toDouble())).toFloat()
                    path.lineTo(x, y)
                }
            }
            //Closed path
            path.close()
            mMainPaint?.let { canvas.drawPath(path, it) }
        }
    }

3. Draw a line from the center point to the corner

    private fun drawLines(canvas: Canvas) {
        val path = Path()
        for (i in 0 until count) {
            path.reset()
            path.moveTo(centerX.toFloat(), centerY.toFloat())
            //Calculate the coordinates of each point on the outermost spider silk
            val x = (centerX + radius * sin((angle * i).toDouble())).toFloat()
            val y = (centerY + radius * cos((angle * i).toDouble())).toFloat()
            path.lineTo(x, y)
            mMainPaint?.let { canvas.drawPath(path, it) }
        }
    }

4. Draw text

You can judge according to the value of i or quadrant in the if selection statement as needed.

    private fun drawText(canvas: Canvas) {
        val fontMetrics: Paint.FontMetrics = mTextPaint!!.fontMetrics
        val fontHeight: Float = fontMetrics.descent - fontMetrics.ascent
        for (i in 0 until count) {
            //Calculate the coordinates of each point on the outermost spider silk
            val x =
                (centerX + (radius + fontHeight / 2) * sin((angle * i).toDouble())).toFloat()
            val y =
                (centerY + (radius + fontHeight / 2) * cos((angle * i).toDouble())).toFloat()
            // Text length to move text based on text length
            val dis: Float = mTextPaint!!.measureText(titles[i])
            //First quadrant, second quadrant
            if (i == 1 || i == 2) {
                canvas.drawText(titles[i], x, y, mTextPaint!!)
            }
            // Three quadrant, four quadrant
            else if (i == 4 || i == 5) {
                canvas.drawText(titles[i], x - dis, y, mTextPaint!!)
            }
//            Coordinate axis
            else if (i == 0) {
                canvas.drawText(titles[i], x - dis / 2, y + dis / 3, mTextPaint!!)
            } 
            else if (i == 3) {
                canvas.drawText(titles[i], x - dis / 2, y, mTextPaint!!)
            }
        }
    }

5. Draw area

    private fun drawRegion(canvas: Canvas) {
        val path = Path()
        mValuePaint?.alpha = 255
        for (i in 0 until count) {
            val percent = data[i] / maxValue
            //Calculate the coordinates of each point on the outermost spider silk
            val x = (centerX + radius * sin((angle * i).toDouble()) * percent).toFloat()
            val y = (centerY + radius * cos((angle * i).toDouble()) * percent).toFloat()
            if (i == 0) {
                path.moveTo(centerX.toFloat(), y)
            } else {
                path.lineTo(x, y)
            }
            //Draw small dots
            mValuePaint?.let { canvas.drawCircle(x, y, 20F, it) }
        }
        mValuePaint?.alpha = 127
        //Draw filled area
        mValuePaint?.let { canvas.drawPath(path, it) }
    }

Topics: Android kotlin view