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>

三、总结

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

相关推荐
千里马学框架5 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台5 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone5 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc5 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo5 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077005 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼5 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone5 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen5 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone5 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui