Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
package com.levelfillingvectordrawable
import android.content.res.Resources
import android.graphics.*
import android.graphics.drawable.DrawableWrapper
import android.graphics.drawable.VectorDrawable
import androidx.annotation.DrawableRes
import androidx.core.graphics.withSave
import com.sdsmdg.harjot.vectormaster.models.VectorModel
import java.lang.reflect.Field
import kotlin.math.abs
/**
* A Drawable that modifies a vector drawable by drawing a 'fill' controlled by the `level`
* starting at the bottom of the drawable, clipped by a path component of the vector.
* The vector can outlined or filled vector, doesn't really matter, but you may wish to control drawing order.
* You can change the draw order with fillOnTop.
* You can specify a specific path in the vector by name to fill.
* You can also provide a list of tint colors as an INCREASING list of Pairs <min level, color int>
* Theme integration is not supported.
*/
class LevelFillingVectorDrawable(res: Resources,
@DrawableRes id: Int,
fillablePathname: String? = null)
: DrawableWrapper(res.getDrawable(id, null))
{
/**
* Additional configurations
*/
@Suppress("MemberVisibilityCanBePrivate")
var fillAlphaMultipler = 1f
@Suppress("MemberVisibilityCanBePrivate")
var fillOnTop: Boolean = false
@Suppress("MemberVisibilityCanBePrivate")
val fillPaint: Paint = Paint()
/**
* Don't provide too many tints, tint lookup is currently O(n)
*/
var levelTints: Iterable<Pair<Int, Int>> = emptyList()
set(value) {
field = value
updateTint()
}
private val vectorModel: VectorModel
private var transformedPath = Path()
private val reflTintFilterField: Field
private val fillRect = RectF()
// Set the maximum level, at which this drawable will be drawn as completely filled.
// Note, the Android maximum level is 10000. You may set it below this if you wish.
var filledLevel = 100
init {
fillPaint.color = Color.BLACK
vectorModel = VectorMaster.buildVectorModel(res.getXml(id), false)
val density = res.displayMetrics.density
val fillablePath = fillablePathname?.let {
vectorModel.pathModels.first {it.name == fillablePathname}?.path
} ?: vectorModel.fullpath
fillablePath.transform(Matrix().apply {setScale(density, density)}, transformedPath)
reflTintFilterField = VectorDrawable::class.java.getDeclaredField("mTintFilter")
reflTintFilterField.isAccessible = true
}
override fun onLevelChange(level: Int): Boolean {
super.onLevelChange(level)
updateFillRect()
updateTint()
return true
}
override fun onBoundsChange(bounds: Rect) {
super.onBoundsChange(bounds)
updateFillRect()
}
private fun updateTint() {
val tint = levelTints.lastOrNull { (minLevel, _) ->
minLevel <= this.level
}?.second
tint?.let {
this.setTint(tint)
}
}
private fun updateFillRect() {
// filledLevel = 0: top<-bottom; filledLevel = 100: no change
val fillRatio: Float = Math.min(this.level, filledLevel)/filledLevel.toFloat()
val fillTop = bounds.bottom - fillRatio*abs(bounds.top - bounds.bottom) // move up from bottom
fillRect.set(bounds.left + 0f, fillTop, bounds.right + 0f, bounds.bottom + 0f)
invalidateSelf()
}
// Draw: draw the parent. Based on current level, add a fill clipped by the fillable Path
override fun draw(canvas: Canvas) {
if (fillOnTop) {
super.draw(canvas)
}
val tintFilter = reflTintFilterField.get(this.drawable)
val clearFilter =
if (tintFilter != null && tintFilter is ColorFilter && fillPaint.colorFilter == null) {
fillPaint.colorFilter = tintFilter
true
}
else false
canvas.withSave {
fillPaint.alpha = (fillAlphaMultipler * this@LevelFillingVectorDrawable.alpha).toInt()
canvas.clipPath(transformedPath)
canvas.drawRect(fillRect, fillPaint)
}
if (clearFilter)
fillPaint.colorFilter = null
if (!fillOnTop) {
super.draw(canvas)
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment