Memory Leak in Custom Alert dialog animation Android

131 Views Asked by At

I'm using an alert dialog to show an animation (green tick mark on success api call). But it's causing a memory leak if I press the home button before the animation ends.

To reproduce, I enabled "Finish Activities" in Developer Options.

Attaching the code below for "Tick Animation" and the custom dialog box which shows that animation.

SuccessAnimation.kt

class SuccessAnimation @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null,
) : RelativeLayout(context, attrs) {
    private val circleFadeInTime: Long
    private val checkAnimationTime: Long
    val postAnimationTime: Long

    init {
        inflate(context, R.layout.success_content, this)
        val a = context.obtainStyledAttributes(attrs, R.styleable.SuccessAnimation, 0, 0)
        try {
            circleFadeInTime = a.getInteger(R.styleable.SuccessAnimation_circleAppearanceTime, DEFAULT_CIRCLE_TIME).toLong()
            checkAnimationTime = a.getInteger(R.styleable.SuccessAnimation_checkMarkAnimationTime, DEFAULT_ANIMATION_TIME).toLong()
            postAnimationTime = a.getInteger(R.styleable.SuccessAnimation_postAnimationTime, DEFAULT_POST_ANIMATION_TIME).toLong()
        } finally {
            a.recycle()
        }
        isClickable = true // Prevent anything else from happening!

    }

    val animationDuration = circleFadeInTime + checkAnimationTime + postAnimationTime
    private val circle: View = findViewById(R.id.green_circle)
    private val checkMark = findViewById<AnimatedCheckMark>(R.id.check_mark).apply { setAnimationTime(checkAnimationTime) }


    private var onAnimationFinished: (() -> Unit)? = {}

    /**
     * Set a callback to be invoked when the animation finishes
     * @param listener listener to be called
     */
    fun setSuccessFinishedListener(listener: (() -> Unit)?) {
        this.onAnimationFinished = listener
    }

    /**
     * start the animation, also handles making sure the view is visible.
     */
    fun show() {
        if (visibility != VISIBLE) {
            visibility = VISIBLE
            post { show() }
            return
        }
        circle.visibility = VISIBLE
        circle.scaleX = 0f
        circle.scaleY = 0f
        val animator = ValueAnimator.ofFloat(0f, 1f)
        animator.addUpdateListener { animation: ValueAnimator ->
            val scale = animation.animatedFraction
            circle.scaleY = scale
            circle.scaleX = scale
            circle.invalidate()
        }
        animator.duration = circleFadeInTime
        animator.interpolator = OvershootInterpolator()
        animator.addListener(object : AnimatorListenerAdapter() {
            override fun onAnimationEnd(animation: Animator) {
                checkMark!!.visibility = VISIBLE
                invalidate()
                checkMark.startAnimation { view: AnimatedCheckMark ->
                    view.postDelayed({
                        visibility = GONE
                        checkMark.visibility = INVISIBLE
                        circle.visibility = INVISIBLE
                        onAnimationFinished?.invoke()
                    }, postAnimationTime)
                }
            }
        })
        invalidate()
        animator.start()
    }
    companion object{
        private const val DEFAULT_CIRCLE_TIME = 300
        private const val DEFAULT_ANIMATION_TIME = 500
        private const val DEFAULT_POST_ANIMATION_TIME = 500

    }
}

SuccessAnimationPopup.kt

class SuccessAnimationPopup(context: Context,
                            private val callback: () -> Unit) :
    AlertDialog(context, android.R.style.Theme_Translucent_NoTitleBar) {
    init {
        window?.let {
            it.setFlags(
                WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION,
                WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION
            )
            it.setFlags(
                WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS,
                WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS
            )
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.success_animation_popup)
        setCancelable(false)

        val success = findViewById<SuccessAnimation>(R.id.success_animation)
        success?.setSuccessFinishedListener {
            dismiss()
            callback()
        }
        success?.show()
    }
}

It is being used like the following:

SuccessAnimationPopup(view.context) {}.show() I only have "view" here and not the activity.

Been trying to find the root cause. Have also added onDetachedFromWindow lifecycle callback in SuccessAnimation.kt and tried setting the onAnimationListener = null, still doesn't work.

What am I missing here?

Also, the AlertDialog constructor is not accepting a nullable context, hence I wasn't able to pass a WeakReference since it's nullable.

0

There are 0 best solutions below