mirror of
https://github.com/dzeiocom/charts.git
synced 2025-07-23 13:29:54 +00:00
feat: Add basic animation support (#20)
This commit is contained in:
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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<Entry>
|
||||
}
|
||||
}
|
||||
|
@ -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() {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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() {
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user