利用kotlin實(shí)現(xiàn)一個(gè)餅圖實(shí)例代碼
前言
餅圖是許多人最熟悉的圖表類(lèi)型,也是使用頻率最高的圖表類(lèi)型之一,本文主要給大家介紹了關(guān)于利用kotlin實(shí)現(xiàn)餅圖的相關(guān)內(nèi)容,分享出來(lái)供大家參考學(xué)習(xí),代碼不難,所以打算用kotlin來(lái)實(shí)現(xiàn),增加熟練度,下面來(lái)一起看看吧。
先看看做的是什么

看完圖,我們來(lái)整理下思路
- 餅圖居中,每塊區(qū)域都是一個(gè)扇形,需要canvas.drawArc根據(jù)角度來(lái)繪制
- 需要path.arcTo定位到扇形弧度的一半來(lái)繪制折線(xiàn)的起點(diǎn)
- 通過(guò)canvas.drawPath繪制折線(xiàn),折線(xiàn)的長(zhǎng)度根據(jù)餅圖大小來(lái)設(shè)置比例
- 通過(guò)canvas.drawText繪制文字,文字的大小根據(jù)餅圖的大小來(lái)設(shè)置比例,繪制文字的位置需要計(jì)算文字的寬度
思路清晰后就擼起袖子加油干
知識(shí)點(diǎn)
我們先來(lái)了解一個(gè)概念,我們?cè)趐aint畫(huà)扇形的時(shí)候,對(duì)應(yīng)的度數(shù)是在哪個(gè)位置呢?

看到圖后應(yīng)該明白了吧
繪制餅圖
我們先來(lái)看看他的參數(shù),很明顯,左、上、右、下參數(shù)形成一個(gè)面板,startAngle 為起始的角度,sweepAngle 為從起始角度開(kāi)始繪制多少度,useCenter為是否連接到圓心,paint為畫(huà)筆
public void drawArc(float left, float top, float right, float bottom, float startAngle,
float sweepAngle, boolean useCenter, @NonNull Paint paint) {
super.drawArc(left, top, right, bottom, startAngle, sweepAngle, useCenter, paint);
}
我們以當(dāng)前控件的width、height為面板來(lái)畫(huà)一個(gè)圓形的餅圖
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawArc(0f, 0f, width, height, 0f, 360f, true, paintRed)
}
哇塞,好丑哦,結(jié)果顯示的是一個(gè)橢圓,如果要繪制一個(gè)圓形的餅圖,我們必須得保證left=top=right=bottom

設(shè)置餅圖居中
/**
* view的寬度
*/
var width: Float = 0f
/**
* view的高度
*/
var height: Float = 0f
/**
* drawArc距離左邊的距離
*/
var left: Float = 0f
/**
* drawArc距離上邊的距離
*/
var top: Float = 0f
/**
* drawArc距離右邊的距離
*/
var right: Float = 0f
/**
* drawArc距離下邊的距離
*/
var bottom: Float = 0f
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawArc(left, top, right, bottom, 0f, 360f, true, paint)
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
setBackgroundColor(resources.getColor(R.color.black))
width = w.toFloat()
height = h.toFloat()
left = width / 4f
top = width / 4f
right = width - left
bottom = width - top
}
完美居中

接下來(lái),我們要把上面從0度到360度多分幾個(gè)步驟來(lái)繪制
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
...
canvas.drawArc(left, top, right, bottom, 0f, 20f, true, paintPuple)
canvas.drawArc(left, top, right, bottom, 20f, 10f, true, paintGray)
canvas.drawArc(left, top, right, bottom, 30f, 40f, true, paintGreen)
canvas.drawArc(left, top, right, bottom, 70f, 110f, true, paintBlue)
canvas.drawArc(left, top, right, bottom, 180f, 110f, true, paintRed)
canvas.drawArc(left, top, right, bottom, 290f, 70f, true, paintYellow)
}
還不錯(cuò)
上圖的度數(shù)是寫(xiě)死的,現(xiàn)在我們來(lái)把他寫(xiě)活
提供一個(gè)設(shè)置個(gè)數(shù)的集合,比如農(nóng)名伯伯賣(mài)水果,梨子賣(mài)了10個(gè),香蕉賣(mài)了3個(gè),蘋(píng)果賣(mài)了7個(gè),那么這個(gè)個(gè)數(shù)的集合為pieList=(10,3,7)。
因?yàn)轱瀳D是根據(jù)角度來(lái)繪制的,我們必須將這個(gè)個(gè)數(shù)集合換算成角度集合,換算的過(guò)程中我們需要知道每一種水果所占總水果的比例,然后通過(guò)這個(gè)比例去乘上360度,就知道每一種水果所占的度數(shù)。
梨子的占比為10/(10+3+7)=1/2,可得梨子占餅圖的度數(shù)為1/2*360=180度,按照這種方式計(jì)算,香蕉和蘋(píng)果占餅圖的度數(shù)分別為54度和126度,那么,餅圖的分布也就出來(lái)了
現(xiàn)在,我們來(lái)定義一個(gè)個(gè)數(shù)集合,計(jì)算出比例的集合和度數(shù)的集合,下面是比例的集合,度數(shù)的集合我們?cè)诶L制的時(shí)候再去計(jì)算
/**
* 個(gè)人分類(lèi)集合
*/
var pieList = arrayListOf(10f,3f,7f)
/**
* 餅圖所占的比例
*/
var scaleList = arrayListOf<Float>()
/**
* 個(gè)數(shù)分類(lèi)的總量
*/
var total: Float = 0f
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
//計(jì)算個(gè)數(shù)的總和
total = pieList.sum()
//存儲(chǔ)比例值
for (a in pieList) {
scaleList.add(a.div(total))
}
}
比例集合拿到了,接下來(lái),我們?nèi)パh(huán)這個(gè)比例值,然后將比例值乘上360度,計(jì)算出角度值,供drawArc的sweepAngle使用,但是,我們還缺少一個(gè)startAngle起始角度,我們可以定義一個(gè)起始角度為0度,然后每次根據(jù)計(jì)算出的角度值sweepAngle去累加起始度數(shù),用代碼來(lái)實(shí)現(xiàn)下
/**
* 記錄當(dāng)前畫(huà)餅圖的度數(shù)
*/
var currentDegree: Float = 0f
/**
* 累加餅圖的度數(shù)作為下一個(gè)繪制的起始度數(shù)
*/
var srctorDegree: Float = 0f
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
for (scale in scaleList) {
val paint = Paint()
paint.strokeWidth = dip(10.0f).toFloat()
paint.isAntiAlias = true
//定義一個(gè)隨機(jī)生成的顏色數(shù),來(lái)區(qū)分不同的扇形區(qū)域
val hex = "#" + Integer.toHexString((-16777216 * Math.random()).toInt())
paint.color = Color.parseColor(hex)
//角度數(shù)
srctorDegree = scale * 360
canvas.drawArc(left, top, right, bottom, currentDegree, srctorDegree, true, paint)
//累加角度
currentDegree += srctorDegree
}
}
ok,現(xiàn)在我們可以隨機(jī)的去定義個(gè)數(shù)來(lái)生成占比的餅圖了

繪制折線(xiàn)
接下來(lái),我們來(lái)繪制折線(xiàn),折線(xiàn)的起點(diǎn)是每個(gè)扇形弧上的一半,path的arcTo方法也可以繪制圓,且方法參數(shù)使用也是一樣,我們可以讓arcTo跟著canvas.drawArc一塊畫(huà),arcTo的startAngle起始角度為canvas.drawArc起始角度加上sweepAngle度數(shù)的一半,這樣,就定位到了弧邊的一半,arcTo的sweepAngle為0就行了,我們只定位,不繪制
... canvas.drawArc(left, top, right, bottom, currentDegree, srctorDegree, true, paint) val path = Path() path.arcTo(left, top, right, bottom, currentDegree + srctorDegree / 2, 0f, false) ...
現(xiàn)在,path的位置定位到弧邊的一半了,接下來(lái),我們要知道當(dāng)前path的坐標(biāo)然后根據(jù)坐標(biāo)去繪制折線(xiàn),
val bounds = RectF() //將path當(dāng)前的坐標(biāo)賦值給bounds path.computeBounds(bounds, true)
現(xiàn)在拿到坐標(biāo)了,我們?cè)賮?lái)看看效果圖,折線(xiàn)和文字呈四個(gè)方向,我們不如把餅圖分成四個(gè)區(qū)域,以圓心為坐標(biāo)軸原點(diǎn),切分四個(gè)象限:
- 第一象限:折線(xiàn)為右上,文字在折線(xiàn)右邊
- 第二象限:折線(xiàn)為左上,文字在折線(xiàn)左邊
- 第三象限:折線(xiàn)為左下,文字在折線(xiàn)左邊
- 第四象限:折線(xiàn)為右下,文字在折線(xiàn)右邊
那么,接下來(lái)就是如何判斷當(dāng)前起始點(diǎn)在哪個(gè)象限了,先以第一象限為例,如果當(dāng)前的坐標(biāo)大于餅圖橫軸方向一半,并且小于餅圖縱軸方向的一半,那么就是第一象限,其他依次類(lèi)推
/**
* 橫線(xiàn)的長(zhǎng)度
*/
var lineae: Int = 30
/**
* 斜線(xiàn)的長(zhǎng)度
*/
var slantLine: Int = 30
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
//計(jì)算橫線(xiàn)的比例
lineae = (width / 30f).toInt()
//計(jì)算斜線(xiàn)的比例
slantLine = (width / 40f).toInt()
}
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
for (scale in scaleList) {
...
val path = Path()
path.arcTo(left, top, right, bottom, currentDegree + srctorDegree / 2, 0f, false)
val bounds = RectF()
path.computeBounds(bounds, true)
//第一象限
if (bounds.left >= width / 2 && bounds.top <= width / 2) {
path.lineTo(bounds.left + lineae, bounds.top)
path.lineTo(bounds.left + lineae + slantLine, bounds.top - slantLine)
canvas.drawPath(path, paintLine)
//第二象限
} else if (bounds.left <= width / 2 && bounds.top <= width / 2) {
path.lineTo(bounds.left - lineae, bounds.top)
path.lineTo(bounds.left - lineae - slantLine, bounds.top - slantLine)
canvas.drawPath(path, paintLine)
//第三象限
} else if (bounds.left <= width / 2 && bounds.top >= width / 2) {
path.lineTo(bounds.left - lineae, bounds.top)
path.lineTo(bounds.left - lineae - slantLine, bounds.top + slantLine)
canvas.drawPath(path, paintLine)
//第四象限
} else {
path.lineTo(bounds.left + lineae, bounds.top)
path.lineTo(bounds.left + lineae + slantLine, bounds.top + slantLine)
canvas.drawPath(path, paintLine)
}
}
...
}
哎呀,出來(lái)了
繪制文字
接下來(lái)就是繪制文字了,第一、四象限還好,文字可以在折線(xiàn)后面跟著畫(huà),但是二、三象限的文字就不允許了,我們必須往前移動(dòng)文字寬度的距離才能完美銜接到折線(xiàn)上,所以,我們來(lái)定義一個(gè)計(jì)算文字的方法
/** * 獲取文字的寬度 */ private fun getStringWidth(str: String): Float = paintLine.measureText(str)
文字是會(huì)隨著餅圖的大小進(jìn)行改變的,所以設(shè)置文字大小的比例
paintLine.textSize = dip(width / 100).toFloat()
接下來(lái)就開(kāi)始繪制文字吧
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
...
//獲取當(dāng)前的百分比文字
val textStr = String.format("%.2f%%", scale * 100)
//獲取文字的寬度
val textWidth = getStringWidth(textStr)
//第一象限
if (bounds.left >= width / 2 && bounds.top <= width / 2) {
...
canvas.drawText(textStr, bounds.left + lineae + slantLine, bounds.top - slantLine, paintText)
...
//第二象限
} else if (bounds.left <= width / 2 && bounds.top <= width / 2) {
...
canvas.drawText(textStr, bounds.left - lineae - slantLine - textWidth, bounds.top - slantLine, paintText)
...
//第三象限
} else if (bounds.left <= width / 2 && bounds.top >= width / 2) {
...
canvas.drawText(textStr, bounds.left - lineae - slantLine - textWidth, bounds.top + lineae, paintText)
...
//第四象限
} else {
...
canvas.drawText(textStr, bounds.left + lineae + slantLine, bounds.top + slantLine, paintText)
...
}
}
嗯,還不錯(cuò),

然后我們?cè)倏纯葱Ч麍D,餅圖中間還有一塊與背景色一樣的黑圓,這不跟簡(jiǎn)單了嘛
//定義中間黑圓的畫(huà)筆
paintCicle.color = resources.getColor(R.color.black)
paintCicle.isAntiAlias = true
paintCicle.style = Paint.Style.FILL
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
...
//在循環(huán)結(jié)束餅圖的時(shí)候,以餅圖的原點(diǎn)為中心畫(huà)圓
canvas.drawCircle(width / 2, width / 2, width / 8, paintCicle)
}

然后我們暴露一個(gè)方法,提供給Activity去調(diào)用
/**
* 設(shè)置扇形參數(shù)
*/
fun setPieData(a: ArrayList<Float>) {
pieList.clear()
pieList.addAll(a)
invalidate()
}
那么,Activity就可以這么去調(diào)用了
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_second)
pie1.setPieData(arrayListOf(1f,10f,15f,9f,15f))
pie2.setPieData(arrayListOf(3f,8f,15f,7f,9f))
pie3.setPieData(arrayListOf(9f,3f,7f,3f,4f,2f,1f))
}

總結(jié)
以上就是這篇文章的全部?jī)?nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,如果有疑問(wèn)大家可以留言交流,謝謝大家對(duì)腳本之家的支持。
相關(guān)文章
Android 開(kāi)發(fā)中使用Linux Shell實(shí)例詳解
這篇文章主要介紹了Android 開(kāi)發(fā)中使用Linux Shell實(shí)例詳解的相關(guān)資料,需要的朋友可以參考下2017-03-03
Android自定義控件實(shí)現(xiàn)手勢(shì)密碼
這篇文章主要介紹了Android自定義控件實(shí)現(xiàn)手勢(shì)密碼的相關(guān)資料,實(shí)現(xiàn)手勢(shì)解鎖功能,感興趣的小伙伴們可以參考一下2016-07-07
Android自定義表格控件滿(mǎn)足人們對(duì)視覺(jué)的需求
隨著人們對(duì)視覺(jué)的需求,基本組件已無(wú)法滿(mǎn)足人們求新求異的要求,于是我們常常會(huì)自定義組件,用來(lái)實(shí)現(xiàn)更美觀的UI界面,接下來(lái)將介紹Android如何自定義表格控件,感興趣的朋友可以了解下,或許對(duì)你學(xué)習(xí)自定義控件有所幫助2013-02-02
如何使用Flutter實(shí)現(xiàn)手寫(xiě)簽名效果
Flutter插件提供了用于繪制平滑簽名的簽名板,下面這篇文章主要給大家介紹了關(guān)于如何使用Flutter實(shí)現(xiàn)手寫(xiě)簽名效果的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-12-12
Android開(kāi)發(fā)之關(guān)閉和打開(kāi)Speaker(揚(yáng)聲器)的方法
這篇文章主要介紹了Android開(kāi)發(fā)之關(guān)閉和打開(kāi)Speaker(揚(yáng)聲器)的方法,結(jié)合實(shí)例形式簡(jiǎn)單分析了Android揚(yáng)聲器的操作技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2016-03-03
Android 開(kāi)發(fā)系統(tǒng)自帶語(yǔ)音模塊應(yīng)用
本篇文章 主要介紹 Android 開(kāi)發(fā)自帶語(yǔ)音模塊實(shí)例,在開(kāi)發(fā)Android系統(tǒng)中會(huì)用到系統(tǒng)語(yǔ)音搜索模塊,這里給大家一個(gè)參考實(shí)例2016-07-07
Android實(shí)現(xiàn)在屏幕上移動(dòng)圖片的方法
這篇文章主要介紹了Android實(shí)現(xiàn)在屏幕上移動(dòng)圖片的方法,實(shí)例分析了Android操作圖片的相關(guān)技巧,需要的朋友可以參考下2015-06-06
Android 顯示和隱藏軟鍵盤(pán)的方法(手動(dòng))
在Android開(kāi)發(fā)中,經(jīng)常會(huì)有一個(gè)需求,做完某項(xiàng)操作后,隱藏鍵盤(pán),也即讓Android中的軟鍵盤(pán)不顯示。今天,和大家分享如何利用代碼來(lái)實(shí)現(xiàn)對(duì)Android的軟件盤(pán)的隱藏、顯示的操作2016-01-01
Android菜單(動(dòng)畫(huà)菜單、360波紋菜單)
本文主要介紹了Android菜單常用集合:FragmentTabHost系統(tǒng)菜單、上移式菜單、360波紋菜單、展開(kāi)式菜單。詳解注釋?zhuān)芍苯幽脕?lái)用。下面跟著小編一起來(lái)看下吧2017-03-03
Android面試Intent采用了什么設(shè)計(jì)模式解析
這篇文章主要為大家介紹了Android面試Intent采用了什么設(shè)計(jì)模式解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-03-03

