自定义圆环(扇形)绘制
Posted 夜尽天明89
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了自定义圆环(扇形)绘制相关的知识,希望对你有一定的参考价值。
这个圆环是静态的,没有自动增加的动画
绘制并不复杂,有些细节点容易搞错,这里写出来,算是做个笔记。
先放出源码,细节点,后面会说明。建议看后面的说明。最后,会进行扩展,绘制扇形
需求/功能说明:
1、假设一个班级里有 X 个人,班级里的学习有的篮球、有的足球、有的书法、有的绘画,还有的,什么都没学
2、圆环绘制的起点,是圆环顶部,逆时针依次绘制
效果图:
代码:
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.RectF
import android.text.TextPaint
import android.util.AttributeSet
import android.util.Log
import android.view.View
class MyView : View
private var mContext: Context? = null
private var vW: Int = 0
private var vH: Int = 0
//半径
private var radius: Float = 0f
//背景圆环画笔
private var bgCirclePaint: Paint? = null
//圆环画笔
private var lanQiuCirclePaint: Paint? = null
private var zuQiuCirclePaint: Paint? = null
private var shuFaCirclePaint: Paint? = null
private var huiHuaCirclePaint: Paint? = null
//文字画笔1
private var textPaint_1: TextPaint? = null
//文字画笔2
private var textPaint_2: TextPaint? = null
//文字1的偏移量
private var textOffset_1: Float = 0f
//文字2的偏移量
private var textOffset_2: Float = 0f
//掌握对应的矩形,用于辅助绘制 掌握 圆环
private var rectF: RectF? = null
constructor(context: Context?) : super(context)
mContext = context
init()
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
mContext = context
init()
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
)
mContext = context
init()
private fun init()
//半径。根据布局文件,控件的宽高是 200dp。20是圆环画笔宽度。应该10就够,我这里往回缩了一点
radius = UiUtils.dip2px(mContext, 100f) - 20f
//背景圆画笔
bgCirclePaint = Paint(Paint.ANTI_ALIAS_FLAG)
bgCirclePaint?.style = Paint.Style.STROKE
bgCirclePaint?.color = Color.parseColor("#888888")
bgCirclePaint?.strokeWidth = 20f
//篮球圆环画笔
lanQiuCirclePaint = Paint(Paint.ANTI_ALIAS_FLAG)
lanQiuCirclePaint?.style = Paint.Style.STROKE
lanQiuCirclePaint?.color = Color.parseColor("#ff0000")
lanQiuCirclePaint?.strokeWidth = 20f
//足球圆环画笔
zuQiuCirclePaint = Paint(Paint.ANTI_ALIAS_FLAG)
zuQiuCirclePaint?.style = Paint.Style.STROKE
zuQiuCirclePaint?.color = Color.parseColor("#00ff00")
zuQiuCirclePaint?.strokeWidth = 20f
//书法圆环画笔
shuFaCirclePaint = Paint(Paint.ANTI_ALIAS_FLAG)
shuFaCirclePaint?.style = Paint.Style.STROKE
shuFaCirclePaint?.color = Color.parseColor("#0000ff")
shuFaCirclePaint?.strokeWidth = 20f
//绘画画笔。不同于上面,另外一种写法:把一个画笔传进去
huiHuaCirclePaint = Paint(shuFaCirclePaint)
huiHuaCirclePaint?.color = Color.parseColor("#fe7c2b")
//文字画笔1
textPaint_1 = TextPaint(Paint.ANTI_ALIAS_FLAG)
textPaint_1?.setTextSize(UiUtils.dip2px(mContext!!, 13f).toFloat());
textPaint_1?.setColor(Color.parseColor("#888888"));
textPaint_1?.setTextAlign(Paint.Align.CENTER);
textOffset_1 = (textPaint_1!!.ascent() + textPaint_1!!.descent()) / 2
//文字画笔2
textPaint_2 = TextPaint(Paint.ANTI_ALIAS_FLAG)
textPaint_2?.setTextSize(UiUtils.dip2px(mContext!!, 12f).toFloat());
textPaint_2?.setColor(Color.parseColor("#252525"));
textPaint_2?.setTextAlign(Paint.Align.CENTER);
textOffset_2 = (textPaint_2!!.ascent() + textPaint_2!!.descent()) / 2
//总人数
private var mTotalNum: Int = 0
//篮球个数
private var mLanQiuNum: Int = 0
//足球个数
private var mZuQiuNum: Int = 0
//书法个数
private var mShuFaNum: Int = 0
//绘画
private var mHuiHuaNum: Int = 0
//没有学习的个数
private var mMeiXueNum: Int = 0
//打篮球的人数对应的角度
private var lanQiuAngle: Float = 0f
//角度开始位置
private var lanQiuAngleStart: Float = 0f
//角度结束位置
private var lanQiuAngleEnd: Float = 0f
//踢足球的人数对应的角度
private var zuQiuAngle: Float = 0f
private var zuQiuAngleStart: Float = 0f
private var zuQiuAngleEnd: Float = 0f
//书法的人数占的角度
private var shuFaAngle: Float = 0f
private var shuFaAngleStart: Float = 0f
private var shuFaAngleEnd: Float = 0f
//绘画的人数占的角度
private var huiHuaAngle: Float = 0f
private var huiHuaAngleStart: Float = 0f
private var huiHuaAngleEnd: Float = 0f
fun setData(
totalNum: Int,
lanQiuNum: Int,
zuQiuNum: Int,
shuFaNum: Int,
huiHuaNum: Int,
meiXueNum: Int
)
mTotalNum = totalNum
mLanQiuNum = lanQiuNum
mZuQiuNum = zuQiuNum
mShuFaNum = shuFaNum
mHuiHuaNum = huiHuaNum
mMeiXueNum = meiXueNum
//数字是否有效
val numIsEffective: Boolean =
(mTotalNum != 0) && (mTotalNum == mLanQiuNum + mZuQiuNum + mShuFaNum + mHuiHuaNum + mMeiXueNum)
if (numIsEffective)
//数据有效,开始下面的计算
val fTotalNum: Float = mTotalNum.toFloat()
//篮球
lanQiuAngle = if (mLanQiuNum != 0)
//数不为0
360 * (mLanQiuNum / fTotalNum)
else
0f
//逆时针画,最后用到 onDraw 里,是负度数。(符号表示方向)
lanQiuAngleStart = 0f
lanQiuAngleEnd = lanQiuAngleStart - lanQiuAngle
//足球
zuQiuAngle = if (mZuQiuNum != 0)
//数不为0
360 * (mZuQiuNum / fTotalNum)
else
0f
zuQiuAngleStart = lanQiuAngleEnd
zuQiuAngleEnd = zuQiuAngleStart - zuQiuAngle
//书法
shuFaAngle = if (mShuFaNum != 0)
//数不为0
360 * (mShuFaNum / fTotalNum)
else
0f
shuFaAngleStart = zuQiuAngleEnd
shuFaAngleEnd = shuFaAngleStart - shuFaAngle
//绘画
huiHuaAngle = if (mHuiHuaNum != 0)
//数不为0
360 * (mHuiHuaNum / fTotalNum)
else
0f
huiHuaAngleStart = shuFaAngleEnd
huiHuaAngleEnd = huiHuaAngleStart - huiHuaAngle
//都不学的不用管,如果有剩余角度,就是 都不学 的
Log.e("篮球:", "度数:$lanQiuAngle ;开始:$lanQiuAngleStart ;结束:$lanQiuAngleEnd")
Log.e("足球:", "度数:$zuQiuAngle ;开始:$zuQiuAngleStart ;结束:$zuQiuAngleEnd")
Log.e("书法:", "度数:$shuFaAngle ;开始:$shuFaAngleStart ;结束:$shuFaAngleEnd")
Log.e("绘画:", "度数:$huiHuaAngle ;开始:$huiHuaAngleStart ;结束:$huiHuaAngleEnd")
rectF = RectF(-radius, -radius, radius, radius)
invalidate()
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int)
super.onSizeChanged(w, h, oldw, oldh)
vW = w
vH = h
rectF = RectF(-radius, -radius, radius, radius)
override fun onDraw(canvas: Canvas?)
super.onDraw(canvas)
//保存下画布
canvas?.save();
//移动画布坐标原点到控件中心
canvas?.translate(vW / 2f, vH / 2f);
//旋转画布。使得正上方向为0度。顺时针为正度数方向
canvas?.rotate(-90f)//canvas?.rotate(270f)
//绘制一个背景圆
canvas?.drawCircle(0f, 0f, radius, bgCirclePaint!!)
if (lanQiuAngle > 0f)
//表示有度数。有度数,才绘制
canvas?.drawArc(rectF!!, lanQiuAngleStart, -lanQiuAngle, false, lanQiuCirclePaint!!)
if (zuQiuAngle > 0f)
canvas?.drawArc(rectF!!, zuQiuAngleStart, -zuQiuAngle, false, zuQiuCirclePaint!!)
if (shuFaAngle != 0f)
canvas?.drawArc(rectF!!, shuFaAngleStart, -shuFaAngle, false, shuFaCirclePaint!!)
if (huiHuaAngle > 0f)
canvas?.drawArc(rectF!!, huiHuaAngleStart, -huiHuaAngle, false, huiHuaCirclePaint!!)
// 释放画布
canvas?.restore();
//释放完成后,画布角度恢复正常(正右方向为0度,顺时针为正度数)、坐标原点为控件左上角
//保存下画布
canvas?.save();
//移动画布坐标原点到控件中心
canvas?.translate(vW / 2f, vH / 2f);
canvas?.drawText(
"比例",
0f,
-UiUtils.dip2px(mContext, 8f) - textOffset_1,
textPaint_1!!
)
canvas?.drawText(
"$mLanQiuNum + mZuQiuNum + mShuFaNum + mHuiHuaNum/$mTotalNum",
0f,
UiUtils.dip2px(mContext, 8f) - textOffset_2,
textPaint_2!!
)
// 释放画布
canvas?.restore();
使用:
<...MyView
android:id="@+id/my_view"
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_marginTop="30dp"
android:background="#ffffff"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
//总数
var totalNum: Int = 8000
//篮球
var lanQiuNum: Int = 1000
//足球
var zuQiuNum: Int = 2000
//书法
var shuFaNum: Int = 3000
//绘画
var huiHuaNum: Int = 1000
//没学
var meiXueNum: Int = 1000
//设置数据
my_view?.setData(
totalNum = totalNum,
lanQiuNum = lanQiuNum,
zuQiuNum = zuQiuNum,
shuFaNum = shuFaNum,
huiHuaNum = huiHuaNum,
meiXueNum = meiXueNum
)
----------以上是源码------------------以上是源码------------------------以上是源码-----------------------
关于文字的绘制,没什么好说的。有兴趣的,去看一个大神写的博客
接下来,开始讲解。(自认为 看完下面讲,以后类似这种圆环,都能自己画)
牢记:度数的正负,是表示方向。正度数:顺时针;负度数,逆时针。记住这句话,再往下看
1、移动画布原点。这个操作可有可无,根据自己的需求来。我是进行了移动。
在此之前,了解下坐标系:一个控件,在绘制的时候,原点默认是左上角,则:这个时候,控件的中心坐标,就是 (vW/2,vH/2) 。vW、vH 分别是控件的宽高,下同
画布坐标系默认是这样的:
现在,我对坐标系进行移动。(save、restore是配套使用的,千万注意。关于这2个方法的理解,我也说不好,请自行调研学习)
释放完成(restore)后,画布角度恢复正常(正右方向为0度,顺时针为正度数)、坐标原点为控件左上角
canvas?.save()
canvas?.translate(vW / 2f, vH / 2f)//移动画布坐标原点到控件中心
.....//操作
canvas?.restore()
移动完成后,控件的坐标系,就变成了
我这里移动,是为了绘制方便,实际中是否需要移动,根据项目、功能需要自行决定
2、确定角度。绘制圆环(或者圆),肯定要有个起点,然后沿着自己想要的方向进行绘制。要确定自己想要的方向,就一定要知道系统的默认方向。(通过上面的坐标系,也应该猜出来了,水平向右是正方向,顺时针为正角度,验证下)(关于圆环的绘制,后面会讲)
canvas?.save();
//移动画布坐标原点到控件中心
canvas?.translate(vW / 2f, vH / 2f);
//绘制一个背景圆
canvas?.drawCircle(0f, 0f, radius, bgCirclePaint!!)
//绘制一个圆环。起点是0度,扫过45度,正方向
canvas?.drawArc(rectF!!, 0f, 45f, false, 画笔)
canvas?.restore();
绘制了一个圆环:起点是0度位置,方向是正方向(角度为正)。由此可知:系统默认是0度起点是 水平向右,默认正方向,是顺时针。
3、确定线段宽度及线段中心。因为绘制圆环的画笔比较粗(20像素,不同的项目、功能,可能会更粗)
private var linePaint_1: Paint? = null
private var linePaint_2: Paint? = null
linePaint_1 = Paint(Paint.ANTI_ALIAS_FLAG)
linePaint_1?.style = Paint.Style.STROKE
linePaint_1?.color = Color.parseColor("#55ff0000")
linePaint_1?.strokeWidth = 20f //画笔的宽度(粗细)是 20像素
linePaint_2 = Paint(Paint.ANTI_ALIAS_FLAG)
linePaint_2?.style = Paint.Style.STROKE
linePaint_2?.color = Color.parseColor("#000000")
linePaint_2?.strokeWidth = 1f //画笔的宽度(粗细)是1像素
canvas?.save();
canvas?.translate(vW / 2f, vH / 2f)
//过控件中心,在竖直方向,画一条线。粗bi
canvas?.drawLine(0f,-vH/2f,0f,vH/2f,linePaint_1!!)
//过控件中心,在竖直方向,画一条线。细笔
canvas?.drawLine(0f,-vH/2f,0f,vH/2f,linePaint_2!!)
//偏离竖直中心线 10像素(粗笔宽度的一半),画一条线
canvas?.drawLine(10f,-vH/2f,10f,vH/2f,linePaint_2!!)
canvas?.restore()
由此可知:在指定位置画线(曲线同理),画笔的中心在指定位置上。或者说:指定位置的点,平分了画笔的宽度
4、圆环绘制方法(代码)
canvas?.drawArc(rectF!!, 0f, 45f, false, lanQiuCirclePaint!!)
这里的5个参数,必须知道各自的含义及用法
第一参数:矩形。指定一个矩形,矩形的边和圆环(画笔粗细的中心)相切(数学上的知识点)
第二个参数:起点。即:从哪个度数开始绘制
第三个参数:扫过的角度。这里千万、千万、千万注意,是
扫过的角度,不是终点所在角度
第四个参数:useCenter(是否用中点)。下面会详细说
第五个参数:画笔
接下来依次说明上面的前4个参数(第五个画笔没必要说明)
1、说明第一个参数,我先绘制一个圆环,范围是 -30度 到 +30度:圆环线段的粗细是20像素,矩形我用1像素的边线
//半径。根据布局文件,控件的宽高是 200dp。20是圆环画笔宽度。应该10就够,我这里往回缩了一点
radius = UiUtils.dip2px(mContext, 100f) - 20f
----------
canvas?.save();
//移动画布坐标原点到控件中心
canvas?.translate(vW / 2f, vH / 2f);
//绘制一个背景圆
canvas?.drawCircle(0f, 0f, radius, bgCirclePaint!!)
//起点是 -30度,扫过 +60 度的角度
canvas?.drawArc(rectF!!, -30f, 60f, false, lanQiuCirclePaint!!)
canvas?.drawLine(-radius, -radius, radius, -radius, linePaint_2!!)
canvas?.drawLine(-radius, -radius, -radius, radius, linePaint_2!!)
canvas?.drawLine(radius, -radius, radius, radius, linePaint_2!!)
canvas?.drawLine(-radius, radius, radius, radius, linePaint_2!!)
canvas?.restore();
注意下最右边,矩形的边,和圆环中心线,是相切的(相切位置,左右各10像素)。当然,这里特别说明是圆环中心线,是因为圆环比较粗。如果绘制了画笔宽度是1像素的圆环,就是圆环和矩形相切了。
2、说明第二个参数:这个其实没什么额外的说明,就是指定了,你要在哪个角度开始绘制
3、说明第三个参数:扫过的角度。这个是易错点。为了说明这个问题,我需要绘制2个首尾相接的圆环。
设:总数是5000。第一个圆环,表示1000;第二个表示 1500。总和是 2500(占总数一半)
1000 对应的度数占比是 360*(1000/5000)=72;1500 对应的度数占比是 360*(1500/5000) = 108。即:第一个圆环,要扫过 72度;第二个圆环,要扫过 108度
canvas?.drawArc(rectF!!, 0f, 72f, false, 画笔)
canvas?.drawArc(rectF!!, 72f, 108f, false, 画笔) //因为是首尾相接,第二个圆环的开始度数,就是第一个的结束度数
4、第四个参数说明:useCenter:是否使用中点。是一个 boolean 值,分别来看下,就知道差别了。以上的圆环展示图,都是 false。现在,来看下 true 的情况
canvas?.drawArc(rectF!!, 0f, 72f, true, 画笔)
好理解吧。useCenter 为true 就是表示 圆环的起点、终点 和中心连起来
知道了上面的几点,来分析下需求中的:圆环绘制的起点,是圆环顶部,逆时针依次绘制。
1、绘制的起点,要从顶部开始,就是竖直方向上的上方;2、绘制是逆时针的,也就是负度数
现在已经知道了,系统默认的 0度对应右边、顺时针为正度数。如果要实现 从顶部开始,逆时针绘制。那就是,起点是 -90度,后续的度数,都为 负数,且每次计算,都要注意 -90度。
能不能让顶部作为起点呢?
可以的,既然可以平移坐标系,那么,也是可以旋转画布的。把画布旋转个 -90度 或者 270 度,就行了。
canvas?.save();
//移动画布坐标原点到控件中心
canvas?.translate(vW / 2f, vH / 2f);
//旋转画布。使得正上方向为0度。顺时针为正度数方向
canvas?.rotate(-90f)//canvas?.rotate(270f)
canvas?.drawCircle(0f, 0f, radius, bgCirclePaint!!)
canvas?.drawArc(rectF!!, 0f, -72f, true, lanQiuCirclePaint!!)
canvas?.restore();
简单吧。接下来,只要注意不同圆环之间的首尾相接就行。前一个的终点度数,就是下一个的起点度数
绘制扇形
假设现在需求变了,要绘制扇形了。怎么办?
好办,扇形和圆环的区别,就是是否和中点有连线。如果中点和圆环的起点、终点链接起来了,不就是扇形么?
动手写下代码:
canvas?.save();
//移动画布坐标原点到控件中心
canvas?.translate(vW / 2f, vH / 2f);
//旋转画布。使得正上方向为0度。顺时针为正度数方向
canvas?.rotate(-90f)//canvas?.rotate(270f)
//绘制一个背景圆
canvas?.drawCircle(0f, 0f, radius, bgCirclePaint!!)
canvas?.drawArc(rectF!!, 0f, -72f, true, lanQiuCirclePaint!!)
canvas?.drawArc(rectF!!, -72f, -108f, true, zuQiuCirclePaint!!)
canvas?.restore();
额,形状是扇形了,但是是空心的。不符合要求,需要实心才行。
看下画笔:
//篮球圆环画笔
lanQiuCirclePaint = Paint(Paint.ANTI_ALIAS_FLAG)
lanQiuCirclePaint?.style = Paint.Style.STROKE
lanQiuCirclePaint?.color = Color.parseColor("#ff0000")
lanQiuCirclePaint?.strokeWidth = 20f
注意这句话:
lanQiuCirclePaint?.style = Paint.Style.STROKE
设置画笔风格、样式的。画笔有3种样式
1.Paint.Style.STROKE:描边
2.Paint.Style.FILL_AND_STROKE:描边并填充
3.Paint.Style.FILL:填充
既然要实心,那就改成
lanQiuCirclePaint?.style = Paint.Style.FILL
看出问题了吗?仅仅是填充了内部,但是没有描边。正确的做法是:
lanQiuCirclePaint?.style = Paint.Style.FILL_AND_STROKE
以上是关于自定义圆环(扇形)绘制的主要内容,如果未能解决你的问题,请参考以下文章