前言
距離上一次寫東西已經(jīng)過去快一年了段磨,這一年發(fā)生了太多的事情了取逾。上家公司拖欠工資,都離職幾個(gè)月了到現(xiàn)在也沒給苹支,哎……砾隅。這么久沒寫東西,其實(shí)主要還是因?yàn)閼姓邸⑿那椴缓们绻 2贿^我現(xiàn)在的公司挺不錯(cuò)的,待遇還行寻定,主要是離家近儒洛,開車30分鐘左右就到家了,開心狼速。好了廢話就說這么多琅锻,下面言歸正傳。由于現(xiàn)在公司的項(xiàng)目中需要自定義的組件還是有點(diǎn)多的向胡,其中曲線圖和波浪進(jìn)度條這兩個(gè)組件花費(fèi)了一些時(shí)間恼蓬,之前我是談貝塞爾色變的,不過只從寫完這兩個(gè)組件我感覺僵芹,也就是那么回事处硬。所以這里記錄一下,一是為了幫助別的同學(xué)拇派,二是方便以后自己查閱荷辕。今天先從這個(gè)曲線圖開始,波浪進(jìn)度條會(huì)在后面的文章中為大家講解件豌。OK疮方,下面開始。
貝塞爾曲線
在開始之前大家還是要對(duì)貝塞爾曲線有一定的了解的茧彤,我覺得這一篇文章講的挺不錯(cuò)的案站,大家可以先去看下,先對(duì)貝塞爾曲線有一定的了解棘街。也可以自行百度一下,文章有很多承边,也挺好理解的遭殉。
效果圖
大家先來看下效果圖。
分析
從上圖可以看出博助,用貝塞爾曲線去畫這個(gè)圖再合適不過了险污。這里我們用3階貝塞爾曲線就可以畫的很完美了。
既然是曲線圖,所以在開始之前我們要先分析一下點(diǎn)蛔糯。
如上圖我已經(jīng)將所有的點(diǎn)給標(biāo)出來了拯腮,每?jī)蓚€(gè)點(diǎn)之間就是一個(gè)3階貝塞爾曲線。
知道了每?jī)蓚€(gè)點(diǎn)之前是一條貝塞爾曲線了,但是3階貝塞爾有兩個(gè)控制點(diǎn)淮逻,這兩個(gè)控制點(diǎn)應(yīng)該怎么定位琼懊?說到這里我就要給大家推薦一個(gè)不錯(cuò)的工具網(wǎng)站了,通過這個(gè)工具網(wǎng)站我們可以模擬畫出我們想要的樣子,然后再推算出公式爬早,這樣就很Eazy了哼丈。下面是我們來模擬一下第一個(gè)點(diǎn)到第二個(gè)點(diǎn)的曲線:
上圖中黑色的線就是我們想要的曲線,藍(lán)色的點(diǎn)是A就是起點(diǎn)(第一個(gè)點(diǎn))筛严,紅色的B點(diǎn)就是終點(diǎn)(第二個(gè)點(diǎn))醉旦,黃色的D點(diǎn)就是我們的控制點(diǎn)1,綠色的C點(diǎn)就是我們的控制點(diǎn)二桨啃。由此可以看出兩個(gè)控制點(diǎn)的X軸坐標(biāo)都是一樣的(兩個(gè)點(diǎn)的中間)车胡,那控制點(diǎn)X軸坐標(biāo)的計(jì)算公式如下:
- 公式一
控制點(diǎn)X軸坐標(biāo) = (終點(diǎn)X軸 - 起點(diǎn)X軸) / 2F + 起點(diǎn)X軸 - 公式二
控制點(diǎn)X軸坐標(biāo) = (終點(diǎn)X軸 + 起點(diǎn)X軸) / 2F
以上兩個(gè)公式看你高興,用哪個(gè)都一樣优幸。
控制的X軸確定了Y軸就簡(jiǎn)單了吨拍,控制點(diǎn)1的Y軸和起點(diǎn)一樣,控制點(diǎn)2的Y軸和終點(diǎn)一樣网杆。
控制點(diǎn)的X軸和Y軸都確定了那么就來寫下偽代碼吧:
val path = Path() //定義Path用來存放要畫的路徑
val startPoint = PointF() //假設(shè)這個(gè)是起點(diǎn)
val endPoint = PointF() //假設(shè)這個(gè)是終點(diǎn)
val centerX = (startPoint.x + endPoint.x) / 2F //根據(jù)上面的公式二計(jì)算控制點(diǎn)的X軸坐標(biāo)羹饰。
path.moveTo(startPoint.x, startPoint.y) //先使用move方法把畫筆移到起點(diǎn)位置。
path.cubicTo(centerX, startPoint.y, centerX, endPoint.y, endPoint.x, endPoint.y) //使用cubicTo方法繪制3階貝塞爾曲線碳却。
cubicTo方法的參數(shù)說明如下(按順序):
float x1 :控制點(diǎn)1的X軸坐標(biāo)队秩。
float y1 :控制點(diǎn)1的Y軸坐標(biāo)。
float x2 :控制點(diǎn)2的X軸坐標(biāo)昼浦。
float y2 :控制點(diǎn)2的Y軸坐標(biāo)馍资。
float x3 :終點(diǎn)的X軸坐標(biāo)。
float y3 :終點(diǎn)的Y軸坐標(biāo)关噪。
接下來我們?cè)儆蒙厦娴囊?guī)則(控制點(diǎn)的X軸在起點(diǎn)和終點(diǎn)之間)來模擬第二個(gè)點(diǎn)到第三個(gè)點(diǎn)試一下:
測(cè)量、計(jì)算
假設(shè)我們有一堆點(diǎn)使兔,不建钥!不要假設(shè),我們寫組件就是給外部調(diào)用的虐沥。SO…… 我們先提供一個(gè)方法讓別人把數(shù)據(jù)傳給我們:
//用來存放所有數(shù)據(jù)熊经。
private var dataList: List<Float> = emptyList()
//用來存放所有點(diǎn)泽艘。
private var points: List<PointF> = emptyList()
//用來記錄最大值。
private var maxValue: Float = 100F
//設(shè)置數(shù)據(jù)源镐依。
fun setData(data: List<Float>) {
dataList = data.apply {
points = map {
maxValue = if (maxValue >= it) maxValue else it
PointF()
}
}
}
由于數(shù)據(jù)源有幾個(gè)我們就需要有多少個(gè)點(diǎn)匹涮,所以我在設(shè)置數(shù)據(jù)源的時(shí)候直接創(chuàng)建出相應(yīng)個(gè)數(shù)的點(diǎn)(PointF)并且計(jì)算出最大值。
既然有點(diǎn)了我們就開始來計(jì)算這些點(diǎn)的位置吧槐壳。
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val w = measuredWidth //獲取當(dāng)前View的寬度
val h = measuredHeight //獲取當(dāng)前View的高度
val availableH = h - paddingTop - paddingBottom - lineWidth //計(jì)算真正可用的高度然低,lineWidth 是曲線畫筆的strokeWidth。把它減掉是為了防止曲線畫不完整宏粤。
//計(jì)算單個(gè)間距的寬度
val oneSpace = (w.toFloat() - paddingStart - paddingEnd) / dataList.lastIndex
//計(jì)算左邊的邊距
val leftStart = paddingStart + lineWidth * 0.5F
//計(jì)算曲線圖頂部的距離
val graphTop = paddingTop + lineWidth * 0.5F
points.forEachIndexed { i, p ->
p.x = leftStart + i * oneSpace //計(jì)算每個(gè)點(diǎn)的X軸坐標(biāo)
p.y = graphTop + (availableH - dataList[i] / maxValue * availableH) //計(jì)算每個(gè)點(diǎn)Y軸的坐標(biāo)脚翘,頂部的距離+當(dāng)前數(shù)據(jù)占最大值的百分比然后換算出占View高度的百分比。
}
得到所有的點(diǎn)之后就利用我們上面分析的來繪制曲線就OK了:
//計(jì)算曲線圖路徑
linePath.reset()
var startP: PointF
var endP: PointF
for (i in 0 until points.lastIndex) {
startP = points[i]
endP = points[i + 1]
if (i == 0) {
linePath.moveTo(startP.x, startP.y)
}
((startP.x + endP.x) / 2F).also {
linePath.cubicTo(it, startP.y, it, endP.y, endP.x, endP.y)
}
}
最后就是用canvas畫出來绍哎。
canvas.drawPath(linePath, graphPaint)
到這里就已經(jīng)可以把我們要的曲線畫出來了来农。至于漸變背景填充色,坐標(biāo)線啥的就So Easy了崇堰。以下是全部的代碼:
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.view.View
/**
* **描述:** 貝塞爾曲線圖組件
*
* **創(chuàng)建人:** kelin
*
* **創(chuàng)建時(shí)間:** 2021/9/3 6:26 PM
*
* **版本:** v 1.0.0
*/
class GraphView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : View(context, attrs, defStyleAttr) {
/**
* 用來存放曲線路徑沃于。
*/
private val linePath = Path()
/**
* 用來存放曲線圖背景的路徑。
*/
private val graphBgPath = Path()
/**
* 用來存放所有數(shù)據(jù)海诲。
*/
private var dataList: List<Float> = emptyList()
/**
* 用來記錄最大值繁莹。
*/
private var maxValue: Float = 0F
/**
* 用來存放所有點(diǎn)。
*/
private var points: List<PointF> = emptyList()
/**
* 用來定義選中軸的標(biāo)線所超出圖表的大小特幔。
*/
private val axisLineOverlySize = 4.dp2pxF
/**
* 定義曲線的寬度咨演。
*/
private val lineWidth = 4.dp2pxF
/**
* 用來記錄最大的點(diǎn)的位置。
*/
private var maxPoint: PointF = PointF()
/**
* 定義用來畫曲線的畫筆蚯斯。
*/
private val graphPaint by lazy {
Paint(Paint.ANTI_ALIAS_FLAG).apply {
strokeWidth = lineWidth
strokeCap = Paint.Cap.ROUND
}
}
/**
* 定義曲線圖背景漸變著色器薄风。
*/
private val graphBgGradient by lazy {
LinearGradient(
0F,
0F,
0F,
measuredHeight.toFloat(),
intArrayOf(Color.parseColor("#7DBAEFE6"), Color.parseColor("#7DD7F5F0"), Color.parseColor("#7DF9FEFD"), Color.WHITE),
listOf(0.5F, 0.65F, 0.85F, 1F).toFloatArray(),
Shader.TileMode.REPEAT
)
}
/**
* 設(shè)置數(shù)據(jù)源。
*/
fun setData(data: List<Float>) {
dataList = data.apply {
points = map {
maxValue = if (maxValue >= it) maxValue else it
PointF()
}
}
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val w = measuredWidth
val h = measuredHeight
val availableH = h - paddingTop - paddingBottom - lineWidth * 4F - axisLineOverlySize
//計(jì)算單個(gè)間距的寬度
val oneSpace = (w.toFloat() - lineWidth * 3F - paddingStart - paddingEnd) / dataList.lastIndex //計(jì)算左邊的邊距
val leftStart = paddingStart + lineWidth * 1.5F
val graphTop = paddingTop + lineWidth * 1.5F + axisLineOverlySize
points.forEachIndexed { i, p ->
p.x = leftStart + i * oneSpace
dataList[i].also {
p.y = graphTop + (availableH - it / maxValue * availableH)
if (maxPoint.y == 0F && maxValue == it) {
maxPoint = p
}
}
}
//計(jì)算曲線圖路徑
linePath.reset()
var startP: PointF
var endP: PointF
for (i in 0 until points.lastIndex) {
startP = points[i]
endP = points[i + 1]
if (i == 0) {
linePath.moveTo(startP.x, startP.y)
}
((startP.x + endP.x) / 2F).also {
linePath.cubicTo(it, startP.y, it, endP.y, endP.x, endP.y)
}
}
//計(jì)算曲線圖背景路徑拍嵌。
graphBgPath.set(linePath)
val bottom = h - paddingBottom - axisLineOverlySize
graphBgPath.lineTo(points.last().x, bottom)
graphBgPath.lineTo(leftStart, bottom)
}
override fun onDraw(canvas: Canvas) {
//畫漸變底色
graphPaint.apply {
style = Paint.Style.FILL
shader = graphBgGradient
}
canvas.drawPath(graphBgPath, graphPaint)
//畫曲線線條
graphPaint.apply {
color = Color.parseColor("#0AA490")
style = Paint.Style.STROKE
shader = null
}
canvas.drawPath(linePath, graphPaint)
//畫當(dāng)前軸的軸線
graphPaint.strokeWidth = 0.8F.dp2pxF
canvas.drawLine(maxPoint.x, 0F + paddingTop, maxPoint.x, height.toFloat() - paddingBottom, graphPaint)
//畫當(dāng)前坐標(biāo)點(diǎn)
graphPaint.style = Paint.Style.FILL
canvas.drawCircle(maxPoint.x, maxPoint.y, lineWidth, graphPaint)
graphPaint.strokeWidth = lineWidth * 0.7F
graphPaint.color = Color.BLACK
graphPaint.style = Paint.Style.STROKE
canvas.drawCircle(maxPoint.x, maxPoint.y, lineWidth, graphPaint)
}
}
val Int.dp2pxF: Float
get() = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, toFloat(), AppModule.getContext().resources.displayMetrics)
說明
我們需求是不能左右滑動(dòng)的遭赂,而且滑動(dòng)也不再本篇文章的討論范疇,所以并不支持左右滑動(dòng)横辆。另外我們的需求是不能手動(dòng)點(diǎn)擊改變當(dāng)前坐標(biāo)的撇他,只能選中最大坐標(biāo)。
實(shí)現(xiàn)效果
最后貼出我實(shí)現(xiàn)的效果
最后
如果你喜歡本文內(nèi)容困肩,或本文內(nèi)容對(duì)你有所幫助,還請(qǐng)點(diǎn)贊脆侮、收藏哦僻弹。您的支持是我繼續(xù)創(chuàng)作的動(dòng)力。