android 坐标图绘制曲线,Android艺术之画一条平滑的曲线
前言
說(shuō)的是曲線,其實(shí)想法是來(lái)自一個(gè)曲線圖的需求。圖表這種東西,項(xiàng)目開(kāi)發(fā)中也不少見(jiàn),大多情況找個(gè)通用的開(kāi)源框架改改就得了(老板們別打我),然而通用趕不上腦洞,要做交互和視覺(jué)比較特別的圖表時(shí),還是自己造一個(gè)輪子比較靠譜,這次要研究的就是一個(gè)優(yōu)雅而平滑的曲線怎么畫(huà)出來(lái)。
實(shí)現(xiàn)方法分析
曲線圖的責(zé)任概括起來(lái)就是把數(shù)據(jù)輸出為對(duì)應(yīng)的圖像,我們這次需求的目標(biāo)效果圖是這樣的:
坐標(biāo)軸和指示線等功能不是這篇文章的重點(diǎn),拋開(kāi)它們先不討論,這次研究的重點(diǎn)在于曲線的繪制,而說(shuō)到繪制曲線,最常用的參數(shù)曲線函數(shù)就是貝塞爾曲線。
二次貝塞爾曲線:
三次貝塞爾曲線:
關(guān)于貝塞爾曲線更詳細(xì)的內(nèi)容戳這里,Android也提供了繪制貝塞爾曲線的方法,方法參數(shù)就是對(duì)應(yīng)貝塞爾曲線的控制點(diǎn):
// 二次貝塞爾曲線
path.quadTo(auxiliaryX, auxiliaryY, endPointX, endPointY);
canvas.drawPath(path, paint);
// 三次貝塞爾曲線
path.cubicTo(auxiliaryOneX, auxiliaryOneY, auxiliaryTwoX, auxiliaryTwoY, endPointX, endPointY);
canvas.drawPath(path, paint);
仔細(xì)分析目標(biāo)效果圖,我們可以把曲線圖拆分為一段段短的曲線,每?jī)蓚€(gè)數(shù)據(jù)點(diǎn)之間用三次貝塞爾曲線來(lái)繪制,保證曲線經(jīng)過(guò)每個(gè)數(shù)據(jù)點(diǎn),如下圖所示,紅色和藍(lán)色的線段分別是兩條三次貝塞爾曲線。
這樣我們的任務(wù)就變?yōu)榇_定每一段三次貝塞爾曲線四個(gè)控制點(diǎn)P0、P1、P2和P3的位置,毫無(wú)疑問(wèn),數(shù)據(jù)點(diǎn)可以作為曲線的起點(diǎn)P0和終點(diǎn)P3,某個(gè)數(shù)值點(diǎn)既是前一條曲線的終點(diǎn)也是后一條曲線的起點(diǎn),而確定剩下的P1和P2的位置還需要一個(gè)重要的課堂知識(shí):
光滑曲線必定處處可導(dǎo)。 —— 《高中數(shù)學(xué)·必修一》
要保證整條曲線圖是光滑的,關(guān)鍵在于貝塞爾曲線的連接點(diǎn)也要保證光滑,而三次貝塞爾曲線中P0和P1的連線就是P0點(diǎn)的切線,P2和P3的連線是P3的切線,所以我們只要保證數(shù)據(jù)點(diǎn)左右控制點(diǎn)共線,就可以保證曲線在數(shù)據(jù)點(diǎn)處是可導(dǎo)光滑的。
如圖所示,A0、A3、B3分別是三個(gè)數(shù)據(jù)點(diǎn),A1、A2、B1、B2是控制點(diǎn),A0、A1、A2和A3構(gòu)建了一條三次貝塞爾曲線,B0、B1、B2和B3也構(gòu)建了一條三次貝塞爾曲線,當(dāng)A2和B1共線時(shí),A2和B1的連線就是A3點(diǎn)的切線,數(shù)據(jù)點(diǎn)A3點(diǎn)就是可導(dǎo)光滑的。之后,只要我們使控制點(diǎn)A2、B1連線的斜率和A3左右數(shù)據(jù)點(diǎn)A0、B3連線的斜率保持一致,就可以讓曲線的效果更自然。
設(shè)A0的坐標(biāo)為(A0X,A0Y),A3的坐標(biāo)為(A3X,A3Y),B3的坐標(biāo)為(B3X,B3Y),控制點(diǎn)A2、B1的坐標(biāo)計(jì)算方法如下:
令
A0和B3連線的斜率 k = (B3Y - A0Y) / (B3X - A0X)
常數(shù) b = A3Y - k * A3X
則
A2的X坐標(biāo) A2X = A3X - (A3X - A0X) * rate
A2的Y坐標(biāo) A2Y = k * A2X + b
B1的X坐標(biāo) B1X = A3X + (B3X - A3X) * rate
B1的Y坐標(biāo) B1Y = k * B1X + b
rate是一個(gè)(0, 0.5)區(qū)間內(nèi)的值,數(shù)值越大,數(shù)值點(diǎn)之間的曲線弧度越小。 除此以外,如果數(shù)值點(diǎn)是第一個(gè)點(diǎn)或者最后一個(gè)點(diǎn),可以把斜率k視為0,然后只計(jì)算左控制點(diǎn)或者有控制點(diǎn)。 我們只要把每個(gè)數(shù)值點(diǎn)左右的控制點(diǎn)坐標(biāo)計(jì)算出來(lái),然后畫(huà)出每一段曲線,就可以組成一個(gè)完整的圓滑曲線了。
代碼實(shí)現(xiàn)
基本原理就是這么多,還是貼代碼實(shí)際。先計(jì)算全部數(shù)據(jù)點(diǎn)的坐標(biāo),用mValuePointList保存起來(lái),max是圖表顯示的最大值,scaleX和scaleY分別是單位長(zhǎng)度
private fun calculateValuePoint(itemList: List, max: Float, scaleX: Float, scaleY: Float){
mValuePointList.clear()
for ((i, item) in itemList.withIndex()) {
val x = i * scaleX
val y = (max - item.value) * scaleY
mValuePointList.add(PointF(x, y))
}
}
然后計(jì)算控制點(diǎn)的坐標(biāo),用mControlPointList保存起來(lái)
private fun calculateControlPoint(pointList: List){
mControlPointList.clear()
if (pointList.size <= 1) {
return
}
for ((i, point) in pointList.withIndex()) {
when (i) {
0 -> {//第一項(xiàng)
//添加后控制點(diǎn)
val nextPoint = pointList[i + 1]
val controlX = point.x + (nextPoint.x - point.x) * SMOOTHNESS
val controlY = point.y
mControlPointList.add(PointF(controlX, controlY))
}
pointList.size - 1 -> {//最后一項(xiàng)
//添加前控制點(diǎn)
val lastPoint = pointList[i - 1]
val controlX = point.x - (point.x - lastPoint.x) * SMOOTHNESS
val controlY = point.y
mControlPointList.add(PointF(controlX, controlY))
}
else -> {//中間項(xiàng)
val lastPoint = pointList[i - 1]
val nextPoint = pointList[i + 1]
val k = (nextPoint.y - lastPoint.y) / (nextPoint.x - lastPoint.x)
val b = point.y - k * point.x
//添加前控制點(diǎn)
val lastControlX = point.x - (point.x - lastPoint.x) * SMOOTHNESS
val lastControlY = k * lastControlX + b
mControlPointList.add(PointF(lastControlX, lastControlY))
//添加后控制點(diǎn)
val nextControlX = point.x + (nextPoint.x - point.x) * SMOOTHNESS
val nextControlY = k * nextControlX + b
mControlPointList.add(PointF(nextControlX, nextControlY))
}
}
}
}
最后繪制曲線和數(shù)值
//連接各部分曲線
mPath.reset()
val firstPoint = pointList.first()
mPath.moveTo(firstPoint.x, height)
mPath.lineTo(firstPoint.x, firstPoint.y)
for (i in 0 until pointList.size * 2 step 2) {
val leftControlPoint = controlPointList[i]
val rightControlPoint = controlPointList[i + 1]
val rightPoint = pointList[i / 2 + 1]
mPath.cubicTo(leftControlPoint.x, leftControlPoint.y, rightControlPoint.x, rightControlPoint.y, rightPoint.x, rightPoint.y)
}
val lastPoint = pointList.last()
//填充漸變色
mPath.lineTo(lastPoint.x, height)
mPath.lineTo(firstPoint.x, height)
mPaint.alpha = 255
mPaint.style = Paint.Style.FILL
mPaint.shader = LinearGradient(0F, 0F, 0F, height, COLOR_GRAPH_FILL, null, Shader.TileMode.CLAMP)
canvas.drawPath(mPath, mPaint)
//繪制全部路徑
mPath.setLastPoint(lastPoint.x, height)
mPaint.strokeWidth = SIZE_GRAPH
mPaint.style = Paint.Style.STROKE
mPaint.shader = null
mPaint.color = COLOR_GRAPH
canvas.drawPath(mPath, mPaint)
for (i in 0..pointList.size()) {
val point = pointList[i]
//畫(huà)數(shù)值線
mPaint.color = COLOR_POINT
mPaint.alpha = 100
canvas.drawLine(point.x, point.y, point.x, height, mPaint)
//畫(huà)數(shù)值點(diǎn)
mPaint.style = Paint.Style.FILL
mPaint.alpha = 255
canvas.drawCircle(point.x, point.y, SIZE_POINT, mPaint)
}
OK!大功告成,最終效果圖:
剩下的刻度效果和滑動(dòng)效果并不是太復(fù)雜,有時(shí)間再寫(xiě)一篇吧,謝謝各位看官支持。
本文來(lái)自網(wǎng)易實(shí)踐者社區(qū),經(jīng)作者馮文浩授權(quán)發(fā)布。
總結(jié)
以上是生活随笔為你收集整理的android 坐标图绘制曲线,Android艺术之画一条平滑的曲线的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: c语言清除html标签的方法,去除HTM
- 下一篇: unity3d读取android文本文件