feat: Add basic animation support (#20)

This commit is contained in:
Florian Bouillon 2023-01-12 22:24:00 +01:00 committed by GitHub
parent ed06aa697e
commit c9fee7ca24
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 176 additions and 45 deletions

22
.editorconfig Normal file
View File

@ -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

1
.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
* text=auto eol=lf

View File

@ -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
}

View File

@ -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)
}

View File

@ -6,6 +6,8 @@ import com.dzeio.charts.series.SerieInterface
interface ChartViewInterface {
val animator: Animation
/**
* Chart Type
*/

View File

@ -41,20 +41,45 @@ class BarSerie(
private val rect = Rect()
override fun onDraw(canvas: Canvas, drawableSpace: RectF) {
private var entriesCurrentY: HashMap<Double, AnimationProgress> = 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() {

View File

@ -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
}

View File

@ -31,16 +31,43 @@ class LineSerie(
textAlign = Paint.Align.CENTER
}
override fun onDraw(canvas: Canvas, drawableSpace: RectF) {
private var entriesCurrentY: HashMap<Double, AnimationProgress> = 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() {

View File

@ -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

View File

@ -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())

View File

@ -23,6 +23,11 @@
android:layout_width="match_parent"
android:layout_height="200dp" />
<com.dzeio.charts.ChartView
android:id="@+id/chart_stacked"
android:layout_width="match_parent"
android:layout_height="200dp" />
<com.dzeio.charts.ChartView
android:id="@+id/chart_customization"
android:layout_width="match_parent"