diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..61a9130 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml index 1b10434..2baa0fd 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -16,6 +16,7 @@ diff --git a/.idea/misc.xml b/.idea/misc.xml index be0cc41..00206b7 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,7 +1,10 @@ - + + + + diff --git a/README.md b/README.md index 7104750..3d83735 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ -[![](https://jitpack.io/v/furkanaskin/ClickablePieChart.svg)](https://jitpack.io/#furkanaskin/ClickablePieChart) +[![](https://jitpack.io/v/gsotti/ClickablePieChart.svg)](https://jitpack.io/#gsotti/ClickablePieChart) # ClickablePieChart -Android Pie Chart library, supported with **Kotlin DSL**. +Android Chart library, supported with **Kotlin DSL**. -PieChart +PieChart ## Installation Step 1. Add the JitPack repository to your build file @@ -18,7 +18,7 @@ allprojects { Step 2. Add the dependency ```gradle dependencies { - implementation 'com.github.furkanaskin:ClickablePieChart:1.0.7' + implementation 'com.github.gsotti:ClickablePieChart:1.0.13' } ``` @@ -31,6 +31,17 @@ dependencies { chart.setPieChart(pieChart) ``` + +Or create a BarChart + +```kotlin + val barChart = BarChart( + slices = provideSlices(), clickListener = null + ).build() + + chart.setBarChart(barChart) +``` + Also you can use **Kotlin DSL** for building your chart. ```kotlin val pieChartDSL = buildChart { @@ -43,6 +54,20 @@ Also you can use **Kotlin DSL** for building your chart. } chart.setPieChart(pieChartDSL) ``` + +Or create a BarChart + +```kotlin + val barChartDSL = buildBarChart { + slices { provideSlices() } + clickListener { percentage, index -> + // ... + } + } + chart.setBarChart(barChartDSL) +``` + + To setup with legend you need an root layout for legend. ```kotlin chart.showLegend(legendLayout) @@ -52,6 +77,11 @@ Or use with custom legend adapter by inheriting from LegendAdapter chart.showLegend(legendLayout, CustomLegendAdapter()) ``` +Or if you use a barChart you can also change the orientation of the legendAdapter +```kotlin + chart4.showLegend(rootLayout = legendLayout, orientation = LinearLayoutManager.HORIZONTAL or LinearLayoutManager.VERTICAL) +``` + ## XML Attributes @@ -87,5 +117,10 @@ chart.showLegend(legendLayout, CustomLegendAdapter()) + + + + +
integer Animation duration with milliseconds.
app:orientationstringOrientation of BarChart (horizontal/vertical)
diff --git a/app/src/main/java/com/faskn/clickablepiechart/MainActivity.kt b/app/src/main/java/com/faskn/clickablepiechart/MainActivity.kt index d2cfc75..3ab3071 100644 --- a/app/src/main/java/com/faskn/clickablepiechart/MainActivity.kt +++ b/app/src/main/java/com/faskn/clickablepiechart/MainActivity.kt @@ -2,11 +2,10 @@ package com.faskn.clickablepiechart import android.os.Bundle import androidx.appcompat.app.AppCompatActivity -import com.faskn.lib.PieChart -import com.faskn.lib.Slice -import com.faskn.lib.buildChart +import androidx.recyclerview.widget.LinearLayoutManager +import com.faskn.lib.* import kotlinx.android.synthetic.main.activity_main.* -import kotlin.random.Random + class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { @@ -17,7 +16,7 @@ class MainActivity : AppCompatActivity() { val pieChartDSL = buildChart { slices { provideSlices() } sliceWidth { 80f } - sliceStartPoint { 0f } + sliceStartPoint { -90f } clickListener { angle, index -> // ... } @@ -34,29 +33,51 @@ class MainActivity : AppCompatActivity() { chart.showLegend(legendLayout) //OR SET WITH CUSTOMER LEGEND ADAPTER - chart2.setPieChart(pieChart) - chart2.showLegend(legendLayout2,CustomLegendAdapter()) + //chart2.setPieChart(pieChart) + //chart2.showLegend(legendLayout2,CustomLegendAdapter()) + + + val barChart = BarChart( + slices = provideSlices(), clickListener = null + ).build() + + val barChartDSL = buildBarChart { + slices { provideSlices() } + clickListener { percentage, index -> + // ... + } + } + + chart3.setBarChart(barChart) + chart3.showLegend(legendLayout3) + + + chart4.setBarChart(barChartDSL) + chart4.showLegend( + rootLayout = legendLayout4, + layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false) + ) } private fun provideSlices(): ArrayList { return arrayListOf( Slice( - Random.nextInt(1000, 3000).toFloat(), + 2F, R.color.colorPrimary, "Google" ), Slice( - Random.nextInt(1000, 2000).toFloat(), + 2F, R.color.colorPrimaryDark, "Facebook" ), Slice( - Random.nextInt(1000, 5000).toFloat(), + 1F, R.color.materialIndigo600, "Twitter" ), Slice( - Random.nextInt(1000, 10000).toFloat(), + 4F, R.color.colorAccent, "Other" ) diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index af84670..7978849 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,63 +1,123 @@ - + android:layout_height="match_parent"> + android:orientation="vertical" + tools:context=".MainActivity"> - - - - + android:layout_margin="24dp" + android:weightSum="2" + android:orientation="horizontal"> + - + + + + + + - android:layout_weight="1" - android:orientation="horizontal"> + + - - - + + + + + + + + + + android:layout_margin="24dp" + android:weightSum="2" + android:orientation="horizontal"> + + + + + + + + + - \ No newline at end of file + \ No newline at end of file diff --git a/assets/device-2020-11-12-104411.png b/assets/device-2020-11-12-104411.png new file mode 100644 index 0000000..08b3486 Binary files /dev/null and b/assets/device-2020-11-12-104411.png differ diff --git a/lib/src/main/java/com/faskn/lib/BarChartDsl.kt b/lib/src/main/java/com/faskn/lib/BarChartDsl.kt new file mode 100644 index 0000000..18bbc35 --- /dev/null +++ b/lib/src/main/java/com/faskn/lib/BarChartDsl.kt @@ -0,0 +1,84 @@ +package com.faskn.lib + +/** + * Created by turkergoksu on 30-Aug-20 + */ + +@DslMarker +annotation class BarChartDsl +data class BarChart( + var slices: ArrayList, + var clickListener: ((String, Float) -> Unit)? +) { + fun build(): BarChart { + initScaledValues() + initPercentages() + return BarChart(slices, clickListener) + } + + private fun initScaledValues() { + slices.forEachIndexed { _, slice -> + slice.scaledValue = (slice.dataPoint / getSumOfDataPoints()) + } + } + + private fun initPercentages() { + var remainder = 100 + slices.forEach { slice -> + val percentage = (100 * slice.scaledValue!!.toInt()) + slice.percentage = percentage + remainder -= percentage + } + var i = 0 + while (remainder != 0) { + slices[i].percentage = slices[i].percentage!! + 1 + remainder -= 1 + i = (i + 1) % 4 + } + } + + private fun getSumOfDataPoints(): Float { + return slices.sumByDouble { slice -> slice.dataPoint.toDouble() }.toFloat() + } +} + +fun buildBarChart(block: BarChartBuilder.() -> Unit) = BarChartBuilder().apply(block).build() + +@PieChartDsl +class BarChartBuilder { + private lateinit var slices: ArrayList + private var clickListener: ((String, Float) -> Unit)? = null + + fun slices(block: () -> ArrayList) { + slices = block() + } + + fun clickListener(block: ((String, Float) -> Unit)?) { + clickListener = block + } + + + fun build(): BarChart { + initScaledValues() + initPercentages() + return BarChart(slices, clickListener) + } + + private fun initScaledValues() { + slices.forEachIndexed { i, slice -> + val scaledValue = slice.dataPoint / getSumOfDataPoints() + slice.scaledValue = scaledValue + } + } + + private fun initPercentages() { + slices.forEachIndexed { index,slice -> + val percentage = (100 * slice.scaledValue!!).toInt() + slices[index].percentage = percentage + } + } + + 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/ClickableBarChart.kt b/lib/src/main/java/com/faskn/lib/ClickableBarChart.kt new file mode 100644 index 0000000..2048706 --- /dev/null +++ b/lib/src/main/java/com/faskn/lib/ClickableBarChart.kt @@ -0,0 +1,312 @@ +package com.faskn.lib + +/** + * Created by Furkan on 6.08.2020 + */ + +import android.animation.ValueAnimator +import android.content.Context +import android.content.res.ColorStateList +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.drawable.ColorDrawable +import android.util.AttributeSet +import android.view.* +import android.view.animation.LinearInterpolator +import android.widget.LinearLayout +import android.widget.PopupWindow +import android.widget.TextView +import androidx.core.content.ContextCompat +import androidx.core.view.doOnPreDraw +import androidx.core.widget.ImageViewCompat +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.faskn.lib.legend.LegendAdapter +import kotlin.math.abs + +class ClickableBarChart @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : View(context, attrs, defStyleAttr) { + + private var slicePaint: Paint = Paint().apply { + isAntiAlias = true + isDither = true + style = Paint.Style.FILL + } + + private var touchX = 0f + private var touchY = 0f + + // PieChart variables + private var barChart: BarChart? = null + private var slices: ArrayList? = null + + // Animation variables + private var animator: ValueAnimator? = null + private var showPopup = true + private var animationDuration: Int = 1000 + + // Attributes + private var popupText: String? = null + private var showPercentage = false + private var currentAnimationPercentage = 0 + private var orientation: Orientation = Orientation.HORIZONTAL + + init { + initAttributes(attrs) + } + + private fun init() { + initSlices() + startAnimation() + } + + private fun initAttributes(attrs: AttributeSet?) { + val typedArray = + context.theme.obtainStyledAttributes(attrs, R.styleable.ClickablePieChart, 0, 0) + + try { + popupText = typedArray.getString(R.styleable.ClickablePieChart_popupText) ?: "" + + showPercentage = + typedArray.getBoolean(R.styleable.ClickablePieChart_showPercentage, false) + + animationDuration = + abs(typedArray.getInt(R.styleable.ClickablePieChart_animationDuration, 0)) + + showPopup = typedArray.getBoolean(R.styleable.ClickablePieChart_showPopup, true) + + orientation = Orientation.valueOf( + typedArray.getString(R.styleable.ClickablePieChart_orientation)?.toUpperCase() + ?: Orientation.VERTICAL.name + ) + + } finally { + typedArray.recycle() + } + } + + private fun startAnimation() { + animator?.cancel() + animator = ValueAnimator.ofInt(0, 100).apply { + duration = animationDuration.toLong() + interpolator = LinearInterpolator() + addUpdateListener { valueAnimator -> + currentAnimationPercentage = valueAnimator.animatedValue as Int + invalidate() + } + } + animator?.start() + } + + private fun initSlices() { + slices = barChart?.slices + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + + var startPercentage = 0F + + if (slices.isNullOrEmpty().not()) { + slices?.forEach { slice -> + + var endPercentage = (startPercentage + slice.percentage!!.toFloat()) + slicePaint.color = ContextCompat.getColor(context, slice.color) + + if (orientation == Orientation.HORIZONTAL) { + + if (startPercentage < currentAnimationPercentage && endPercentage <= currentAnimationPercentage) { + canvas.drawRect( + (measuredWidth * startPercentage) / 100, + 0F, + ((measuredWidth * (startPercentage + slice.percentage!!.toFloat())) / 100), + measuredHeight.toFloat(), + slicePaint + ) + } else if (startPercentage < currentAnimationPercentage && endPercentage > currentAnimationPercentage) { + canvas.drawRect( + (measuredWidth * startPercentage) / 100, + 0F, + ((measuredWidth * currentAnimationPercentage) / 100).toFloat(), + measuredHeight.toFloat(), + slicePaint + ) + } + } else { + + if (startPercentage < currentAnimationPercentage && endPercentage <= currentAnimationPercentage) { + canvas.drawRect( + 0F, + measuredHeight - (((measuredHeight * (startPercentage + slice.percentage!!.toFloat())) / 100)), + measuredWidth.toFloat(), + measuredHeight - ((measuredHeight * startPercentage) / 100), + slicePaint + ) + } else if (startPercentage < currentAnimationPercentage && endPercentage > currentAnimationPercentage) { + canvas.drawRect( + 0F, + measuredHeight - (((measuredHeight * currentAnimationPercentage) / 100).toFloat()), + measuredWidth.toFloat(), + measuredHeight - ((measuredHeight * startPercentage) / 100), + slicePaint + ) + } + } + + startPercentage = endPercentage + } + } else { + slicePaint.color = ContextCompat.getColor(context, R.color.semiGray) + canvas.drawRect( + 0F, + measuredHeight.toFloat(), + measuredWidth.toFloat(), + 0F, + slicePaint + ) + } + } + + override fun onTouchEvent(event: MotionEvent): Boolean { + return when (event.action) { + MotionEvent.ACTION_DOWN -> { + touchX = event.x + touchY = event.y + true + } + MotionEvent.ACTION_UP -> { + + touchX = event.x + touchY = event.y + + + var currentPercentage = if (orientation == Orientation.VERTICAL) { + 100 - ((touchY * 100) / measuredHeight) + } else { + (touchX * 100) / measuredWidth + } + var calculatedPercentage = 0F + + run { + slices?.forEachIndexed { index, slice -> + + val start = calculatedPercentage + val end = calculatedPercentage + slice.percentage!!.toFloat() + + if (start <= currentPercentage && end > currentPercentage && showPopup) { + barChart?.clickListener?.invoke( + calculatedPercentage.toString(), + index.toFloat() + ) + + showInfoPopup(index, event, ((start + end) / 2).toInt()) + return@run + } + + calculatedPercentage = end + } + } + true + } + else -> false + } + } + + private fun showInfoPopup(index: Int, event: MotionEvent, center: Int) { + val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater + val popupView = inflater.inflate(R.layout.popup_slice, null) + val width = LinearLayout.LayoutParams.WRAP_CONTENT + val height = LinearLayout.LayoutParams.WRAP_CONTENT + val popupWindow = PopupWindow(popupView, width, height, true) + var parent = this + + var popupText = "${slices?.get(index)!!.dataPoint.toInt()} $popupText" + if (showPercentage) { + popupText = "$popupText (%${slices?.get(index)!!.percentage})" + } + popupView.findViewById(R.id.textViewPopupText).text = popupText + + ImageViewCompat.setImageTintList( + popupView.findViewById(R.id.imageViewPopupCircleIndicator), + ColorStateList.valueOf( + ContextCompat.getColor( + context, + slices?.get(index)?.color ?: R.color.semiGray + ) + ) + ) + + popupWindow.setBackgroundDrawable(ColorDrawable()) + popupWindow.showAtLocation( + this, + Gravity.NO_GRAVITY, + event.x.toInt(), + event.y.toInt() + ) + + val currentViewLocation = IntArray(2) + this.getLocationOnScreen(currentViewLocation) + + if (orientation == Orientation.VERTICAL) { + popupView.doOnPreDraw { + popupWindow.update( + currentViewLocation[0] + (parent.measuredWidth / 2) - (it.width / 2), + (currentViewLocation[1] + parent.measuredHeight - (parent.measuredHeight * center) / 100), + popupWindow.width, + popupWindow.height + ) + } + } else { + popupView.doOnPreDraw { + popupWindow.update( + currentViewLocation[0] + ((parent.measuredWidth * center) / 100) - (it.width / 2), + currentViewLocation[1] + (parent.measuredHeight / 2), + popupWindow.width, + popupWindow.height + ) + } + } + + + } + + fun setBarChart(barChart: BarChart) { + this.barChart = barChart + init() + invalidateAndRequestLayout() + } + + fun showPopup(show: Boolean) { + showPopup = show + } + + fun showLegend( + rootLayout: ViewGroup, + adapter: LegendAdapter = LegendAdapter(), + layoutManager: RecyclerView.LayoutManager? = null + ) { + val recyclerView = RecyclerView(context) + recyclerView.layoutManager = + layoutManager ?: LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false) + + recyclerView.adapter = adapter + slices?.toMutableList()?.let { adapter.setup(it) } + recyclerView.overScrollMode = OVER_SCROLL_NEVER + rootLayout.addView(recyclerView) + invalidateAndRequestLayout() + } + + private fun invalidateAndRequestLayout() { + invalidate() + requestLayout() + } + + + enum class Orientation { + VERTICAL, + HORIZONTAL; + } +} \ 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 a86f742..b324dd7 100644 --- a/lib/src/main/java/com/faskn/lib/ClickablePieChart.kt +++ b/lib/src/main/java/com/faskn/lib/ClickablePieChart.kt @@ -288,11 +288,14 @@ class ClickablePieChart @JvmOverloads constructor( showPopup = show } - fun showLegend(rootLayout: ViewGroup, adapter: LegendAdapter = LegendAdapter()) { + fun showLegend( + rootLayout: ViewGroup, + adapter: LegendAdapter = LegendAdapter(), + layoutManager: RecyclerView.LayoutManager? = null + ) { val recyclerView = RecyclerView(context) - val linearLayoutManager = - LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false) - recyclerView.layoutManager = linearLayoutManager + recyclerView.layoutManager = + layoutManager ?: LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false) recyclerView.adapter = adapter slices?.toMutableList()?.let { adapter.setup(it) } recyclerView.overScrollMode = OVER_SCROLL_NEVER diff --git a/lib/src/main/res/values/attrs.xml b/lib/src/main/res/values/attrs.xml index d845644..1d87fd6 100644 --- a/lib/src/main/res/values/attrs.xml +++ b/lib/src/main/res/values/attrs.xml @@ -6,5 +6,6 @@ + \ No newline at end of file