androidkotlintransitiondrawablegradientdrawable

Transitioning a GradientDrawable


I have a function that creates a gradient across the background of my MainActivity. The gradient is created programmatically as a GradientDrawable. Certain interactions in the App change the color of the gradient.

The gradient and colors are properly shown, and they do change accordingly, however, I would like a transition effect on the color changes. Right now, the colors transition instantly with no crossfade.

fun createGradient() {
    gd.shape = GradientDrawable.RECTANGLE
    mainActivityID.background = gd
    gd.gradientType = GradientDrawable.LINEAR_GRADIENT
    gd.orientation = (GradientDrawable.Orientation.BL_TR)
}

Based on interactions in the App, I use a when statement to change the colors of the GradientDrawable

fun changeGradient(){

    //val td = gd as TransitionDrawable
    //td.isCrossFadeEnabled = true
    //td.startTransition(1500)

    when(calories){
        in 0..50 -> gd.colors = intArrayOf(
            ContextCompat.getColor(this, R.color.dBlue),
            ContextCompat.getColor(this, R.color.lBlue)
        )
        in 51..100 -> gd.colors = intArrayOf(
            ContextCompat.getColor(this, R.color.dRed),
            ContextCompat.getColor(this, R.color.lRed)
        )
        // etc...
    }
}

I tried casting the GradientDrawable as a TransitionDrawable (as seen in the commented code above), but that crashes with:

java.lang.ClassCastException: android.graphics.drawable.GradientDrawable cannot be cast to android.graphics.drawable.TransitionDrawable

How do I add a 1500 millisecond transition between the GradientDrawable color changes?

Progress crashes with java.lang.NegativeArraySizeException: -16777216

when(calories){
    in 0..50 -> animateGradient(gd, IntArray(Color.BLACK), IntArray(Color.CYAN))
    in 51..100 -> animateGradient(gd, IntArray(Color.RED), IntArray(Color.BLUE))
    in 101..150 -> animateGradient(gd, IntArray(Color.GREEN), IntArray(Color.YELLOW))
    // etc... about 15 more statements.
}

Solution

  • I don't think gradient drawable has built in transition methods, but You can run your own value animator to perform color transition.

    var anim : Animator? = null
    
    fun animateGradient(gd: GradientDrawable, from : IntArray, to: IntArray){
        require(from.size == to.size)
        anim?.cancel()
        val arraySize = from.size
        val props = Array<PropertyValuesHolder>(arraySize){
            PropertyValuesHolder.ofObject(it.toString(), ArgbEvaluator(), from[it], to[it])
        }
        val anim = ValueAnimator.ofPropertyValuesHolder(*props)
        anim.addUpdateListener {
            gd.colors = IntArray(arraySize){i ->
                it.getAnimatedValue(i.toString()) as Int
            }
        }
        anim.duration = 1500
        anim.start()
        this.anim = anim
    }
    

    Edit: modified method so it that tracks current color itself, reduced number of arguments.

    var anim : Animator? = null
    // variable that tracks current color - must be initialized with default gradient colors
    var currentGradient = intArrayOf(
            ContextCompat.getColor(this, R.color.dBlue),
            ContextCompat.getColor(this, R.color.lBlue)
        )
    
    fun animateGradient(targetColors: IntArray){
        val from = currentGradient
        require(from.size == targetColors.size)
        anim?.cancel()
        val arraySize = from.size
        val props = Array<PropertyValuesHolder>(arraySize){
            PropertyValuesHolder.ofObject(it.toString(), ArgbEvaluator(), from[it], targetColors[it])
        }
        val anim = ValueAnimator.ofPropertyValuesHolder(*props)
        anim.addUpdateListener {valueAnim ->
            IntArray(arraySize){i ->
                valueAnim.getAnimatedValue(i.toString()) as Int
            }.let{
                currentGradient = it
                gd.colors = it
            }
        }
        anim.duration = 1500
        anim.start()
        this.anim = anim
    }
    

    Call site:

    when(calories){
        in 0..50 -> animateGradient(intArrayOf(
            ContextCompat.getColor(this, R.color.dBlue),
            ContextCompat.getColor(this, R.color.lBlue)
        ))
        in 51..100 -> animateGradient(intArrayOf(
            ContextCompat.getColor(this, R.color.dRed),
            ContextCompat.getColor(this, R.color.lRed)
        ))
        // etc...
    }