From c9fee7ca24ea3a89c963a7059b87e7c61d6df633 Mon Sep 17 00:00:00 2001 From: Avior Date: Thu, 12 Jan 2023 22:24:00 +0100 Subject: [PATCH] feat: Add basic animation support (#20) --- .editorconfig | 22 ++++++++++ .gitattributes | 1 + .../main/java/com/dzeio/charts/Animation.kt | 41 +++++++++++++------ .../main/java/com/dzeio/charts/ChartView.kt | 32 +++++++-------- .../com/dzeio/charts/ChartViewInterface.kt | 4 +- .../java/com/dzeio/charts/series/BarSerie.kt | 31 +++++++++++++- .../java/com/dzeio/charts/series/BaseSerie.kt | 10 ++++- .../java/com/dzeio/charts/series/LineSerie.kt | 35 ++++++++++++++-- .../com/dzeio/charts/series/SerieInterface.kt | 5 ++- .../com/dzeio/chartstest/ui/MainFragment.kt | 33 +++++++++++++-- sample/src/main/res/layout/fragment_main.xml | 7 +++- 11 files changed, 176 insertions(+), 45 deletions(-) create mode 100644 .editorconfig create mode 100644 .gitattributes diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..e51417b --- /dev/null +++ b/.editorconfig @@ -0,0 +1,22 @@ +root = true + +# Base Configuration +[*] +indent_style = tab +indent_size = 4 +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true +max_line_length = 120 +end_of_line = lf + +# Markdown Standards +[*.md] +indent_style = space +indent_size = 2 +trim_trailing_whitespace = false + + +# Java/Kotlin Standards +[*.{java,kt,kts,gradle,xml}] +indent_style = space diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6313b56 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/library/src/main/java/com/dzeio/charts/Animation.kt b/library/src/main/java/com/dzeio/charts/Animation.kt index 08c19b9..f318f17 100644 --- a/library/src/main/java/com/dzeio/charts/Animation.kt +++ b/library/src/main/java/com/dzeio/charts/Animation.kt @@ -1,7 +1,6 @@ package com.dzeio.charts import kotlin.math.abs -import kotlin.math.max data class Animation( /** @@ -22,28 +21,24 @@ data class Animation( /** * Update the value depending on the maximum obtainable value * - * @param maxValue the maximum value the item can obtain * @param targetValue the value you want to obtain at the end of the animation * @param currentValue the current value + * @param startValue the value at which the the base started on + * @param step override the auto moveValue change * * @return the new updated value */ fun updateValue( - maxValue: Float, targetValue: Float, currentValue: Float, - minValue: Float, - minStep: Float + startValue: Float, + step: Float? ): Float { if (!enabled) { return targetValue } - if (currentValue < minValue) { - return minValue - } - - val moveValue = max(minStep, (maxValue - targetValue) / refreshRate) + val moveValue = step ?: (abs(targetValue - startValue) / duration * refreshRate) var result = targetValue if (currentValue < targetValue) { @@ -53,14 +48,34 @@ data class Animation( } if ( - abs(targetValue - currentValue) <= moveValue || - result < minValue || - result > maxValue + abs(targetValue - currentValue) <= moveValue ) { return targetValue } return result } + /** + * Update the value depending on the maximum obtainable value + * + * @param targetValue the value you want to obtain at the end of the animation + * @param currentValue the current value + * @param startValue the value at which the the base started on + * + * @return the new updated value + */ + fun updateValue( + targetValue: Float, + currentValue: Float, + startValue: Float + ): Float { + return updateValue( + targetValue, + currentValue, + startValue, + null + ) + } + fun getDelay() = this.duration / this.refreshRate } diff --git a/library/src/main/java/com/dzeio/charts/ChartView.kt b/library/src/main/java/com/dzeio/charts/ChartView.kt index 562f5d3..5f5261d 100644 --- a/library/src/main/java/com/dzeio/charts/ChartView.kt +++ b/library/src/main/java/com/dzeio/charts/ChartView.kt @@ -21,6 +21,8 @@ class ChartView @JvmOverloads constructor(context: Context?, attrs: AttributeSet const val TAG = "Charts/ChartView" } + override val animator: Animation = Animation() + override var type: ChartType = ChartType.BASIC override var debug: Boolean = false @@ -60,22 +62,6 @@ class ChartView @JvmOverloads constructor(context: Context?, attrs: AttributeSet // } } -// val animator: Runnable = object : Runnable { -// override fun run() { -// var needNewFrame = false -// for (serie in series) { -// val result = serie.onUpdate() -// if (result) { -// needNewFrame = true -// } -// } -// if (needNewFrame) { -// postDelayed(this, animation.getDelay().toLong()) -// } -// invalidate() -// } -// } - // rect used for calculations private val rect = RectF() @@ -144,15 +130,25 @@ class ChartView @JvmOverloads constructor(context: Context?, attrs: AttributeSet ) } + var needRedraw = false if (type == ChartType.STACKED) { for (serie in series.reversed()) { - serie.onDraw(canvas, rect) + val tmp = serie.onDraw(canvas, rect) + if (tmp) { + needRedraw = true + } } } else { for (serie in series) { - serie.onDraw(canvas, rect) + val tmp = serie.onDraw(canvas, rect) + if (tmp) { + needRedraw = true + } } } + if (needRedraw) { + postDelayed({ this.invalidate() }, animator.getDelay().toLong()) + } super.onDraw(canvas) } diff --git a/library/src/main/java/com/dzeio/charts/ChartViewInterface.kt b/library/src/main/java/com/dzeio/charts/ChartViewInterface.kt index c1ae574..a9d0e8f 100644 --- a/library/src/main/java/com/dzeio/charts/ChartViewInterface.kt +++ b/library/src/main/java/com/dzeio/charts/ChartViewInterface.kt @@ -6,6 +6,8 @@ import com.dzeio.charts.series.SerieInterface interface ChartViewInterface { + val animator: Animation + /** * Chart Type */ @@ -49,4 +51,4 @@ interface ChartViewInterface { * @return the whole dataset (sorted and cleaned up of dupps) */ fun getDataset(): ArrayList -} \ No newline at end of file +} diff --git a/library/src/main/java/com/dzeio/charts/series/BarSerie.kt b/library/src/main/java/com/dzeio/charts/series/BarSerie.kt index 02337e2..2700d86 100644 --- a/library/src/main/java/com/dzeio/charts/series/BarSerie.kt +++ b/library/src/main/java/com/dzeio/charts/series/BarSerie.kt @@ -41,20 +41,45 @@ class BarSerie( private val rect = Rect() - override fun onDraw(canvas: Canvas, drawableSpace: RectF) { + private var entriesCurrentY: HashMap = hashMapOf() + + override fun onDraw(canvas: Canvas, drawableSpace: RectF): Boolean { val displayedEntries = getDisplayedEntries() val barWidth = view.xAxis.getEntryWidth(drawableSpace).toFloat() val zero = view.yAxis.getPositionOnRect(0f, drawableSpace) + var needUpdate = false + + val iterator = entriesCurrentY.iterator() + while (iterator.hasNext()) { + val key = iterator.next().key + if (displayedEntries.find { it.x == key } == null) iterator.remove() + } + for (entry in displayedEntries) { + if (entriesCurrentY[entry.x] == null) { + entriesCurrentY[entry.x] = AnimationProgress(zero) + } + // calculated height in percent from 0 to 100 - val top = view.yAxis.getPositionOnRect(entry, drawableSpace) + var top = view.yAxis.getPositionOnRect(entry, drawableSpace) var posX = drawableSpace.left + view.xAxis.getPositionOnRect( entry, drawableSpace ).toFloat() + // change value with the animator + if (!entriesCurrentY[entry.x]!!.finished) { + val newY = view.animator.updateValue(top, entriesCurrentY[entry.x]!!.value, zero) + if (!needUpdate && top != newY) { + needUpdate = true + } + entriesCurrentY[entry.x]!!.finished = top == newY + top = newY + entriesCurrentY[entry.x]!!.value = top + } + val right = (posX + barWidth).coerceAtMost(drawableSpace.right) if (posX > right) { @@ -134,6 +159,8 @@ class BarSerie( if (doDisplayIn) textPaint else textExternalPaint ) } + + return needUpdate } override fun refresh() { diff --git a/library/src/main/java/com/dzeio/charts/series/BaseSerie.kt b/library/src/main/java/com/dzeio/charts/series/BaseSerie.kt index fb1ec2b..23c032f 100644 --- a/library/src/main/java/com/dzeio/charts/series/BaseSerie.kt +++ b/library/src/main/java/com/dzeio/charts/series/BaseSerie.kt @@ -15,6 +15,11 @@ sealed class BaseSerie( const val TAG = "Charts/BaseSerie" } + protected data class AnimationProgress( + var value: Float, + var finished: Boolean = false + ) + override var formatValue: (entry: Entry) -> String = { entry -> entry.y.roundToInt().toString()} override var yAxisPosition: YAxisPosition = YAxisPosition.RIGHT @@ -31,7 +36,7 @@ sealed class BaseSerie( for (i in 0 until entries.size) { val it = entries[i] if (it.x in minX..maxX) { - if (result.size === 0 && i > 0) { + if (result.size == 0 && i > 0) { result.add((entries[i - 1])) } lastIndex = i @@ -43,8 +48,9 @@ sealed class BaseSerie( result.add(entries [lastIndex + 1]) } + result.sortBy { it.x } return result } - abstract override fun onDraw(canvas: Canvas, drawableSpace: RectF) + abstract override fun onDraw(canvas: Canvas, drawableSpace: RectF): Boolean } diff --git a/library/src/main/java/com/dzeio/charts/series/LineSerie.kt b/library/src/main/java/com/dzeio/charts/series/LineSerie.kt index 467cd1a..8076bf9 100644 --- a/library/src/main/java/com/dzeio/charts/series/LineSerie.kt +++ b/library/src/main/java/com/dzeio/charts/series/LineSerie.kt @@ -31,16 +31,43 @@ class LineSerie( textAlign = Paint.Align.CENTER } - override fun onDraw(canvas: Canvas, drawableSpace: RectF) { + private var entriesCurrentY: HashMap = hashMapOf() + + override fun onDraw(canvas: Canvas, drawableSpace: RectF): Boolean { val displayedEntries = getDisplayedEntries() - displayedEntries.sortBy { it.x } var previousPosX: Float? = null var previousPosY: Float? = null + var needUpdate = false + + val iterator = entriesCurrentY.iterator() + while (iterator.hasNext()) { + val key = iterator.next().key + if (displayedEntries.find { it.x == key } == null) iterator.remove() + } + + val zero = view.yAxis.getPositionOnRect(0f, drawableSpace) + for (entry in displayedEntries) { + if (entriesCurrentY[entry.x] == null) { + entriesCurrentY[entry.x] = AnimationProgress(zero) + } + // calculated height in percent from 0 to 100 - val top = view.yAxis.getPositionOnRect(entry, drawableSpace) + var top = view.yAxis.getPositionOnRect(entry, drawableSpace) + + // change value with the animator + if (!entriesCurrentY[entry.x]!!.finished) { + val newY = view.animator.updateValue(top, entriesCurrentY[entry.x]!!.value, zero) + if (!needUpdate && top != newY) { + needUpdate = true + } + entriesCurrentY[entry.x]!!.finished = top == newY + top = newY + entriesCurrentY[entry.x]!!.value = top + } + val posX = (drawableSpace.left + view.xAxis.getPositionOnRect(entry, drawableSpace) + view.xAxis.getEntryWidth(drawableSpace) / 2f).toFloat() @@ -65,6 +92,8 @@ class LineSerie( previousPosX = posX previousPosY = top } + + return needUpdate } override fun refresh() { diff --git a/library/src/main/java/com/dzeio/charts/series/SerieInterface.kt b/library/src/main/java/com/dzeio/charts/series/SerieInterface.kt index 0b19ac2..c78b63f 100644 --- a/library/src/main/java/com/dzeio/charts/series/SerieInterface.kt +++ b/library/src/main/java/com/dzeio/charts/series/SerieInterface.kt @@ -35,8 +35,9 @@ sealed interface SerieInterface { * * @param canvas the canvas to draw on * @param drawableSpace the space you are allowed to draw on + * @return do the serie need to be drawn again or not */ - fun onDraw(canvas: Canvas, drawableSpace: RectF) + fun onDraw(canvas: Canvas, drawableSpace: RectF): Boolean /** * run when manually refreshing the system @@ -44,4 +45,4 @@ sealed interface SerieInterface { * this is where the pre-logic is handled to make [onDraw] quicker */ fun refresh() -} \ No newline at end of file +} diff --git a/sample/src/main/java/com/dzeio/chartstest/ui/MainFragment.kt b/sample/src/main/java/com/dzeio/chartstest/ui/MainFragment.kt index b9f0e1f..637f73c 100644 --- a/sample/src/main/java/com/dzeio/chartstest/ui/MainFragment.kt +++ b/sample/src/main/java/com/dzeio/chartstest/ui/MainFragment.kt @@ -36,16 +36,42 @@ class MainFragment : Fragment() { val serie1 = BarSerie(this) val serie2 = BarSerie(this) + animator.duration = 750 + // transform the chart into a grouped chart - type = ChartType.STACKED + type = ChartType.GROUPED + yAxis.setYMin(0f) // utils function to use Material3 auto colors materielTheme(this, requireView()) serie2.barPaint.color = Color.RED // give the serie it's entries - serie1.entries = generateRandomDataset(1) - serie2.entries = generateRandomDataset(1) + serie1.entries = generateRandomDataset(5) + serie2.entries = generateRandomDataset(5) + + // refresh the Chart + refresh() + } + + binding.chartStacked.apply { + // setup the Serie + val serie1 = BarSerie(this) + val serie2 = BarSerie(this) + + animator.duration = 750 + + // transform the chart into a grouped chart + type = ChartType.STACKED + yAxis.setYMin(0f) + + // utils function to use Material3 auto colors + materielTheme(this, requireView()) + serie2.barPaint.color = Color.RED + + // give the serie it's entries + serie1.entries = generateRandomDataset(10) + serie2.entries = generateRandomDataset(10) // refresh the Chart refresh() @@ -68,6 +94,7 @@ class MainFragment : Fragment() { binding.chartBar.apply { // setup the Serie val serie = BarSerie(this) + yAxis.setYMin(0f) // utils function to use Material3 auto colors materielTheme(this, requireView()) diff --git a/sample/src/main/res/layout/fragment_main.xml b/sample/src/main/res/layout/fragment_main.xml index eeaafe6..d8d6133 100644 --- a/sample/src/main/res/layout/fragment_main.xml +++ b/sample/src/main/res/layout/fragment_main.xml @@ -23,6 +23,11 @@ android:layout_width="match_parent" android:layout_height="200dp" /> + + - \ No newline at end of file +