183 lines
5.8 KiB
Kotlin
183 lines
5.8 KiB
Kotlin
package io.xdrm.lebonprix.anim
|
|
|
|
import android.animation.ValueAnimator
|
|
import android.content.Context
|
|
import android.graphics.*
|
|
import android.graphics.drawable.Drawable
|
|
import android.support.v4.content.res.ResourcesCompat
|
|
import android.util.Log
|
|
import android.view.animation.DecelerateInterpolator
|
|
import io.xdrm.lebonprix.R
|
|
import kotlin.math.*
|
|
|
|
class BarChart(
|
|
data : Array<Int>,
|
|
private var originalWidth : Int,
|
|
private val originalHeight : Int,
|
|
val ctx: Context
|
|
) : Drawable() {
|
|
|
|
// PARAMS
|
|
private val relativeMinBarSize = 0.01F
|
|
private val relativeSideMargin = 0.1F // only 1 side
|
|
private val relativeSpaceWidth = 1F // relative to bar size
|
|
|
|
// fixed/processed values
|
|
private val minReadableSlotSize = 90F
|
|
private val textDistance = 120F
|
|
private val fontSize = 32F
|
|
private val sideMargin = originalWidth * 0.5F * relativeSideMargin
|
|
private val width = originalWidth - 2F*sideMargin
|
|
private val minBarSize = relativeMinBarSize * width
|
|
private val minSlotSize = minBarSize * (1F + relativeSpaceWidth)
|
|
// available width / (size of minimum bar + space) - ending space
|
|
// private val maxSlotCount = width / (minBarSize * (1F+relativeSpaceWidth) - minBarSize*relativeSpaceWidth)
|
|
|
|
// calculated on the run
|
|
private var barSize: Float
|
|
private var slotSize: Float
|
|
|
|
|
|
// animation
|
|
private val animationDuration = 500L
|
|
private var animatedHeight = 0
|
|
private val animator = ValueAnimator.ofInt(0, originalHeight)
|
|
|
|
// price -> count
|
|
private val distributions = arrayListOf<Pair<Float,Int>>()
|
|
|
|
init{
|
|
require( data.isNotEmpty() )
|
|
|
|
// get maximum number of bars
|
|
val barCount = data.distinct().size
|
|
val minPrice = data.min()!!.toFloat()
|
|
val maxPrice = data.max()!!.toFloat()
|
|
|
|
// defaults
|
|
slotSize = max(width / barCount, minSlotSize )
|
|
var slotCount = ( width / slotSize ).toInt()
|
|
// avail width / (elemts + 1 + 1 end space)
|
|
slotSize = width / (slotCount+1F-0.5F)
|
|
|
|
|
|
barSize = slotSize / (1F+relativeSpaceWidth)
|
|
var step = ( maxPrice - minPrice ) / slotCount.toFloat()
|
|
|
|
// move between min and max incrementing by 'step' every time
|
|
var slot = minPrice
|
|
var sum = 0
|
|
while( slot <= maxPrice ){
|
|
val min = slot - step*0.5F
|
|
val max = slot + step*0.5F
|
|
|
|
data.forEach {
|
|
// first slot -> inclusive min
|
|
if( slot <= minPrice && it >= min && it <= max )
|
|
sum++
|
|
|
|
else if( it > min && it <= max )
|
|
sum++
|
|
}
|
|
|
|
distributions.add( Pair(slot, sum) )
|
|
sum = 0
|
|
slot += step
|
|
}
|
|
|
|
/**
|
|
* SETUP ANIMATION
|
|
*/
|
|
animator.duration = animationDuration
|
|
animator.interpolator = DecelerateInterpolator()
|
|
animator.addUpdateListener {
|
|
animatedHeight = it.animatedValue as Int
|
|
invalidateSelf()
|
|
}
|
|
animator.start()
|
|
}
|
|
|
|
override fun draw(canvas: Canvas) {
|
|
//create base color (roboto light with gradient)
|
|
val gradientPaint = Paint()
|
|
gradientPaint.shader = LinearGradient(0F,0F, originalWidth.toFloat(), animatedHeight.toFloat(), 0xff71f0b5.toInt(), 0xff3e91e3.toInt(), Shader.TileMode.CLAMP)
|
|
|
|
val textColor = Paint()
|
|
textColor.typeface = Typeface.create("sans-serif-light", Typeface.NORMAL)
|
|
textColor.textSize = this.fontSize
|
|
textColor.textAlign = Paint.Align.CENTER
|
|
textColor.color = Color.WHITE
|
|
textColor.flags = Paint.ANTI_ALIAS_FLAG
|
|
|
|
// text background
|
|
val bg = Paint()
|
|
bg.color = ResourcesCompat.getColor(ctx.resources, R.color.colorBackground, null)
|
|
bg.flags = Paint.ANTI_ALIAS_FLAG
|
|
|
|
canvas.drawRoundRect(
|
|
RectF(-50F, originalHeight-textDistance+10F, originalWidth+50F, originalHeight+textDistance+1F),
|
|
100F, 100F,
|
|
bg
|
|
)
|
|
|
|
drawText(canvas, textColor)
|
|
|
|
drawBars(canvas, gradientPaint)
|
|
|
|
setBounds(0,0,originalWidth,animatedHeight)
|
|
}
|
|
|
|
private fun drawText(canvas: Canvas, paint: Paint){
|
|
distributions.forEachIndexed { i, value ->
|
|
|
|
// get mod where text is printed 1 over mod times (exceptions: start, end)
|
|
val mod = max(1, (minReadableSlotSize / slotSize).toInt())
|
|
|
|
if( i == 0 || i == distributions.size-1 || i % mod == 0 ){
|
|
|
|
val x = sideMargin + i.toFloat() * slotSize + barSize/2F
|
|
val y = originalHeight.toFloat()
|
|
var text = "${ round(value.first) }"
|
|
if( text == "${value.first.toInt()}.0" )
|
|
text = "${ value.first.toInt() }"
|
|
|
|
|
|
//save canvas, rotate, translate , draw, then restore to previous state
|
|
canvas.save()
|
|
canvas.rotate(-45F,x,y- textDistance*0.4F)
|
|
canvas.drawText(text,x,y - textDistance*0.4F, paint)
|
|
canvas.restore()
|
|
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
private fun drawBars(canvas: Canvas, paint: Paint){
|
|
val maxCount = distributions.flatMap { listOf(it.second.toFloat()) }.max()!!
|
|
val maxAnimatedHeight = animatedHeight - textDistance
|
|
|
|
distributions.forEachIndexed { i, d ->
|
|
|
|
// process coordinates
|
|
val x = sideMargin + i.toFloat() * slotSize
|
|
|
|
val height = d.second.toFloat() / maxCount
|
|
val y = maxAnimatedHeight * (1 - height)
|
|
|
|
canvas.drawRoundRect(
|
|
RectF(x, y, x+barSize, originalHeight.toFloat()-textDistance),
|
|
barSize, barSize,
|
|
paint)
|
|
|
|
}
|
|
}
|
|
|
|
override fun setAlpha(alpha: Int) {}
|
|
|
|
override fun getOpacity(): Int {
|
|
return PixelFormat.OPAQUE
|
|
}
|
|
|
|
override fun setColorFilter(colorFilter: ColorFilter?) {}
|
|
} |