Android自定义View—量角器

696 阅读2分钟

一、简介

其他两种尺子已经做出来了

Android自定义View—直尺 - 掘金 (juejin.cn)

Android自定义View—等腰直角三角尺 - 掘金 (juejin.cn)

最近公司有需求出一套教学工具包含尺子、三角尺、量角器,一般来说图省事都会直接用个View然后设置个背景就完事了,虽然方便但是这样局限就太多了,想着用自定义View去实现一下,刚好能够锻炼下自定义View的技能

咱们先看效果图:

Screenshot_20230911_232246.png

再来一张放大的图:

image.png

二、步骤

2.1 画半圆

根据View的宽高,我们可以得到一个矩形,此时如果我们直接去利用这个矩形画半圆会发现,0刻度线跟0刻度的数字会超出视图之外,所以这里是将矩形根据中心点缩小0.9倍

val rectCenterX = width/2f
val rectCenterY = height/2f

val newWidth = width * 0.9f
val newHeight = height * 0.9f
rectF.set(
    rectCenterX-newWidth/2f,
    rectCenterY-newHeight/2f,
    rectCenterX+newWidth/2f,
    rectCenterY+newHeight/2f
)

Screenshot_20230911_233239.png

然后画半圆需要半径,这里的半径取得是宽度的一半跟矩形的最小值,以确保能把半圆给画出来,其中半圆的中心点是width/2f,是View的宽度的中心点,centerY则是矩形的高度

radius = min(rectF.width()/2f,rectF.height())
minLength = radius/14f
val centerX = width/2f
val centerY = rectF.height()
canvas.drawCircle(centerX, centerY, radius, bgPaint)

画出来的效果如图(为了效果更明显,加了点透明度):

Screenshot_20230911_234445.png

可以看到,半圆是画出来了,但是这个半圆的范围有点多了,所以我们需要裁切掉一点

//加个offset偏移值,留出一半多一点,用于显示0刻度线及0刻度文字
clipOutRect(0f,centerY + offset,width.toFloat(),height.toFloat())

Screenshot_20230911_234842.png

2.2 画刻度线

接着我们要去逆时针画刻度线,其中度数%5等于0时,判断是10还是5,10的话画长线,5的话画中线,其余的画短线,假设中心点为 (x0, y0),半径为 r,圆上的点的计算公式为:

x = x0 + r * cos(θ)

y = y0 + r * sin(θ)

代码中的minLength为一个长度基数

//1°等于PI/180
val unit = PI/180
var angle = 0f
while (angle<=180f){
    val startX = centerX + radius* cos(angle * unit).toFloat()
    val startY = centerY - radius * sin(angle * unit).toFloat()
    var endX: Float
    var endY: Float
    var lineWidth:Float
    if (angle % (intervalsBetweenValues / 2f) == 0f) {
        if (angle%intervalsBetweenValues == 0f) {
            //画长线
            lineWidth = minLength * 2f
        }else{
            //画中线
            lineWidth = minLength * 1.5f
        }
    }else{
        //画短线
        lineWidth = minLength
    }
    endX = centerX + ((radius - lineWidth)* cos(angle*unit).toFloat())
    endY = centerY - ((radius- lineWidth) * sin(angle*unit).toFloat())
    drawLine(startX, startY, endX, endY, drawPaint)
    angle++
}

画出的图如下:

Screenshot_20230911_235845.png

2.3 画度数

接下来就是画度数了,思路是找出字体的中心点,使其跟随角度进行旋转绘制,以保证文字的旋转角度跟刻度线的角度一致

val startTextX =  centerX + ((radius - lineWidth*1.5f)* cos(angle*unit).toFloat())
val startTextY =  centerY - ((radius- lineWidth*1.5f) * sin(angle*unit).toFloat())
val valueString = angle.toInt().toString() + "°"
val textWidth: Float = drawPaint.measureText(valueString)
val textHeight: Float = drawPaint.descent() - drawPaint.ascent()
val textCenterX = startTextX + textWidth/2* cos(angle*unit).toFloat()
val textCenterY = startTextY - textHeight/2* sin(angle*unit).toFloat()
val textX = textCenterX - textWidth / 2
val textY = textCenterY + textHeight / 2
// 绘制旋转的文本
save()
rotate(90f - angle, textCenterX, textCenterY)
drawText(
    valueString,
    textX,
    textY,
    drawPaint
)
restore()

附图:

Screenshot_20230912_000328.png

三、完整代码

上面的步骤我们已经可以看到样子了,改一下背景就可以了,下面放出完整的代码:

View

class ProtractorView@JvmOverloads constructor(
    private val context: Context?,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) :
    View(context, attrs, defStyleAttr) {
    //刻度跟字
    private val drawPaint = Paint()

    //背景
    private val bgPaint = Paint()

    //刻度的宽度
    private var linesWidth = 0f

    //刻度的颜色
    private var linesColor = Color.BLACK

    //值的文本颜色
    private var valuesTextColor = Color.BLACK

    //值的文本大小
    private var valuesTextSize = 0f

    //每两个值之间的间隔数,也指多少个最小单位,比如0cm到1cm有10个最小单位1mm
    private var intervalsBetweenValues = 0

    //最短刻度长度为基准
    private var minLength = 0f

    //半径
    private var radius = 0f

    //矩形,方便定位
    private var rectF = RectF()

    private val offset = 30f

    init {
        val array = context!!.obtainStyledAttributes(attrs, R.styleable.Protractor)
        intervalsBetweenValues = array.getInt(R.styleable.Protractor_intervalsBetweenValues, 10)
        valuesTextSize = array.getDimensionPixelSize(R.styleable.Protractor_valuesTextSize, 4).toFloat()
        valuesTextColor = array.getColor(R.styleable.Protractor_valuesTextColor, Color.BLACK)
        linesWidth = array.getDimensionPixelSize(R.styleable.Protractor_linesWidth, 1).toFloat()
        linesColor = array.getColor(R.styleable.Protractor_linesColor, Color.BLACK)
        array.recycle()
        initView()
    }

    private fun initView() {
        bgPaint.color = Color.WHITE
        bgPaint.style = Paint.Style.FILL
        bgPaint.isAntiAlias = true

        drawPaint.color = Color.BLACK
        drawPaint.isAntiAlias = true
        drawPaint.textSize = valuesTextSize
        drawPaint.strokeWidth = linesWidth

    }

    /**
     * 假设中心点为 (x0, y0),半径为 r
     * 使用以下公式计算选定角度的点的坐标:
     * x = x0 + r * cos(θ)
     * y = y0 + r * sin(θ)
     */
    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        val rectCenterX = width/2f
        val rectCenterY = height/2f

        val newWidth = width * 0.9f
        val newHeight = height * 0.9f
        rectF.set(
            rectCenterX-newWidth/2f,
            rectCenterY-newHeight/2f,
            rectCenterX+newWidth/2f,
            rectCenterY+newHeight/2f
        )
        radius = min(rectF.width()/2f,rectF.height())
        minLength = radius/14f
        val centerX = width/2f
        val centerY = rectF.height()

        drawPaint.color = linesColor
        canvas?.apply {
            //先画半圆
            save()
            clipOutRect(0f,centerY + offset,width.toFloat(),height.toFloat())
            drawCircle(centerX, centerY, radius, bgPaint)
            restore()
            //1°等于PI/180
            val unit = PI/180
            var angle = 0f
            while (angle<=180f){
                var needDrawText = false
                val startX = centerX + radius* cos(angle * unit).toFloat()
                val startY = centerY - radius * sin(angle * unit).toFloat()
                var endX: Float
                var endY: Float
                var lineWidth:Float
                if (angle % (intervalsBetweenValues / 2f) == 0f) {
                    if (angle%intervalsBetweenValues == 0f) {
                        //画长线
                        lineWidth = minLength * 2f
                        needDrawText = true
                    }else{
                        //画中线
                        lineWidth = minLength * 1.5f
                    }
                }else{
                    //画短线
                    lineWidth = minLength
                }
                endX = centerX + ((radius - lineWidth)* cos(angle*unit).toFloat())
                endY = centerY - ((radius- lineWidth) * sin(angle*unit).toFloat())
                drawLine(startX, startY, endX, endY, drawPaint)
                if (needDrawText){
                    drawPaint.color = valuesTextColor
                    val startTextX =  centerX + ((radius - lineWidth*1.5f)* cos(angle*unit).toFloat())
                    val startTextY =  centerY - ((radius- lineWidth*1.5f) * sin(angle*unit).toFloat())
                    val valueString = angle.toInt().toString() + "°"
                    val textWidth: Float = drawPaint.measureText(valueString)
                    val textHeight: Float = drawPaint.descent() - drawPaint.ascent()
                    val textCenterX = startTextX + textWidth/2* cos(angle*unit).toFloat()
                    val textCenterY = startTextY - textHeight/2* sin(angle*unit).toFloat()
                    val textX = textCenterX - textWidth / 2
                    val textY = textCenterY + textHeight / 2
                    // 绘制旋转的文本
                    save()
                    rotate(90f - angle, textCenterX, textCenterY)
                    drawText(
                        valueString,
                        textX,
                        textY,
                        drawPaint
                    )
                    restore()
                    drawPaint.color = linesColor
                }
                angle++
            }
        }
    }

attrs.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="Protractor">
        <attr name="intervalsBetweenValues" format="integer"/>
        <attr name="valuesTextSize" format="dimension"/>
        <attr name="valuesTextColor" format="color"/>
        <attr name="linesWidth" format="dimension"/>
        <attr name="linesColor" format="color"/>
    </declare-styleable>

</resources>

layout

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    xmlns:custom="http://schemas.android.com/apk/res-auto"
    android:layout_height="wrap_content">

    <com.example.studytools.view.ProtractorView
        android:layout_width="400dp"
        android:layout_height="200dp"
        custom:intervalsBetweenValues="10"
        custom:linesColor="@android:color/black"
        custom:linesWidth="0.1dp"
        custom:valuesTextSize="4sp"/>

</RelativeLayout>

代码成品图:

Screenshot_20230911_232246.png

四、总结

数学知识在自定义View中还是很重要的,上面的只是demo,还有很多可以优化的地方,做的不好的地方欢迎大家指出