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