Android自定义View—等腰直角三角尺

一、简介

Android自定义View---量角器 - 掘金 (juejin.cn)

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

前面两篇文章分别实现了量角器和直尺,现在我们来实现等腰直角三角尺,老规矩,先看效果图:

二、步骤

这个等腰直角三角尺的实现比其他两个要复杂点,因为涉及到数字和刻度线的旋转,运用到比较多的数学知识

首先我们得先复习下等腰直角三角形的基础知识:

直角边:直角边:斜边 = 1:1:√2

斜边^2 = 直角边^2 + 直角边^2

2.1 画三角形背景

外三角

scss 复制代码
val viewWidth = width.toFloat()
val viewHeight = height.toFloat()
//三角形外边长
length = min(viewWidth, viewHeight)
//等腰直角外三角形
outPath.reset()
outPath.moveTo(0f,0f)
outPath.lineTo(length,0f)
outPath.lineTo(0f, length)
outPath.close()
canvas.drawPath(outPath,bgPaint)

内三角 这里的内三角我们取外三角的长度的3分之一,使用canvas.clipOutPath()挖空一个内三角

scss 复制代码
//等腰内三角形
inPath.reset()
val inLength = length/3
//边距
val padding = (length - inLength) / 2f
val x = padding / 2f
val y = x
minLength = y/14f
inPath.moveTo(x, y)
inPath.lineTo(length - padding,y)
inPath.lineTo(x,length - padding)
inPath.close()
//注意这里要先裁剪,再画三角形
canvas.save()
canvas.clipOutPath(inPath)
canvas.drawPath(outPath,bgPaint)
canvas.restore()

2.2 画刻度线

横向刻度 横向刻度的绘制跟之前的直尺差不太多,关键的地方在于toValue要减去1,为的是三角形的尖尖画数字或者长刻度的时候,不会超出三角形的范围

toValue 指的是尺子的长度,这里指的是最大刻度

interval 指的两条刻度之间的宽度

intervalsBetweenValues 指的是数值之间的间隔数,尺子的刻度值比如0和1之间相差了10个1m

思路还是一样,先判断position是不是5的倍数,不是则画短线,是的话,如果是10的倍数,画长线,不是则画中线

arduino 复制代码
//根据view的宽度去决定长度,取Int整数
var toValue = ((length) / (interval * intervalsBetweenValues) - 1).toInt()
var offset =  (length - (toValue * interval * intervalsBetweenValues)) / 2f
var width =  offset
var position = 0

while (true) {
    //画剩下的刻度
    if (position > toValue / valuesInterval * intervalsBetweenValues) break
    if (width > length + drawPaint.measureText("10000")) break
    if (position % (intervalsBetweenValues / 2) == 0) {
        if (position % intervalsBetweenValues == 0) {
            //===============长线============
              canvas?.drawLine(width, 0f, width, minLength * 2f, drawPaint)
        } else {
            //============中线==============
            canvas?.drawLine(width, 0f, width, minLength * 1.5f, drawPaint)

        }
    } else {
        //============短线===========
        canvas?.drawLine(width, 0f, width, minLength, drawPaint)

    }
    width += interval
    position++
}

纵向刻度

arduino 复制代码
var width =  offset
var position = 0

while (true) {
    //画剩下的刻度
    if (position > toValue / valuesInterval * intervalsBetweenValues) break
    if (width > length + drawPaint.measureText("10000")) break
    if (position % (intervalsBetweenValues / 2) == 0) {
        if (position % intervalsBetweenValues == 0) {
            //===============长线============
            canvas?.apply {
                //纵
                drawLine(0f, width, minLength * 2, width, drawPaint)
            }
        } else {
            //============中线==============
            //纵
            canvas?.drawLine(0f, width, minLength*1.5f, width, drawPaint)
        }
    } else {
        //============短线===========
        //纵
        canvas?.drawLine(0f, width, minLength, width, drawPaint)
    }
    width += interval
    position++
}

斜向刻度

斜向刻度我这里说明下,一开始我采用的是手动算出斜边上的点,然后计算点与点连成线,与斜边呈现90°角,写的代码很多,涉及到很多的计算,最终是弄出来了,但是过程很不清晰明朗,后面我又换了种方法(展示我的灵魂画工~)

如图,我们可以先在水平位置上画线,然后将线旋转45°,就可以将线画在斜边上了,注意这里的旋转基点是左下角,并不是线本身

scss 复制代码
val sqrt2 = 1.414f
toValue = sqrt((2*toValue*toValue.toDouble())).toInt()
val hypotenuse = length*sqrt2
offset = (hypotenuse - toValue*interval*intervalsBetweenValues)/2
width = offset
position = 0
var lineLength:Float
while (true){
    if (position > toValue / valuesInterval * intervalsBetweenValues) break
    if (width > length * sqrt2 + drawPaint.measureText("10000")) break
    canvas?.apply {
        if (position % (intervalsBetweenValues / 2) == 0) {
            if (position % intervalsBetweenValues == 0) {
                //===============长线===========
                lineLength = length-minLength*2f
            }else {
                //============中线==============
                lineLength = length-minLength*1.5f
            }
        }else{
            //============短线===========
            lineLength = length-minLength
        }

        save()
        rotate(-45f,0f,length)
        drawLine(width, lineLength, width, length, drawPaint)
        restore()

        width += interval
        position++
    }

2.3 画刻度值

横向

横向的跟之前直尺的差不多

scss 复制代码
val valueString =
    (position / intervalsBetweenValues * valuesInterval).toString()
drawText(
    valueString,
    width - drawPaint.measureText(valueString) / 2,
    minLength * 2.5f + textHeight / 2,
    drawPaint
)

纵向

纵向的文字我们需要围绕文字的中心去顺时针旋转90度

scss 复制代码
// 计算旋转后的坐标
val centerX = minLength * 2.5f+ textHeight / 2
val centerY = width
val textWidth: Float = drawPaint.measureText( (toValue - valueString.toInt()).toString())
val textHeight: Float = drawPaint.descent() - drawPaint.ascent()
val textX = centerX - textWidth / 2
val textY = centerY + textHeight / 2
// 绘制旋转的文本
save()
rotate(90f, centerX, centerY)
drawText(
    (toValue - valueString.toInt()).toString(),
    textX,
    textY,
    drawPaint
)
restore()

斜边

斜边上的文字,我们需要围绕其自身逆时针旋转45度

ini 复制代码
// 计算旋转后的坐标
val centerX = width
val centerY = length - minLength*2.5f - textHeight / 2
val textWidth: Float = drawPaint.measureText(valueString)
val textHeight: Float = drawPaint.descent() - drawPaint.ascent()
val textX = centerX - textWidth / 2
val textY = centerY + textHeight / 2
// 绘制旋转的文本
save()
rotate(-45f, 0f, length)
drawText(
    valueString,
    textX,
    textY,
    drawPaint
)
restore()

最后呈现的效果如图:

三、完整代码

View

scss 复制代码
/**
 * 等腰直角三角尺
 */
class IsoscelesTriangleView @JvmOverloads constructor(
    private val context: Context?,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) :
    View(context, attrs, defStyleAttr) {
    //刻度跟字
    private val drawPaint = Paint()

    //背景
    private val bgPaint = Paint()

    //外三角Path
    private val outPath = Path()

    //内三角Path
    private val inPath = Path()

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

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

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

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

    private var textHeight = 0

    //相邻两个值的跳跃间隔
    private var valuesInterval = 0

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

    //间隔,即两条刻度间的距离
    private var interval = 0f

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

    private var length = 0f

    init {
        val array = context!!.obtainStyledAttributes(attrs, R.styleable.IsoscelesTriangle)
        interval= array.getDimensionPixelSize(R.styleable.IsoscelesTriangle_interval, 1).toFloat()
        intervalsBetweenValues=array.getInt(R.styleable.IsoscelesTriangle_intervalsBetweenValues, 10)
        valuesInterval=array.getInt(R.styleable.IsoscelesTriangle_valuesInterval, 1)
        valuesTextSize=array.getDimensionPixelSize(R.styleable.IsoscelesTriangle_valuesTextSize, 4).toFloat()
        valuesTextColor=array.getColor(R.styleable.IsoscelesTriangle_valuesTextColor, Color.BLACK)
        linesWidth=array.getDimensionPixelSize(R.styleable.IsoscelesTriangle_linesWidth, 1).toFloat()
        linesColor=array.getColor(R.styleable.IsoscelesTriangle_linesColor, Color.BLACK)
        array.recycle()

        initView()
    }

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

        drawPaint.textSize = valuesTextSize
        //文本高度
        val fm = drawPaint.fontMetrics
        textHeight = (fm.bottom - fm.top).toInt()

    }

    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        val viewWidth = width.toFloat()
        val viewHeight = height.toFloat()
        //三角形外边长
        length = min(viewWidth, viewHeight)
        //等腰外三角形
        outPath.reset()
        outPath.moveTo(0f,0f)
        outPath.lineTo(length,0f)
        outPath.lineTo(0f, length)
        outPath.close()

        //等腰内三角形
        inPath.reset()
        val inLength = length/3
        //边距
        val padding = (length - inLength) / 2f
        val x = padding / 2f
        val y = x
        minLength = y/14f
        inPath.moveTo(x, y)
        inPath.lineTo(length - padding,y)
        inPath.lineTo(x,length - padding)
        inPath.close()
        canvas?.apply {
            save()
            clipOutPath(inPath)
            drawPath(outPath,bgPaint)
            restore()
        }
        //============刻度========
        drawPaint.color = linesColor
        drawPaint.strokeWidth = linesWidth
        drawLineAndText(canvas)
    }

    private fun drawLineAndText(canvas: Canvas?) {
        //根据view的宽度去决定长度,取Int整数
        var toValue = ((length) / (interval * intervalsBetweenValues) - 1).toInt()
        var offset =  (length - (toValue * interval * intervalsBetweenValues)) / 2f
        var width =  offset
        var position = 0

        while (true) {
            //画剩下的刻度
            if (position > toValue / valuesInterval * intervalsBetweenValues) break
            if (width > length + drawPaint.measureText("10000")) break
            if (position % (intervalsBetweenValues / 2) == 0) {
                if (position % intervalsBetweenValues == 0) {
                    //===============长线============
                    canvas?.apply {
                        //横
                        drawLine(width, 0f, width, minLength * 2f, drawPaint)
                        //纵
                        drawLine(0f, width, minLength * 2, width, drawPaint)

                        drawPaint.color = valuesTextColor
                        //横
                        val valueString =
                            (position / intervalsBetweenValues * valuesInterval).toString()
                        drawText(
                            valueString,
                            width - drawPaint.measureText(valueString) / 2,
                            minLength * 2.5f + textHeight / 2,
                            drawPaint
                        )
                        //纵
                        // 计算旋转后的坐标
                        val centerX = minLength * 2.5f+ textHeight / 2
                        val centerY = width
                        val textWidth: Float = drawPaint.measureText( (toValue - valueString.toInt()).toString())
                        val textHeight: Float = drawPaint.descent() - drawPaint.ascent()
                        val textX = centerX - textWidth / 2
                        val textY = centerY + textHeight / 2
                        // 绘制旋转的文本
                        save()
                        rotate(90f, centerX, centerY)
                        drawText(
                            (toValue - valueString.toInt()).toString(),
                            textX,
                            textY,
                            drawPaint
                        )
                        restore()
                        drawPaint.color = linesColor
                    }
                } else {
                    //============中线==============
                    //横
                    canvas?.drawLine(width, 0f, width, minLength * 1.5f, drawPaint)
                    //纵
                    canvas?.drawLine(0f, width, minLength*1.5f, width, drawPaint)
                }
            } else {
                //============短线===========
                //横
                canvas?.drawLine(width, 0f, width, minLength, drawPaint)
                //纵
                canvas?.drawLine(0f, width, minLength, width, drawPaint)
            }
            width += interval
            position++
        }

        //============斜边=============

        val sqrt2 = 1.414f
        toValue = sqrt((2*toValue*toValue.toDouble())).toInt()
        val hypotenuse = length*sqrt2
        offset = (hypotenuse - toValue*interval*intervalsBetweenValues)/2
        width = offset
        position = 0
        var lineLength:Float
        while (true){
            if (position > toValue / valuesInterval * intervalsBetweenValues) break
            if (width > length * sqrt2 + drawPaint.measureText("10000")) break
            canvas?.apply {
                if (position % (intervalsBetweenValues / 2) == 0) {
                    if (position % intervalsBetweenValues == 0) {
                        //===============长线===========
                        lineLength = length-minLength*2f
                        //=======画字=====
                        drawPaint.color = valuesTextColor
                        val valueString = (toValue-
                            (position / intervalsBetweenValues * valuesInterval)).toString()

                        // 计算旋转后的坐标
                        val centerX = width
                        val centerY = length - minLength*2.5f - textHeight / 2
                        val textWidth: Float = drawPaint.measureText(valueString)
                        val textHeight: Float = drawPaint.descent() - drawPaint.ascent()
                        val textX = centerX - textWidth / 2
                        val textY = centerY + textHeight / 2
                        // 绘制旋转的文本
                        save()
                        rotate(-45f, 0f, length)
                        drawText(
                            valueString,
                            textX,
                            textY,
                            drawPaint
                        )
                        restore()
                        drawPaint.color = linesColor

                    }else {
                        //============中线==============
                        lineLength = length-minLength*1.5f
                    }
                }else{
                    //============短线===========
                    lineLength = length-minLength
                }

                save()
                rotate(-45f,0f,length)
                drawLine(width, lineLength, width, length, drawPaint)
                restore()

                width += interval
                position++
            }
        }
    }
}

attrs.xml

xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="IsoscelesTriangle">
        <attr name="interval" format="dimension"/>
        <attr name="intervalsBetweenValues" format="integer"/>
        <attr name="valuesInterval" 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.IsoscelesTriangleView
        android:layout_width="400dp"
        android:layout_height="400dp"
        custom:interval="5dp"
        custom:intervalsBetweenValues="10"
        custom:linesColor="@android:color/black"
        custom:linesWidth="1dp"
        custom:valuesInterval="1"
        custom:valuesTextSize="8sp"/>

</RelativeLayout>

三、总结

现在三个工具尺子都更完了,各位好大哥好大姐看到这里点个赞吧~

相关推荐
500了1 小时前
Kotlin基本知识
android·开发语言·kotlin
人工智能的苟富贵2 小时前
Android Debug Bridge(ADB)完全指南
android·adb
小雨cc5566ru6 小时前
uniapp+Android面向网络学习的时间管理工具软件 微信小程序
android·微信小程序·uni-app
bianshaopeng8 小时前
android 原生加载pdf
android·pdf
hhzz8 小时前
Linux Shell编程快速入门以及案例(Linux一键批量启动、停止、重启Jar包Shell脚本)
android·linux·jar
火红的小辣椒9 小时前
XSS基础
android·web安全
勿问东西11 小时前
【Android】设备操作
android
五味香11 小时前
C++学习,信号处理
android·c语言·开发语言·c++·学习·算法·信号处理
图王大胜13 小时前
Android Framework AMS(01)AMS启动及相关初始化1-4
android·framework·ams·systemserver
工程师老罗14 小时前
Android Button “No speakable text present” 问题解决
android