一、简介
Android自定义View—量角器 - 掘金 (juejin.cn)
Android自定义View—直尺 - 掘金 (juejin.cn)
前面两篇文章分别实现了量角器和直尺,现在我们来实现等腰直角三角尺,老规矩,先看效果图:
二、步骤
这个等腰直角三角尺的实现比其他两个要复杂点,因为涉及到数字和刻度线的旋转,运用到比较多的数学知识
首先我们得先复习下等腰直角三角形的基础知识:
直角边:直角边:斜边 = 1:1:√2
斜边^2 = 直角边^2 + 直角边^2
2.1 画三角形背景
外三角
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()挖空一个内三角
//等腰内三角形
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的倍数,画长线,不是则画中线
//根据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++
}
纵向刻度
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°,就可以将线画在斜边上了,注意这里的旋转基点是左下角,并不是线本身
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 画刻度值
横向
横向的跟之前直尺的差不多
val valueString =
(position / intervalsBetweenValues * valuesInterval).toString()
drawText(
valueString,
width - drawPaint.measureText(valueString) / 2,
minLength * 2.5f + textHeight / 2,
drawPaint
)
纵向
纵向的文字我们需要围绕文字的中心去顺时针旋转90度
// 计算旋转后的坐标
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度
// 计算旋转后的坐标
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
/**
* 等腰直角三角尺
*/
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 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
<?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>
三、总结
现在三个工具尺子都更完了,各位好大哥好大姐看到这里点个赞吧~