先上图:
由于是新手,代码通篇较简单,啰嗦之处还请见谅
代码仅为关键部分,不是完整项目案例,更多功能可以自行拓展,这里只是记录一下并且给大佬们仅提供思路
核心思路 如图:
addArc
:用于添加一个圆弧路径到当前路径中
arcTo
:与 addArc 方法类似,也是用于添加一个圆弧路径到当前路径中,但与 addArc 不同的是,arcTo 方法可以指定圆弧的起点和终点,而不会自动连接这两个点 如上图所示
lineTo
:用于从当前路径的最后一个点绘制一条直线到指定的点
PathMeasure
:
PathMeasure 类的主要作用是计算路径的长度,以及用于获取路径上的坐标点。具体应用场景如下:
-
绘制动画:可以使用 PathMeasure 获取路径的长度,然后在指定的时间内,通过改变绘制位置的距离和路径长度的比例,来实现路径动画的效果。
-
滑动解锁:可以使用 PathMeasure 获取路径的长度,然后通过手势的滑动速度和路径长度的比例来判断是否达到解锁阈值,从而实现滑动解锁功能。
-
曲线绘制:可以使用 PathMeasure 获取路径上的坐标点,然后通过这些坐标点来绘制曲线。
PathMeasure 类的常用方法如下:
-
PathMeasure(Path path, boolean forceClosed):创建一个用于测量给定路径的 PathMeasure 对象,其中 path 参数是要测量的路径对象,forceClosed 参数表示是否强制关闭路径。
-
getLength()
:获取路径的总长度。 -
getSegment(float startD, float stopD, Path dst, boolean startWithMoveTo)
:从路径上获取一段路径段,并将其存储在另一个 Path 对象中,其中 startD 和 stopD 参数表示要获取路径的起始和结束位置,dst 参数表示存储路径的 Path 对象,startWithMoveTo 参数表示是否在起始点插入一个 moveTo 命令。 -
getPosTan(float distance, float[] pos, float[] tan)
:获取路径上指定距离的坐标位置与正切向量,并存储在相应的数组中,其中 distance 参数表示路径的距离位置,pos 数组存储路径上该位置的坐标,tan 数组存储路径上该位置的正切向量。 -
nextContour():移动到下一个(若存在)封闭子路径上,并返回是否成功移动。
可以根据需要使用上述方法来计算路径的长度、获取路径上的坐标点、绘制路径动画等等应用场景。
关于绘制文字的一些知识点,一张图片概括:
注意文字绘制 是以view的左上角为起点的,不是右下角
关于基线的知识可参考:链接: Android文字基线(Baseline)算法
下面是该View的全部代码
/**
* @author: 听风
* @date: 2023/5/29
*
* 宽 高 比 2.3 :1
*/
class TrackView(context: Context) : View(context) {
constructor(context: Context, attrs: AttributeSet) : this(context) {
lineSpace = dp2px(20)
initPaint()
}
//内部线画笔 内部跑道线
lateinit var innerPaint: Paint
//中间线画笔 中间跑道线
lateinit var centerPaint: Paint
//外部线画笔 外部跑道线
lateinit var outPaint: Paint
//跑道背景色
lateinit var bgPaint: Paint
//中间文字画笔
lateinit var centerTextPaint: Paint
//用户名画笔
lateinit var textPaint: Paint
//名字背景画笔
lateinit var mTextBgPaint: Paint
//icon 画笔
lateinit var iconPaint: Paint
//已运动距离画笔
lateinit var dstPaint: Paint
//外部线path
lateinit var outPath: Path
//中间线path
lateinit var centerPath: Path
//中间运动片段path
private val dstPath = Path()
//跑道背景path
lateinit var bgPath: Path
//内部线path
lateinit var innerPath: Path
//内部线颜色
private val innerColor = Color.BLACK
//中间线颜色
private val centreColor = Color.GRAY
//最外部线颜色
private val outColor = Color.BLACK
//跑道线之间间隔
private var lineSpace = 0
//内部矩形
lateinit var innerRect: RectF //内部矩形
lateinit var innerRectL: RectF //内部矩形
lateinit var innerRectR: RectF
//中间矩形
lateinit var centerRect: RectF //中间矩形
lateinit var centerRect1: RectF //中间矩形
lateinit var centerRect2: RectF
//外部矩形
lateinit var outRect: RectF //外部矩形
lateinit var outRect1: RectF //外部矩形
lateinit var outRect2: RectF
//测量中间文字用
private val textBounds = Rect()
//画布宽高
private var mWidth = 0 //画布宽高
private var mHeight = 0
//
private val mContext: Context? = null
//是否初始化完成 防止重复测量
private var inited = false
//
lateinit var icon: Bitmap
//
lateinit var startBp: Bitmap
//起点图片坐标
private val startPoint = Point()
//测量已运动路径用
var measure = PathMeasure()
//中间跑道路径的实际长度
var pathLength = 0f
private val singleDistance = 0f //单人已运动距离
private val sumDis = 400 //操场总距离
//用来记录path上某距离处坐标
var point = FloatArray(2)
private fun initPaint() {
innerPaint = Paint(Paint.ANTI_ALIAS_FLAG or Paint.DITHER_FLAG).apply {
color = innerColor
style = Paint.Style.STROKE
strokeWidth = dp2px(2).toFloat()
}
centerPaint = Paint(Paint.ANTI_ALIAS_FLAG or Paint.DITHER_FLAG).apply {
color = centreColor
style = Paint.Style.STROKE
strokeWidth = dp2px(1).toFloat()
}
//虚线 参数1:{虚线长度,虚线间隔} 参数2:开始的偏移量
val dashPathEffect =
DashPathEffect(
floatArrayOf(
dp2px(10).toFloat(),
dp2px(5).toFloat()
), 0f
)
centerPaint.pathEffect = dashPathEffect
outPaint = Paint(Paint.ANTI_ALIAS_FLAG or Paint.DITHER_FLAG).apply {
color = outColor
style = Paint.Style.STROKE
strokeWidth = dp2px(2).toFloat()
}
bgPaint = Paint(Paint.ANTI_ALIAS_FLAG or Paint.DITHER_FLAG).apply {
color = Color.parseColor("#55ffffff")
style = Paint.Style.STROKE
strokeWidth = (lineSpace * 2).toFloat()
}
centerTextPaint = Paint(Paint.ANTI_ALIAS_FLAG or Paint.DITHER_FLAG).apply {
color = Color.BLUE
strokeWidth = sp2px(2).toFloat()
textSize = sp2px(30).toFloat()
}
textPaint = Paint(Paint.ANTI_ALIAS_FLAG or Paint.DITHER_FLAG).apply {
color = Color.BLACK
textSize = sp2px(10).toFloat()
}
mTextBgPaint = Paint(Paint.ANTI_ALIAS_FLAG or Paint.DITHER_FLAG).apply {
color = -0x4c080809
style = Paint.Style.FILL_AND_STROKE
strokeWidth = sp2px(10 + 4).toFloat()
strokeCap = Paint.Cap.ROUND
}
iconPaint = Paint(Paint.ANTI_ALIAS_FLAG or Paint.DITHER_FLAG).apply {
style = Paint.Style.STROKE
strokeWidth = 1f
}
dstPaint = Paint(Paint.ANTI_ALIAS_FLAG or Paint.DITHER_FLAG).apply {
style = Paint.Style.STROKE
strokeWidth = dp2px(5).toFloat()
strokeCap = Paint.Cap.ROUND
color = Color.BLUE
}
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
//可能会被多次测量
if (!inited) {
mWidth = MeasureSpec.getSize(widthMeasureSpec)
mHeight = MeasureSpec.getSize(heightMeasureSpec)
inited = true
}
}
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
super.onLayout(changed, left, top, right, bottom)
initRect()
}
private fun initRect() {
//跑道距边缘的Padding
//跑道距边缘的Padding
val leftPadding = dp2px(20)
val rightPadding = dp2px(20)
val topPadding = dp2px(10)
val bottomPadding = dp2px(10)
//画外面
//画外面
val outWidth = outPaint.strokeWidth.toInt()
//最外面跑道线
val x = outWidth / 2 + leftPadding
val y = outWidth / 2 + topPadding
val x1 = width - outWidth / 2 - rightPadding
val y1 = height - outWidth / 2 - bottomPadding
val outSpace = y1 - y //实际高度
outRect = RectF(x.toFloat(), y.toFloat(), x1.toFloat(), y1.toFloat())
//辅助矩形1
val ox1 = x
val oy1 = y
val ox11 = x + outSpace
val oy11 = y + outSpace
outRect1 = RectF(ox1.toFloat(), oy1.toFloat(), ox11.toFloat(), oy11.toFloat())
//辅助矩形2
val ox2 = x1 - outSpace
val oy2 = y
val ox12 = x1
val oy12 = y1
outRect2 = RectF(ox2.toFloat(), oy2.toFloat(), ox12.toFloat(), oy12.toFloat())
//闭合
outPath = Path()
outPath.addArc(outRect2, 270f, 180f)
outPath.arcTo(outRect1, 90f, 180f)
outPath.lineTo((x1 - outSpace / 2).toFloat(), y.toFloat())
//画中间-中间和里面都是重复上面步骤 只是矩形长宽等距离缩小
//画中间
val cx = x + lineSpace
val cy = y + lineSpace
val cx1 = x1 - lineSpace
val cy1 = y1 - lineSpace
//中间矩形实际高度
val centerHeight = cy1 - cy
centerRect = RectF(cx.toFloat(), cy.toFloat(), cx1.toFloat(), cy1.toFloat())
//辅助矩形1
val cx2 = cx
val cy2 = cy
val cx12 = cx + centerHeight
val cy12 = cy + centerHeight
centerRect1 = RectF(cx2.toFloat(), cy2.toFloat(), cx12.toFloat(), cy12.toFloat())
//辅助矩形2
val cx3 = cx1 - centerHeight
val cy3 = cy
val cx13 = cx1
val cy13 = cy1
centerRect2 = RectF(cx3.toFloat(), cy3.toFloat(), cx13.toFloat(), cy13.toFloat())
//闭合
centerPath = Path()
centerPath.addArc(centerRect2, 270f, 180f)
centerPath.arcTo(centerRect1, 90f, 180f)
centerPath.lineTo((cx1 - centerHeight / 2).toFloat(), cy.toFloat())
startPoint.x = cx1 - centerHeight / 2
startPoint.y = cy
//画里面
//画里面
val ix = cx + lineSpace
val iy = cy + lineSpace
val ix1 = cx1 - lineSpace
val iy1 = cy1 - lineSpace
//里面矩形实际高度
val innerHeight = iy1 - iy
innerRect = RectF(ix.toFloat(), iy.toFloat(), ix1.toFloat(), iy1.toFloat())
//辅助矩形1
val ix2 = ix
val iy2 = iy
val ix12 = ix + innerHeight
val iy12 = iy + innerHeight
innerRectL = RectF(ix2.toFloat(), iy2.toFloat(), ix12.toFloat(), iy12.toFloat())
//辅助矩形2
val ix3 = ix1 - innerHeight
val iy3 = iy
val ix13 = ix1
val iy13 = iy1
innerRectR = RectF(ix3.toFloat(), iy3.toFloat(), ix13.toFloat(), iy13.toFloat())
//闭合
innerPath = Path()
innerPath.addArc(innerRectR, 270f, 180f)
innerPath.arcTo(innerRectL, 90f, 180f)
innerPath.lineTo((ix1 - innerHeight / 2).toFloat(), iy.toFloat())
bgPath = centerPath
//测量路径
measure.setPath(centerPath, false)
pathLength = measure.length
icon = BitmapFactory.decodeResource(context.resources, R.drawable.icon_runaway)
startBp = BitmapFactory.decodeResource(context.resources, R.drawable.start)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
//跑道背景
canvas.drawPath(bgPath, bgPaint)
//三条跑道线
//三条跑道线
canvas.drawPath(outPath, outPaint)
canvas.drawPath(centerPath, centerPaint)
canvas.drawPath(innerPath, innerPaint)
//起点图片
canvas.drawBitmap(
startBp!!,
(startPoint.x - (startBp.width * 2)).toFloat(),
(startPoint.y - (startBp.height * 2)).toFloat(),
iconPaint
)
//单人模式
//画已运动距离路径,已运动的path
dstPath.reset()
measure.setPath(centerPath, false)
//拿到从0开始到距离为singleDistance 长度的 path片段
measure.getSegment(0f, getPosition(singleDistance), dstPath, true)
canvas.drawPath(dstPath, dstPaint)
//拿到centerPath 上距离为 singleDistance 的终点坐标
measure.getPosTan(getPosition(singleDistance), point, null)
//画头像
val halfWidth = icon.width / 2
val halfHeight = icon.height / 2
canvas.drawBitmap(icon, point[0] - halfWidth, point[1] - halfHeight, iconPaint)
//画名字
drawName(canvas, point, halfHeight, "微风轻起");
//...
}
private fun drawName(canvas: Canvas, point: FloatArray, halfHeight: Int, name: String) {
//画名字
//头像顶部中心坐标
val x = point[0]
val y = point[1] - halfHeight
//名字距离头像的间隔
val spacing = dp2px(4).toFloat()
textBounds.setEmpty()
//计算给定字符串的边界,并将结果保存在给定的 textBounds 对象中。
textPaint.getTextBounds(name, 0, name.length, textBounds)
//画 文字坐标
val ty: Float
val tx: Float = x - (textBounds.width() / 2)
//确定基线-不了解的可以去了解下
val metricsInt = textPaint.fontMetricsInt
val dy = (metricsInt.bottom - metricsInt.top) / 2 - metricsInt.bottom
ty = y + dy - spacing
//用于给文字加个背景 文字的中间水平线Y
val ly = ty - (textBounds.height() / 2)
canvas.drawLine(tx, ly, tx + textBounds.width(), ly, mTextBgPaint)
canvas.drawText(name, tx, ty, textPaint)
}
/**
* 预设:跑道 400米
* 通过实际距离 经过和预设跑道距离 转换,得到 distance 在跑道上的具体距离,进而确定位置
*/
private fun getPosition(distance: Float): Float {
return pathLength * distance / sumDis % pathLength
}
fun sp2px(spValue: Int): Int {
val fontScale = resources.displayMetrics.scaledDensity
return (spValue * fontScale + 0.5f).toInt()
}
fun dp2px(dp: Int): Int {
val scale = resources.displayMetrics.density
return (dp * scale + 0.5f).toInt()
}
}