一、简介
其他两种尺子已经做出来了
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,还有很多可以优化的地方,做的不好的地方欢迎大家指出