mirror of
https://github.com/dzeiocom/charts.git
synced 2025-04-22 10:42:09 +00:00
feat: Add basic animation support (#20)
This commit is contained in:
parent
ed06aa697e
commit
c9fee7ca24
22
.editorconfig
Normal file
22
.editorconfig
Normal 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
1
.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
* text=auto eol=lf
|
@ -1,7 +1,6 @@
|
|||||||
package com.dzeio.charts
|
package com.dzeio.charts
|
||||||
|
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import kotlin.math.max
|
|
||||||
|
|
||||||
data class Animation(
|
data class Animation(
|
||||||
/**
|
/**
|
||||||
@ -22,28 +21,24 @@ data class Animation(
|
|||||||
/**
|
/**
|
||||||
* Update the value depending on the maximum obtainable value
|
* 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 targetValue the value you want to obtain at the end of the animation
|
||||||
* @param currentValue the current value
|
* @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
|
* @return the new updated value
|
||||||
*/
|
*/
|
||||||
fun updateValue(
|
fun updateValue(
|
||||||
maxValue: Float,
|
|
||||||
targetValue: Float,
|
targetValue: Float,
|
||||||
currentValue: Float,
|
currentValue: Float,
|
||||||
minValue: Float,
|
startValue: Float,
|
||||||
minStep: Float
|
step: Float?
|
||||||
): Float {
|
): Float {
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
return targetValue
|
return targetValue
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentValue < minValue) {
|
val moveValue = step ?: (abs(targetValue - startValue) / duration * refreshRate)
|
||||||
return minValue
|
|
||||||
}
|
|
||||||
|
|
||||||
val moveValue = max(minStep, (maxValue - targetValue) / refreshRate)
|
|
||||||
|
|
||||||
var result = targetValue
|
var result = targetValue
|
||||||
if (currentValue < targetValue) {
|
if (currentValue < targetValue) {
|
||||||
@ -53,14 +48,34 @@ data class Animation(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
abs(targetValue - currentValue) <= moveValue ||
|
abs(targetValue - currentValue) <= moveValue
|
||||||
result < minValue ||
|
|
||||||
result > maxValue
|
|
||||||
) {
|
) {
|
||||||
return targetValue
|
return targetValue
|
||||||
}
|
}
|
||||||
return result
|
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
|
fun getDelay() = this.duration / this.refreshRate
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,8 @@ class ChartView @JvmOverloads constructor(context: Context?, attrs: AttributeSet
|
|||||||
const val TAG = "Charts/ChartView"
|
const val TAG = "Charts/ChartView"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override val animator: Animation = Animation()
|
||||||
|
|
||||||
override var type: ChartType = ChartType.BASIC
|
override var type: ChartType = ChartType.BASIC
|
||||||
|
|
||||||
override var debug: Boolean = false
|
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
|
// rect used for calculations
|
||||||
private val rect = RectF()
|
private val rect = RectF()
|
||||||
|
|
||||||
@ -144,15 +130,25 @@ class ChartView @JvmOverloads constructor(context: Context?, attrs: AttributeSet
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var needRedraw = false
|
||||||
if (type == ChartType.STACKED) {
|
if (type == ChartType.STACKED) {
|
||||||
for (serie in series.reversed()) {
|
for (serie in series.reversed()) {
|
||||||
serie.onDraw(canvas, rect)
|
val tmp = serie.onDraw(canvas, rect)
|
||||||
|
if (tmp) {
|
||||||
|
needRedraw = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for (serie in series) {
|
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)
|
super.onDraw(canvas)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,6 +6,8 @@ import com.dzeio.charts.series.SerieInterface
|
|||||||
|
|
||||||
interface ChartViewInterface {
|
interface ChartViewInterface {
|
||||||
|
|
||||||
|
val animator: Animation
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Chart Type
|
* Chart Type
|
||||||
*/
|
*/
|
||||||
|
@ -41,20 +41,45 @@ class BarSerie(
|
|||||||
|
|
||||||
private val rect = Rect()
|
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 displayedEntries = getDisplayedEntries()
|
||||||
val barWidth = view.xAxis.getEntryWidth(drawableSpace).toFloat()
|
val barWidth = view.xAxis.getEntryWidth(drawableSpace).toFloat()
|
||||||
|
|
||||||
val zero = view.yAxis.getPositionOnRect(0f, drawableSpace)
|
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) {
|
for (entry in displayedEntries) {
|
||||||
|
if (entriesCurrentY[entry.x] == null) {
|
||||||
|
entriesCurrentY[entry.x] = AnimationProgress(zero)
|
||||||
|
}
|
||||||
|
|
||||||
// calculated height in percent from 0 to 100
|
// 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(
|
var posX = drawableSpace.left + view.xAxis.getPositionOnRect(
|
||||||
entry,
|
entry,
|
||||||
drawableSpace
|
drawableSpace
|
||||||
).toFloat()
|
).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)
|
val right = (posX + barWidth).coerceAtMost(drawableSpace.right)
|
||||||
|
|
||||||
if (posX > right) {
|
if (posX > right) {
|
||||||
@ -134,6 +159,8 @@ class BarSerie(
|
|||||||
if (doDisplayIn) textPaint else textExternalPaint
|
if (doDisplayIn) textPaint else textExternalPaint
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return needUpdate
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun refresh() {
|
override fun refresh() {
|
||||||
|
@ -15,6 +15,11 @@ sealed class BaseSerie(
|
|||||||
const val TAG = "Charts/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 formatValue: (entry: Entry) -> String = { entry -> entry.y.roundToInt().toString()}
|
||||||
|
|
||||||
override var yAxisPosition: YAxisPosition = YAxisPosition.RIGHT
|
override var yAxisPosition: YAxisPosition = YAxisPosition.RIGHT
|
||||||
@ -31,7 +36,7 @@ sealed class BaseSerie(
|
|||||||
for (i in 0 until entries.size) {
|
for (i in 0 until entries.size) {
|
||||||
val it = entries[i]
|
val it = entries[i]
|
||||||
if (it.x in minX..maxX) {
|
if (it.x in minX..maxX) {
|
||||||
if (result.size === 0 && i > 0) {
|
if (result.size == 0 && i > 0) {
|
||||||
result.add((entries[i - 1]))
|
result.add((entries[i - 1]))
|
||||||
}
|
}
|
||||||
lastIndex = i
|
lastIndex = i
|
||||||
@ -43,8 +48,9 @@ sealed class BaseSerie(
|
|||||||
result.add(entries [lastIndex + 1])
|
result.add(entries [lastIndex + 1])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
result.sortBy { it.x }
|
||||||
return result
|
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
|
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()
|
val displayedEntries = getDisplayedEntries()
|
||||||
displayedEntries.sortBy { it.x }
|
|
||||||
|
|
||||||
var previousPosX: Float? = null
|
var previousPosX: Float? = null
|
||||||
var previousPosY: 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) {
|
for (entry in displayedEntries) {
|
||||||
|
if (entriesCurrentY[entry.x] == null) {
|
||||||
|
entriesCurrentY[entry.x] = AnimationProgress(zero)
|
||||||
|
}
|
||||||
|
|
||||||
// calculated height in percent from 0 to 100
|
// 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 +
|
val posX = (drawableSpace.left +
|
||||||
view.xAxis.getPositionOnRect(entry, drawableSpace) +
|
view.xAxis.getPositionOnRect(entry, drawableSpace) +
|
||||||
view.xAxis.getEntryWidth(drawableSpace) / 2f).toFloat()
|
view.xAxis.getEntryWidth(drawableSpace) / 2f).toFloat()
|
||||||
@ -65,6 +92,8 @@ class LineSerie(
|
|||||||
previousPosX = posX
|
previousPosX = posX
|
||||||
previousPosY = top
|
previousPosY = top
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return needUpdate
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun refresh() {
|
override fun refresh() {
|
||||||
|
@ -35,8 +35,9 @@ sealed interface SerieInterface {
|
|||||||
*
|
*
|
||||||
* @param canvas the canvas to draw on
|
* @param canvas the canvas to draw on
|
||||||
* @param drawableSpace the space you are allowed 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
|
* run when manually refreshing the system
|
||||||
|
@ -36,16 +36,42 @@ class MainFragment : Fragment() {
|
|||||||
val serie1 = BarSerie(this)
|
val serie1 = BarSerie(this)
|
||||||
val serie2 = BarSerie(this)
|
val serie2 = BarSerie(this)
|
||||||
|
|
||||||
|
animator.duration = 750
|
||||||
|
|
||||||
// transform the chart into a grouped chart
|
// transform the chart into a grouped chart
|
||||||
type = ChartType.STACKED
|
type = ChartType.GROUPED
|
||||||
|
yAxis.setYMin(0f)
|
||||||
|
|
||||||
// utils function to use Material3 auto colors
|
// utils function to use Material3 auto colors
|
||||||
materielTheme(this, requireView())
|
materielTheme(this, requireView())
|
||||||
serie2.barPaint.color = Color.RED
|
serie2.barPaint.color = Color.RED
|
||||||
|
|
||||||
// give the serie it's entries
|
// give the serie it's entries
|
||||||
serie1.entries = generateRandomDataset(1)
|
serie1.entries = generateRandomDataset(5)
|
||||||
serie2.entries = generateRandomDataset(1)
|
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 the Chart
|
||||||
refresh()
|
refresh()
|
||||||
@ -68,6 +94,7 @@ class MainFragment : Fragment() {
|
|||||||
binding.chartBar.apply {
|
binding.chartBar.apply {
|
||||||
// setup the Serie
|
// setup the Serie
|
||||||
val serie = BarSerie(this)
|
val serie = BarSerie(this)
|
||||||
|
yAxis.setYMin(0f)
|
||||||
|
|
||||||
// utils function to use Material3 auto colors
|
// utils function to use Material3 auto colors
|
||||||
materielTheme(this, requireView())
|
materielTheme(this, requireView())
|
||||||
|
@ -23,6 +23,11 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="200dp" />
|
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
|
<com.dzeio.charts.ChartView
|
||||||
android:id="@+id/chart_customization"
|
android:id="@+id/chart_customization"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user