安卓自定义View

您所在的位置:网站首页 操场如何画跑道 安卓自定义View

安卓自定义View

2024-04-27 03:22| 来源: 网络整理| 查看: 265

先上图:

安卓自定义View-类似操场跑道_android

由于是新手,代码通篇较简单,啰嗦之处还请见谅代码仅为关键部分,不是完整项目案例,更多功能可以自行拓展,这里只是记录一下并且给大佬们仅提供思路

核心思路 如图:

安卓自定义View-类似操场跑道_动画_02

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的左上角为起点的,不是右下角

安卓自定义View-类似操场跑道_ico_03

关于基线的知识可参考:链接: 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() } }



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3