自定义圆环(扇形)绘制

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

以上是关于自定义圆环(扇形)绘制的主要内容,如果未能解决你的问题,请参考以下文章

Android 自定义View:绘制轮盘扇形区并加入扇形区点击事件

Android轮盘控件-自定义

自定义UI 绘制饼图

自定义UI 绘制饼图

自定义UI 绘制饼图

Unity3D之Mesh绘制扇形扇面环形