Android貝塞爾曲線之曲線圖

前言

距離上一次寫東西已經(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)蛔糯。

曲線圖中的點(diǎn)

如上圖我已經(jīng)將所有的點(diǎn)給標(biāo)出來了拯腮,每?jī)蓚€(gè)點(diǎn)之間就是一個(gè)3階貝塞爾曲線。
我是3階貝塞爾曲線蚁飒,我長(zhǎng)這樣动壤。

知道了每?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ì)算公式如下:

  1. 公式一
    控制點(diǎn)X軸坐標(biāo) = (終點(diǎn)X軸 - 起點(diǎn)X軸) / 2F + 起點(diǎn)X軸
  2. 公式二
    控制點(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)的效果


完美還原UI狈蚤,嘻嘻

最后

如果你喜歡本文內(nèi)容困肩,或本文內(nèi)容對(duì)你有所幫助,還請(qǐng)點(diǎn)贊脆侮、收藏哦僻弹。您的支持是我繼續(xù)創(chuàng)作的動(dòng)力。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末他嚷,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌筋蓖,老刑警劉巖卸耘,帶你破解...
    沈念sama閱讀 217,657評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異粘咖,居然都是意外死亡蚣抗,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,889評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門瓮下,熙熙樓的掌柜王于貴愁眉苦臉地迎上來翰铡,“玉大人,你說我怎么就攤上這事讽坏《В” “怎么了?”我有些...
    開封第一講書人閱讀 164,057評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵路呜,是天一觀的道長(zhǎng)迷捧。 經(jīng)常有香客問我,道長(zhǎng)胀葱,這世上最難降的妖魔是什么漠秋? 我笑而不...
    開封第一講書人閱讀 58,509評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮抵屿,結(jié)果婚禮上庆锦,老公的妹妹穿的比我還像新娘。我一直安慰自己轧葛,他們只是感情好搂抒,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,562評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著朝群,像睡著了一般燕耿。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上姜胖,一...
    開封第一講書人閱讀 51,443評(píng)論 1 302
  • 那天誉帅,我揣著相機(jī)與錄音,去河邊找鬼右莱。 笑死蚜锨,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的慢蜓。 我是一名探鬼主播亚再,決...
    沈念sama閱讀 40,251評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼晨抡!你這毒婦竟也來了氛悬?” 一聲冷哼從身側(cè)響起则剃,我...
    開封第一講書人閱讀 39,129評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎如捅,沒想到半個(gè)月后棍现,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,561評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡镜遣,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,779評(píng)論 3 335
  • 正文 我和宋清朗相戀三年己肮,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片悲关。...
    茶點(diǎn)故事閱讀 39,902評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡谎僻,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出寓辱,到底是詐尸還是另有隱情艘绍,我是刑警寧澤,帶...
    沈念sama閱讀 35,621評(píng)論 5 345
  • 正文 年R本政府宣布讶舰,位于F島的核電站鞍盗,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏跳昼。R本人自食惡果不足惜般甲,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,220評(píng)論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望鹅颊。 院中可真熱鬧敷存,春花似錦、人聲如沸堪伍。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,838評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽帝雇。三九已至涮俄,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間尸闸,已是汗流浹背彻亲。 一陣腳步聲響...
    開封第一講書人閱讀 32,971評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留吮廉,地道東北人苞尝。 一個(gè)月前我還...
    沈念sama閱讀 48,025評(píng)論 2 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像宦芦,于是被迫代替她去往敵國(guó)和親宙址。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,843評(píng)論 2 354