mirror of
https://github.com/dzeiocom/charts.git
synced 2025-04-22 02:32:10 +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
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
@ -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())
|
||||
|
@ -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"
|
||||
@ -41,4 +46,4 @@
|
||||
|
||||
</ScrollView>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
Loading…
x
Reference in New Issue
Block a user