Android自定义View—量角器

一、简介

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

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

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

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

咱们先看效果图:

再来一张放大的图:

二、步骤

2.1 画半圆

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

ini 复制代码
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
)

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

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

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

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

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

2.2 画刻度线

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

x = x0 + r * cos(θ)

y = y0 + r * sin(θ)

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

scss 复制代码
//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++
}

画出的图如下:

2.3 画度数

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

scss 复制代码
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()

附图:

三、完整代码

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

View

ini 复制代码
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 复制代码
<?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

ini 复制代码
<?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>

代码成品图:

四、总结

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

相关推荐
Kapaseker21 小时前
你不看会后悔的2025年终总结
android·kotlin
alexhilton1 天前
务实的模块化:连接模块(wiring modules)的妙用
android·kotlin·android jetpack
ji_shuke1 天前
opencv-mobile 和 ncnn-android 环境配置
android·前端·javascript·人工智能·opencv
sunnyday04261 天前
Spring Boot 项目中使用 Dynamic Datasource 实现多数据源管理
android·spring boot·后端
幽络源小助理1 天前
下载安装AndroidStudio配置Gradle运行第一个kotlin程序
android·开发语言·kotlin
inBuilder低代码平台1 天前
浅谈安卓Webview从初级到高级应用
android·java·webview
豌豆学姐1 天前
Sora2 短剧视频创作中如何保持人物一致性?角色创建接口教程
android·java·aigc·php·音视频·uniapp
白熊小北极1 天前
Android Jetpack Compose折叠屏感知与适配
android
HelloBan1 天前
setHintTextColor不生效
android
洞窝技术1 天前
从0到30+:智能家居配网协议融合的实战与思考
android