用kotlin來實現一個餅圖

codelang發表於2017-12-08

前言

程式碼不難,所以打算用kotlin來實現,增加熟練度

先看看做的是什麼

用kotlin來實現一個餅圖

看完圖,我們來整理下思路

  • 餅圖居中,每塊區域都是一個扇形,需要canvas.drawArc根據角度來繪製
  • 需要path.arcTo定位到扇形弧度的一半來繪製折線的起點
  • 通過canvas.drawPath繪製折線,折線的長度根據餅圖大小來設定比例
  • 通過canvas.drawText繪製文字,文字的大小根據餅圖的大小來設定比例,繪製文字的位置需要計算文字的寬度

思路清晰後就擼起袖子加油幹

知識點

我們先來了解一個概念,我們在paint畫扇形的時候,對應的度數是在哪個位置呢?

用kotlin來實現一個餅圖

看到圖後應該明白了吧

繪製餅圖

我們先來看看他的引數,很明顯,左、上、右、下引數形成一個皮膚,startAngle 為起始的角度,sweepAngle 為從起始角度開始繪製多少度,useCenter為是否連線到圓心,paint為畫筆

 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);
    }
複製程式碼

我們以當前控制元件的width、height為皮膚來畫一個圓形的餅圖

    @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        canvas.drawArc(0f, 0f, width, height, 0f, 360f, true, paintRed)
    }
複製程式碼

哇塞,好醜哦,結果顯示的是一個橢圓,如果要繪製一個圓形的餅圖,我們必須得保證left=top=right=bottom

用kotlin來實現一個餅圖

設定餅圖居中

    /**
     * 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   
        
     }
複製程式碼

完美居中

用kotlin來實現一個餅圖

接下來,我們要把上面從0度到360度多分幾個步驟來繪製

    @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)
    }
複製程式碼

還不錯

用kotlin來實現一個餅圖

上圖的度數是寫死的,現在我們來把他寫活

提供一個設定個數的集合,比如農名伯伯賣水果,梨子賣了10個,香蕉賣了3個,蘋果賣了7個,那麼這個個數的集合為pieList=(10,3,7)。 因為餅圖是根據角度來繪製的,我們必須將這個個數集合換算成角度集合,換算的過程中我們需要知道每一種水果所佔總水果的比例,然後通過這個比例去乘上360度,就知道每一種水果所佔的度數。 梨子的佔比為10/(10+3+7)=1/2,可得梨子佔餅圖的度數為1/2*360=180度,按照這種方式計算,香蕉和蘋果佔餅圖的度數分別為54度和126度,那麼,餅圖的分佈也就出來了

現在,我們來定義一個個數集合,計算出比例的集合和度數的集合,下面是比例的集合,度數的集合我們在繪製的時候再去計算

    /**
     * 個人分類集合
     */
    var pieList = arrayListOf(10f,3f,7f)

    /**
     * 餅圖所佔的比例
     */
    var scaleList = arrayListOf<Float>()
    /**
     * 個數分類的總量
     */
    var total: Float = 0f
    
    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        //計算個數的總和
        total = pieList.sum()
        //儲存比例值
        for (a in pieList) {
            scaleList.add(a.div(total))
        }
    }

複製程式碼

比例集合拿到了,接下來,我們去迴圈這個比例值,然後將比例值乘上360度,計算出角度值,供drawArc的sweepAngle使用,但是,我們還缺少一個startAngle起始角度, 我們可以定義一個起始角度為0度,然後每次根據計算出的角度值sweepAngle去累加起始度數,用程式碼來實現下

    /**
     * 記錄當前畫餅圖的度數
     */
    var currentDegree: Float = 0f

    /**
     * 累加餅圖的度數作為下一個繪製的起始度數
     */
    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
            //定義一個隨機生成的顏色數,來區分不同的扇形區域
            val hex = "#" + Integer.toHexString((-16777216 * Math.random()).toInt())
            paint.color = Color.parseColor(hex)
            //角度數
            srctorDegree = scale * 360
            canvas.drawArc(left, top, right, bottom, currentDegree, srctorDegree, true, paint)
            //累加角度
            currentDegree += srctorDegree
        }
    }
複製程式碼

ok,現在我們可以隨機的去定義個數來生成佔比的餅圖了

用kotlin來實現一個餅圖

繪製折線

接下來,我們來繪製折線,折線的起點是每個扇形弧上的一半,path的arcTo方法也可以繪製圓,且方法引數使用也是一樣,我們可以讓arcTo跟著canvas.drawArc一塊畫,arcTo的startAngle起始角度為canvas.drawArc起始角度加上sweepAngle度數的一半,這樣,就定位到了弧邊的一半,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)
            ...
    
複製程式碼

現在,path的位置定位到弧邊的一半了,接下來,我們要知道當前path的座標然後根據座標去繪製折線,

            val bounds = RectF()
            //將path當前的座標賦值給bounds
            path.computeBounds(bounds, true)
複製程式碼

現在拿到座標了,我們再來看看效果圖,折線和文字呈四個方向,我們不如把餅圖分成四個區域,以圓心為座標軸原點,切分四個象限:

  • 第一象限:折線為右上,文字在折線右邊
  • 第二象限:折線為左上,文字在折線左邊
  • 第三象限:折線為左下,文字在折線左邊
  • 第四象限:折線為右下,文字在折線右邊

那麼,接下來就是如何判斷當前起始點在哪個象限了,先以第一象限為例,如果當前的座標大於餅圖橫軸方向一半,並且小於餅圖縱軸方向的一半,那麼就是第一象限,其他依次類推

    /**
     *  橫線的長度
     */
    var lineae: Int = 30

    /**
     * 斜線的長度
     */
    var slantLine: Int = 30
    
  override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
         //計算橫線的比例
         lineae = (width / 30f).toInt()
         //計算斜線的比例
         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)
            }       
         }
            ...
     } 
    
複製程式碼

哎呀,出來了

用kotlin來實現一個餅圖

繪製文字

接下來就是繪製文字了,第一、四象限還好,文字可以在折線後面跟著畫,但是二、三象限的文字就不允許了,我們必須往前移動文字寬度的距離才能完美銜接到折線上,所以,我們來定義一個計算文字的方法

     /**
      * 獲取文字的寬度
      */
     private fun getStringWidth(str: String): Float = paintLine.measureText(str)
複製程式碼

文字是會隨著餅圖的大小進行改變的,所以設定文字大小的比例

    paintLine.textSize = dip(width / 100).toFloat()
複製程式碼

接下來就開始繪製文字吧

    @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
            ...
        
            //獲取當前的百分比文字
            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)
              ...
            }
            
     }

複製程式碼

嗯,還不錯,

用kotlin來實現一個餅圖

然後我們再看看效果圖,餅圖中間還有一塊與背景色一樣的黑圓,這不跟簡單了嘛

        //定義中間黑圓的畫筆
        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)
            
            ...
            
          //在迴圈結束餅圖的時候,以餅圖的原點為中心畫圓  
            canvas.drawCircle(width / 2, width / 2, width / 8, paintCicle)
         }

複製程式碼

用kotlin來實現一個餅圖

然後我們暴露一個方法,提供給Activity去呼叫

    /**
     * 設定扇形引數
     */
    fun setPieData(a: ArrayList<Float>) {
        pieList.clear()
        pieList.addAll(a)
        invalidate()
    }
複製程式碼

那麼,Activity就可以這麼去呼叫了

    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))
    }
複製程式碼

用kotlin來實現一個餅圖

總結

感受就是一句話,用kotlin還真***的爽,新建了個qq群,群號492386431,互相交流交流

關注公眾號

用kotlin來實現一個餅圖

相關文章