I am showing a dots loader in my activity in Android using a Timer() object and passing an object of TimerTask() in the scheduleAtFixedRate() API of Timer.
This is my code snippet:-
private fun scheduleTimer() {
timer = Timer()
timer?.scheduleAtFixedRate(object : TimerTask() {
override fun run() {
if (isSingleDir) {
selectedDotPos++
if (selectedDotPos > noOfDots) {
selectedDotPos = 1
}
} else {
if (isFwdDir) {
selectedDotPos++
if (selectedDotPos == noOfDots) {
isFwdDir = !isFwdDir
}
} else {
selectedDotPos--
if (selectedDotPos == 1) {
isFwdDir = !isFwdDir
}
}
}
(scanForActivity(context))?.runOnUiThread {
invalidate()
}
}
}, 0, animDur.toLong())
}
I am making sure that to cancel the timer by calling Timer::cancel(), when the visibility of the view changes. This is the code:-
override fun onVisibilityChanged(changedView: View, visibility: Int) {
super.onVisibilityChanged(changedView, visibility)
if (visibility != VISIBLE) {
timer?.cancel()
} else if (shouldAnimate) {
scheduleTimer()
}
}
But the TimerTask is leaking some memory. I am using Leak Canary to analyze the memory leak. This is the report:-
Looking for help to resolve this.
Complete code of my class:-
class LinearDotsLoader : DotsLoaderBaseView
{
private var timer: Timer? = null
var isSingleDir = true
private var diffRadius: Int = 0
private var isFwdDir = true
constructor(context: Context) : super(context) {
initCordinates()
initPaints()
}
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
initAttributes(attrs)
initCordinates()
initPaints()
initShadowPaints()
}
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
initAttributes(attrs)
initCordinates()
initPaints()
initShadowPaints()
}
override fun initAttributes(attrs: AttributeSet) {
super.initAttributes(attrs)
val typedArray = context.obtainStyledAttributes(attrs, R.styleable.LinearDotsLoader, 0, 0)
this.noOfDots = typedArray.getInt(R.styleable.LinearDotsLoader_loader_noOfDots, 3)
this.selRadius = typedArray.getDimensionPixelSize(R.styleable.LinearDotsLoader_loader_selectedRadius, radius + 10)
this.dotsDistance = typedArray.getDimensionPixelSize(R.styleable.LinearDotsLoader_loader_dotsDist, 15)
this.isSingleDir = typedArray.getBoolean(R.styleable.LinearDotsLoader_loader_isSingleDir, false)
this.expandOnSelect = typedArray.getBoolean(R.styleable.LinearDotsLoader_loader_expandOnSelect, false)
typedArray.recycle()
}
override fun initCordinates() {
diffRadius = this.selRadius - radius
dotsXCorArr = FloatArray(this.noOfDots)
//init X cordinates for all dots
for (i in 0 until noOfDots) {
dotsXCorArr[i] = (i * dotsDistance + (i * 2 + 1) * radius).toFloat()
}
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val calWidth: Int
val calHeight: Int
if (expandOnSelect) {
calWidth = (2 * this.noOfDots * radius + (this.noOfDots - 1) * dotsDistance + 2 * diffRadius)
calHeight = 2 * this.selRadius
} else {
calHeight = 2 * radius
calWidth = (2 * this.noOfDots * radius + (this.noOfDots - 1) * dotsDistance)
}
setMeasuredDimension(calWidth, calHeight)
}
override fun onVisibilityChanged(changedView: View, visibility: Int) {
super.onVisibilityChanged(changedView, visibility)
if (visibility != VISIBLE) {
timer?.cancel()
timer?.purge()
} else if (shouldAnimate) {
scheduleTimer()
}
}
private fun scheduleTimer() {
timer = Timer()
val dotsTimerTask = DotsTimerTask()
timer?.scheduleAtFixedRate(dotsTimerTask, 0, animDur.toLong())
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
drawCircle(canvas)
}
private fun drawCircle(canvas: Canvas) {
for (i in 0 until noOfDots) {
var xCor = dotsXCorArr[i]
if (expandOnSelect) {
if (i + 1 == selectedDotPos) {
xCor += diffRadius.toFloat()
} else if (i + 1 > selectedDotPos) {
xCor += (2 * diffRadius).toFloat()
}
}
var firstShadowPos: Int
var secondShadowPos: Int
if ((isFwdDir && selectedDotPos > 1) || selectedDotPos == noOfDots) {
firstShadowPos = selectedDotPos - 1
secondShadowPos = firstShadowPos - 1
} else {
firstShadowPos = selectedDotPos + 1
secondShadowPos = firstShadowPos + 1
}
if (i + 1 == selectedDotPos) {
selectedCirclePaint?.let {
canvas.drawCircle(
xCor,
(if (expandOnSelect) this.selRadius else radius).toFloat(),
(if (expandOnSelect) this.selRadius else radius).toFloat(),
it
)
}
} else if (showRunningShadow && i + 1 == firstShadowPos) {
canvas.drawCircle(
xCor,
(if (expandOnSelect) this.selRadius else radius).toFloat(),
radius.toFloat(),
firstShadowPaint)
} else if (showRunningShadow && i + 1 == secondShadowPos) {
canvas.drawCircle(
xCor,
(if (expandOnSelect) this.selRadius else radius).toFloat(),
radius.toFloat(),
secondShadowPaint)
} else {
defaultCirclePaint?.let {
canvas.drawCircle(
xCor,
(if (expandOnSelect) this.selRadius else radius).toFloat(),
radius.toFloat(),
it
)
}
}
}
}
var dotsDistance: Int = 15
set(value) {
field = value
initCordinates()
}
var noOfDots: Int = 3
set(noOfDots) {
field = noOfDots
initCordinates()
}
var selRadius: Int = 38
set(selRadius) {
field = selRadius
initCordinates()
}
var expandOnSelect: Boolean = false
set(expandOnSelect) {
field = expandOnSelect
initCordinates()
}
private fun scanForActivity(context: Context?): Activity? {
return when (context) {
null -> null
is Activity -> context
is ContextWrapper -> scanForActivity(context.baseContext)
else -> null
}
}
private fun updateSelectedDot()
{
if (isSingleDir) {
selectedDotPos++
if (selectedDotPos > noOfDots) {
selectedDotPos = 1
}
} else {
if (isFwdDir) {
selectedDotPos++
if (selectedDotPos == noOfDots) {
isFwdDir = !isFwdDir
}
} else {
selectedDotPos--
if (selectedDotPos == 1) {
isFwdDir = !isFwdDir
}
}
}
(scanForActivity(context))?.runOnUiThread {
invalidate()
}
}
private inner class DotsTimerTask: TimerTask(){
override fun run() {
updateSelectedDot()
}
}
}

The answer is to fix racing between cancelling and instantiate/schedule Timer.
To add cancel before each Timer instantiation and starting should be enough.
I opened issue on repo of this code: https://github.com/agrawalsuneet/DotLoadersPack-Android/issues/44