安卓自定义View-类似操场跑道
先上圖:
由于是新手,代碼通篇較簡單,啰嗦之處還請見諒
代碼僅為關鍵部分,不是完整項目案例,更多功能可以自行拓展,這里只是記錄一下并且給大佬們僅提供思路
核心思路 如圖:
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//外部線pathlateinit var outPath: Path//中間線pathlateinit var centerPath: Path//中間運動片段pathprivate val dstPath = Path()//跑道背景pathlateinit var bgPath: Path//內部線pathlateinit 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 = 0fprivate 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 = innerColorstyle = Paint.Style.STROKEstrokeWidth = dp2px(2).toFloat()}centerPaint = Paint(Paint.ANTI_ALIAS_FLAG or Paint.DITHER_FLAG).apply {color = centreColorstyle = Paint.Style.STROKEstrokeWidth = dp2px(1).toFloat()}//虛線 參數1:{虛線長度,虛線間隔} 參數2:開始的偏移量val dashPathEffect =DashPathEffect(floatArrayOf(dp2px(10).toFloat(),dp2px(5).toFloat()), 0f)centerPaint.pathEffect = dashPathEffectoutPaint = Paint(Paint.ANTI_ALIAS_FLAG or Paint.DITHER_FLAG).apply {color = outColorstyle = Paint.Style.STROKEstrokeWidth = dp2px(2).toFloat()}bgPaint = Paint(Paint.ANTI_ALIAS_FLAG or Paint.DITHER_FLAG).apply {color = Color.parseColor("#55ffffff")style = Paint.Style.STROKEstrokeWidth = (lineSpace * 2).toFloat()}centerTextPaint = Paint(Paint.ANTI_ALIAS_FLAG or Paint.DITHER_FLAG).apply {color = Color.BLUEstrokeWidth = sp2px(2).toFloat()textSize = sp2px(30).toFloat()}textPaint = Paint(Paint.ANTI_ALIAS_FLAG or Paint.DITHER_FLAG).apply {color = Color.BLACKtextSize = sp2px(10).toFloat()}mTextBgPaint = Paint(Paint.ANTI_ALIAS_FLAG or Paint.DITHER_FLAG).apply {color = -0x4c080809style = Paint.Style.FILL_AND_STROKEstrokeWidth = sp2px(10 + 4).toFloat()strokeCap = Paint.Cap.ROUND}iconPaint = Paint(Paint.ANTI_ALIAS_FLAG or Paint.DITHER_FLAG).apply {style = Paint.Style.STROKEstrokeWidth = 1f}dstPaint = Paint(Paint.ANTI_ALIAS_FLAG or Paint.DITHER_FLAG).apply {style = Paint.Style.STROKEstrokeWidth = dp2px(5).toFloat()strokeCap = Paint.Cap.ROUNDcolor = 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//跑道距邊緣的Paddingval leftPadding = dp2px(20)val rightPadding = dp2px(20)val topPadding = dp2px(10)val bottomPadding = dp2px(10)//畫外面//畫外面val outWidth = outPaint.strokeWidth.toInt()//最外面跑道線val x = outWidth / 2 + leftPaddingval y = outWidth / 2 + topPaddingval x1 = width - outWidth / 2 - rightPaddingval y1 = height - outWidth / 2 - bottomPaddingval outSpace = y1 - y //實際高度outRect = RectF(x.toFloat(), y.toFloat(), x1.toFloat(), y1.toFloat())//輔助矩形1val ox1 = xval oy1 = yval ox11 = x + outSpaceval oy11 = y + outSpaceoutRect1 = RectF(ox1.toFloat(), oy1.toFloat(), ox11.toFloat(), oy11.toFloat())//輔助矩形2val ox2 = x1 - outSpaceval oy2 = yval ox12 = x1val oy12 = y1outRect2 = 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 + lineSpaceval cy = y + lineSpaceval cx1 = x1 - lineSpaceval cy1 = y1 - lineSpace//中間矩形實際高度val centerHeight = cy1 - cycenterRect = RectF(cx.toFloat(), cy.toFloat(), cx1.toFloat(), cy1.toFloat())//輔助矩形1val cx2 = cxval cy2 = cyval cx12 = cx + centerHeightval cy12 = cy + centerHeightcenterRect1 = RectF(cx2.toFloat(), cy2.toFloat(), cx12.toFloat(), cy12.toFloat())//輔助矩形2val cx3 = cx1 - centerHeightval cy3 = cyval cx13 = cx1val cy13 = cy1centerRect2 = 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 / 2startPoint.y = cy//畫里面//畫里面val ix = cx + lineSpaceval iy = cy + lineSpaceval ix1 = cx1 - lineSpaceval iy1 = cy1 - lineSpace//里面矩形實際高度val innerHeight = iy1 - iyinnerRect = RectF(ix.toFloat(), iy.toFloat(), ix1.toFloat(), iy1.toFloat())//輔助矩形1val ix2 = ixval iy2 = iyval ix12 = ix + innerHeightval iy12 = iy + innerHeightinnerRectL = RectF(ix2.toFloat(), iy2.toFloat(), ix12.toFloat(), iy12.toFloat())//輔助矩形2val ix3 = ix1 - innerHeightval iy3 = iyval ix13 = ix1val iy13 = iy1innerRectR = 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.lengthicon = 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)//單人模式//畫已運動距離路徑,已運動的pathdstPath.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 / 2val halfHeight = icon.height / 2canvas.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: Floatval tx: Float = x - (textBounds.width() / 2)//確定基線-不了解的可以去了解下val metricsInt = textPaint.fontMetricsIntval dy = (metricsInt.bottom - metricsInt.top) / 2 - metricsInt.bottomty = y + dy - spacing//用于給文字加個背景 文字的中間水平線Yval 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.scaledDensityreturn (spValue * fontScale + 0.5f).toInt()}fun dp2px(dp: Int): Int {val scale = resources.displayMetrics.densityreturn (dp * scale + 0.5f).toInt()}}總結
以上是生活随笔為你收集整理的安卓自定义View-类似操场跑道的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: First Day Of Second
- 下一篇: 怎样用迅捷PDF转换器删除PDF中的图片