Skip to content

Commit

Permalink
Added fill animation and PieChart instance
Browse files Browse the repository at this point in the history
  • Loading branch information
turkergoksu committed Aug 16, 2020
1 parent 02e2580 commit e756245
Show file tree
Hide file tree
Showing 4 changed files with 161 additions and 81 deletions.
47 changes: 29 additions & 18 deletions app/src/main/java/com/faskn/clickablepiechart/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -1,38 +1,49 @@
package com.faskn.clickablepiechart

import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import com.faskn.lib.PieChart
import com.faskn.lib.Slice
import kotlinx.android.synthetic.main.activity_main.*
import kotlin.random.Random

class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
chart.setCenterColor(R.color.white)
/* chart.setStartPoint(-90f)*/
chart.setSliceWidth(250f)
chart.setListener { data, index ->
/* Toast.makeText(this, index.toString(), Toast.LENGTH_SHORT).show()*/
}

// Example
var pieChart = PieChart.Builder(arrayOf(
Slice(30f, R.color.brown700),
Slice(60f, R.color.materialRed700),
Slice(120f, R.color.materialIndigo600),
Slice(150f, R.color.materialRed400)
))
val pieChart0 = PieChart.Builder(
arrayOf(
Slice(30f, R.color.brown700),
Slice(60f, R.color.materialRed700),
Slice(120f, R.color.materialIndigo600),
Slice(150f, R.color.materialRed400)
)
)
.setClickListener { string, float ->
Log.d("ses", "s " + string)
Log.d("ses", "f " + float.toString())
}
.build()

chart.setSliceColor(
intArrayOf(
R.color.brown700,
R.color.materialRed700,
R.color.materialIndigo600,
R.color.materialRed400
// Example 2
val pieChart1 = PieChart.Builder(
arrayOf(
Slice(Random.nextInt(0, 100).toFloat(), R.color.brown700),
Slice(Random.nextInt(0, 100).toFloat(), R.color.materialRed700),
Slice(Random.nextInt(0, 100).toFloat(), R.color.materialIndigo600),
Slice(Random.nextInt(0, 100).toFloat(), R.color.materialRed400)
)
)
chart.setDataPoints(floatArrayOf(30f, 60f, 120f, 150f))
.setClickListener { string, float ->
Log.d("ses", "s " + string)
Log.d("ses", "f " + float.toString())
}
.build()

chart.setPieChart(pieChart0)
}
}
145 changes: 85 additions & 60 deletions lib/src/main/java/com/faskn/lib/ClickablePieChart.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ package com.faskn.lib
* Created by Furkan on 6.08.2020
*/

import android.annotation.SuppressLint
import android.animation.ValueAnimator
import android.content.Context
import android.content.res.ColorStateList
import android.graphics.Canvas
Expand All @@ -16,6 +16,7 @@ import android.view.Gravity
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.animation.LinearInterpolator
import android.widget.*
import androidx.core.content.ContextCompat
import androidx.core.view.doOnPreDraw
Expand All @@ -27,34 +28,43 @@ import kotlin.math.sin

class ClickablePieChart @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
private val attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {

private var slicePaint: Paint = Paint()
private var centerPaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG)
private var sliceColors: IntArray = intArrayOf(Color.RED, Color.GREEN, Color.BLUE, Color.YELLOW)
private var rectF: RectF? = null
private var dataPoints: FloatArray = floatArrayOf()
private var sliceStartPoint = 0F
private var sliceStartPoint = 0F // FIXME: 16-Aug-20 remove if unnecessary
private var sliceWidth = 80f
private var touchX = 0f
private var touchY = 0f
private var clickListener: ((String, Float) -> Unit)? = null
private var pointsArray = arrayListOf<Pair<Float, Float>>()

// PieChart variables
private var pieChart: PieChart? = null
private lateinit var slices: List<Slice>

// Animation variables
private var animator: ValueAnimator? = null
private var currentSweepAngle = 0

// Attributes
private lateinit var popupText: String

init {
initAttributes(attrs)
}

private fun init() {
slicePaint.isAntiAlias = true
slicePaint.isDither = true
slicePaint.style = Paint.Style.FILL

centerPaint.color = Color.WHITE
centerPaint.style = Paint.Style.FILL

initAttributes(attrs)
initSlices()
startAnimation()
}

private fun initAttributes(attrs: AttributeSet?) {
Expand All @@ -68,43 +78,76 @@ class ClickablePieChart @JvmOverloads constructor(
}
}

private fun scale(): FloatArray {
val scaledValues = FloatArray(dataPoints.size)
for (i in dataPoints.indices) {
scaledValues.fill((dataPoints[i] / getTotal()) * 360, i, dataPoints.size)
private fun initSlices() {
slices = pieChart?.slices?.toList()!!
}

private fun startAnimation() {
animator?.cancel()
animator = ValueAnimator.ofInt(0, 360).apply {
duration = 1000
interpolator = LinearInterpolator()
addUpdateListener { valueAnimator ->
currentSweepAngle = valueAnimator.animatedValue as Int
invalidate()
}
}
return scaledValues
animator?.start()
}

@SuppressLint("DrawAllocation")
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)

rectF = RectF(
0f,
0f,
width.coerceAtMost(height).toFloat(),
width.coerceAtMost(height).toFloat()
)
}

val scaledValues = scale()
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)

for (i in scaledValues.indices) {
slicePaint.color = ContextCompat.getColor(context, sliceColors[i])
canvas!!.drawArc(rectF!!, sliceStartPoint, scaledValues[i], true, slicePaint)
pointsArray.add(Pair(sliceStartPoint, sliceStartPoint + scaledValues[i]))
sliceStartPoint += scaledValues[i]
}
if (pieChart != null) {
slices.forEach { slice ->
val arc = slice.arc!!
if (currentSweepAngle > arc.startAngle + arc.sweepAngle) {
slicePaint.color = ContextCompat.getColor(context, slice.color)
canvas?.drawArc(
rectF!!,
pieChart?.sliceStartPoint!! + arc.startAngle,
arc.sweepAngle,
true,
slicePaint
)
} else {
if (currentSweepAngle > arc.startAngle) {
slicePaint.color = ContextCompat.getColor(context, slice.color)
canvas?.drawArc(
rectF!!,
pieChart?.sliceStartPoint!! + arc.startAngle,
currentSweepAngle - arc.startAngle,
true,
slicePaint
)
}
}
}

val centerX = (measuredWidth / 2).toFloat()
val centerY = (measuredHeight / 2).toFloat()
val radius = centerX.coerceAtMost(centerY)
val centerX = (measuredWidth / 2).toFloat()
val centerY = (measuredHeight / 2).toFloat()
val radius = centerX.coerceAtMost(centerY)

canvas!!.drawCircle(rectF!!.centerX(), rectF!!.centerY(), radius - sliceWidth, centerPaint)
canvas!!.drawCircle(
rectF!!.centerX(),
rectF!!.centerY(),
radius - pieChart?.sliceWidth!!,
centerPaint
)
}
}

private fun getTotal(): Float = dataPoints.sum()

override fun onTouchEvent(event: MotionEvent?): Boolean {
return when (event?.action) {
MotionEvent.ACTION_DOWN -> {
Expand All @@ -120,7 +163,8 @@ class ClickablePieChart @JvmOverloads constructor(
)
)

touchAngle -= sliceStartPoint
// FIXME: 16-Aug-20 Remove subtraction if unnecessary. On runtime sliceStartPoint is always 0f.
// touchAngle -= sliceStartPoint
touchAngle %= 360

if (touchAngle < 0) {
Expand All @@ -129,10 +173,10 @@ class ClickablePieChart @JvmOverloads constructor(

var total = 0.0f
var forEachStopper = false // what a idiot stuff
dataPoints.forEachIndexed { index, data ->
total += data % 360f
slices.forEachIndexed { index, slice ->
total += slice.dataPoint % 360f
if (touchAngle <= total && !forEachStopper) {
clickListener?.invoke(touchAngle.toString(), index.toFloat())
pieChart?.clickListener?.invoke(touchAngle.toString(), index.toFloat())
forEachStopper = true
showInfoPopup(index)
}
Expand All @@ -150,13 +194,14 @@ class ClickablePieChart @JvmOverloads constructor(
val width = LinearLayout.LayoutParams.WRAP_CONTENT
val height = LinearLayout.LayoutParams.WRAP_CONTENT
val popupWindow = PopupWindow(popupView, width, height, true)
var center = pointsArray[index].toList().average()
var center = slices[index].arc?.average()!!
val halfRadius = rectF!!.centerX()

popupView.findViewById<TextView>(R.id.textViewPopupText).text = "${center.toInt()} $popupText"
popupView.findViewById<TextView>(R.id.textViewPopupText).text =
"${center.toInt()} $popupText"
ImageViewCompat.setImageTintList(
popupView.findViewById<ImageView>(R.id.imageViewPopupCircleIndicator),
ColorStateList.valueOf(ContextCompat.getColor(context, sliceColors[index]))
ColorStateList.valueOf(ContextCompat.getColor(context, slices[index].color))
)

val calculatedX =
Expand All @@ -167,7 +212,7 @@ class ClickablePieChart @JvmOverloads constructor(
val currentViewLocation = IntArray(2)
this.getLocationOnScreen(currentViewLocation)

val halfOfSliceWidth = (sliceWidth / 2).toInt()
val halfOfSliceWidth = (pieChart?.sliceWidth?.p2d(context)!! / 2).toInt()
val popupWindowX =
(currentViewLocation[0] + halfRadius.toInt()) + calculatedX -
(if (calculatedX < 0) -halfOfSliceWidth else halfOfSliceWidth)
Expand All @@ -189,38 +234,18 @@ class ClickablePieChart @JvmOverloads constructor(
popupWindow.height
)
}

val currentData = dataPoints[index]

}

fun setSliceWidth(width: Float) {
sliceWidth = width.p2d(context)
}

fun setListener(listener: (String, Float) -> (Unit)) {
clickListener = listener
}

fun setStartPoint(point: Float) {
sliceStartPoint = point
}

fun setDataPoints(data: FloatArray) {
dataPoints = data
invalidateAndRequestLayout()
fun setPieChart(pieChart: PieChart) {
this.pieChart = pieChart
init()
}

fun setCenterColor(colorId: Int) {
centerPaint.color = ContextCompat.getColor(context, colorId)
invalidateAndRequestLayout()
}

fun setSliceColor(colors: IntArray) {
sliceColors = colors
invalidateAndRequestLayout()
}

private fun invalidateAndRequestLayout() {
invalidate()
requestLayout()
Expand Down
37 changes: 36 additions & 1 deletion lib/src/main/java/com/faskn/lib/PieChart.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,22 @@ package com.faskn.lib
* Created by turkergoksu on 12-Aug-20
*/

class PieChart private constructor() {
class PieChart private constructor(
var slices: Array<Slice>,
var clickListener: ((String, Float) -> Unit)? = null,
var sliceStartPoint: Float,
var sliceWidth: Float
) {
data class Builder(
private var slices: Array<Slice>,
private var clickListener: ((String, Float) -> Unit)? = null,
private var sliceStartPoint: Float? = 0f,
private var sliceWidth: Float? = 80f
) {
init {
initScaledArcs()
}

fun setSlices(slices: Array<Slice>) = apply { this.slices = slices }
fun setClickListener(clickListener: ((String, Float) -> Unit)) =
apply { this.clickListener = clickListener }
Expand All @@ -21,5 +30,31 @@ class PieChart private constructor() {
fun setSliceWidth(sliceWidth: Float) = apply { this.sliceWidth = sliceWidth }

fun getSlices() = slices

fun build(): PieChart =
PieChart(
slices,
clickListener,
sliceStartPoint!!,
sliceWidth!!
)

private fun initScaledArcs() {
slices.forEachIndexed { i, slice ->
val scaledValue = (slice.dataPoint / getSumOfDataPoints()) * 360
if (i != 0) {
slice.arc = Arc(
slices[i - 1].arc?.sweepAngle!!,
slices[i - 1].arc?.sweepAngle!!.plus(scaledValue)
)
} else {
slice.arc = Arc(0f, scaledValue)
}
}
}

private fun getSumOfDataPoints(): Float {
return slices.sumByDouble { slice -> slice.dataPoint.toDouble() }.toFloat()
}
}
}
Loading

0 comments on commit e756245

Please sign in to comment.