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>

三、总结

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

相关推荐
andr_gale27 分钟前
04_rc文件语法规则
android·framework·aosp
祖国的好青年1 小时前
VS Code 搭建 React Native 开发环境(Windows 实战指南)
android·windows·react native·react.js
黄林晴2 小时前
警惕!AGP 9.2 别只改版本号,R8 规则与构建链路全线收紧
android·gradle
小米渣的逆袭2 小时前
Android ADB 完全使用指南
android·adb
儿歌八万首2 小时前
Jetpack Compose Canvas 进阶:结合 animateFloatAsState 让自定义图形动起来
android·动画·compose
zhangphil3 小时前
Android Page 3 Flow读sql数据库媒体文件,Kotlin
android·kotlin
神探小白牙4 小时前
echarts,3d堆叠图
android·3d·echarts
李白的天不白4 小时前
如何项目发布到github上
android·vue.js
summerkissyou19874 小时前
Android-RTC、NTP 和 System Time(系统时间)
android