diff --git a/app/src/main/java/com/faskn/clickablepiechart/MainActivity.kt b/app/src/main/java/com/faskn/clickablepiechart/MainActivity.kt index 4b3b3e3..440fd37 100644 --- a/app/src/main/java/com/faskn/clickablepiechart/MainActivity.kt +++ b/app/src/main/java/com/faskn/clickablepiechart/MainActivity.kt @@ -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) } } \ No newline at end of file diff --git a/lib/src/main/java/com/faskn/lib/ClickablePieChart.kt b/lib/src/main/java/com/faskn/lib/ClickablePieChart.kt index c2cac17..e715aa3 100644 --- a/lib/src/main/java/com/faskn/lib/ClickablePieChart.kt +++ b/lib/src/main/java/com/faskn/lib/ClickablePieChart.kt @@ -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 @@ -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 @@ -27,26 +28,34 @@ 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>() + + // PieChart variables + private var pieChart: PieChart? = null + private lateinit var slices: List + + // 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 @@ -54,7 +63,8 @@ class ClickablePieChart @JvmOverloads constructor( centerPaint.color = Color.WHITE centerPaint.style = Paint.Style.FILL - initAttributes(attrs) + initSlices() + startAnimation() } private fun initAttributes(attrs: AttributeSet?) { @@ -68,17 +78,25 @@ 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, @@ -86,25 +104,50 @@ class ClickablePieChart @JvmOverloads constructor( 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 -> { @@ -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) { @@ -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) } @@ -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(R.id.textViewPopupText).text = "${center.toInt()} $popupText" + popupView.findViewById(R.id.textViewPopupText).text = + "${center.toInt()} $popupText" ImageViewCompat.setImageTintList( popupView.findViewById(R.id.imageViewPopupCircleIndicator), - ColorStateList.valueOf(ContextCompat.getColor(context, sliceColors[index])) + ColorStateList.valueOf(ContextCompat.getColor(context, slices[index].color)) ) val calculatedX = @@ -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) @@ -189,26 +234,11 @@ 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) { @@ -216,11 +246,6 @@ class ClickablePieChart @JvmOverloads constructor( invalidateAndRequestLayout() } - fun setSliceColor(colors: IntArray) { - sliceColors = colors - invalidateAndRequestLayout() - } - private fun invalidateAndRequestLayout() { invalidate() requestLayout() diff --git a/lib/src/main/java/com/faskn/lib/PieChart.kt b/lib/src/main/java/com/faskn/lib/PieChart.kt index 310d9ca..d1169f6 100644 --- a/lib/src/main/java/com/faskn/lib/PieChart.kt +++ b/lib/src/main/java/com/faskn/lib/PieChart.kt @@ -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, + var clickListener: ((String, Float) -> Unit)? = null, + var sliceStartPoint: Float, + var sliceWidth: Float +) { data class Builder( private var slices: Array, private var clickListener: ((String, Float) -> Unit)? = null, private var sliceStartPoint: Float? = 0f, private var sliceWidth: Float? = 80f ) { + init { + initScaledArcs() + } + fun setSlices(slices: Array) = apply { this.slices = slices } fun setClickListener(clickListener: ((String, Float) -> Unit)) = apply { this.clickListener = clickListener } @@ -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() + } } } \ No newline at end of file diff --git a/lib/src/main/java/com/faskn/lib/Slice.kt b/lib/src/main/java/com/faskn/lib/Slice.kt index 054591f..2dc61d6 100644 --- a/lib/src/main/java/com/faskn/lib/Slice.kt +++ b/lib/src/main/java/com/faskn/lib/Slice.kt @@ -6,5 +6,14 @@ package com.faskn.lib data class Slice( val dataPoint: Float, - val color: Int -) \ No newline at end of file + val color: Int, + var arc: Arc? = null +) + +data class Arc( + val startAngle: Float, + val sweepAngle: Float +) { + fun average(): Double = + (startAngle / 2) + (sweepAngle / 2) + (((startAngle % 2) + (sweepAngle % 2)) / 2).toDouble() +} \ No newline at end of file