ValueAnimator not being cancelled when called in onAnimationEnd - android

I have an App with functionality such as Instagram Stories. I have 5 bars that are 1 by 1 being filled to indicate the length of the movie. This works fine, also when the video start playing again the bars get resetted.
But when I switch to the next video and I was in bar3 with the previous video, then bar0 starts (as expected) but also bar4 starts. Switching to a new video triggers onAnimationEnd and I want the animation to be cancelled so that I can reload a fresh progress indication. cancel(), end(), removeListener(this), they all don't work.
fun startAnimation(duration: Long, progressBars: ArrayList<ProgressBar>, currentBar: Int) {
Timber.d("test currentBar = $currentBar")
if (currentBar == 0) {
for (bar in progressBars) {
bar.setProgress(0)
}
}
animator?.end()
animator?.cancel()
animator?.removeAllListeners()
animator = null
animator = ValueAnimator.ofInt(0, progressBars[currentBar].getMax())
animator?.duration = duration
animator?.addUpdateListener { animation ->
progressBars[currentBar].setProgress(animation.animatedValue as Int)
}
animator?.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
super.onAnimationEnd(animation)
// animation.removeListener(this)
animation.cancel()
animation.end()
var nextBar = currentBar + 1
if (nextBar == progressBars.size) {
nextBar = 0
}
Timber.d("test nextBar = $nextBar")
startAnimation(duration, progressBars, nextBar)
}
})
animator?.start()
}

The first thing is; cancel() and end() methods are actually calling the onAnimationEnd() method. So, calling them inside of the onAnimationEnd() method is not helpful.
What you should have is a way to stop the already started animation series. Because it will keep starting the next animation because of the recursion.
Here is the code we want (or something prettier);
lateinit currentViewBarAnimation: CancelableAnimation
fun onCreate(){
currentViewBarAnimation = startAnimation(duration, progressBars, 0)
}
fun onNextVideoStarted(){
currentViewBarAnimation.stopOrCancelOrWhateverYouNameIt()
currentViewBarAnimation = startAnimation(duration, progressBars, 0)
}
CancellableAnimation can be a class that just has a boolean.
class CancelableAnimation(private var cancelled: Boolean = false){
fun cancel(){
cancelled = true
}
fun isCancelled() = cancelled
}
Now the final touch;
fun startAnimation(duration: Long, progressBars: ArrayList<ProgressBar>, currentBar: Int) : CancellableAnimation{
Timber.d("test currentBar = $currentBar")
if (currentBar == 0) {
for (bar in progressBars) {
bar.setProgress(0)
}
}
animator?.end()
animator?.cancel()
animator?.removeAllListeners()
animator = ValueAnimator.ofInt(0, progressBars[currentBar].getMax())
animator?.duration = duration
animator?.addUpdateListener { animation ->
progressBars[currentBar].setProgress(animation.animatedValue as Int)
}
val cancellableAnimation = CancellableAnimation()
animator?.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
super.onAnimationEnd(animation)
if(cancellableAnimation.isCancelled()) return
var nextBar = currentBar + 1
if (nextBar == progressBars.size) {
nextBar = 0
}
Timber.d("test nextBar = $nextBar")
startAnimation(duration, progressBars, nextBar)
}
})
animator?.start()
return cancellableAnimation
}
It looks a little confusing but it is the way to go since moving to a new video is an async action.

Related

Memory Leak in Custom Alert dialog animation Android

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.

Increase value of text when button is hold

I have a plus and min button that work when pressed. Now I want to make them when you hold/press it down it goes up/down more then 1 at a time.
This is one of my regular buttons:
plusBtn.setOnClickListener {
if(isEmpty(PenaltyTimeInputTxt.text))
{
PenaltyTimeInputTxt.setText("0")
}
penaltyInput = PenaltyTimeInputTxt.text.toString().toInt()
if(penaltyInput < 99){
penaltyInput++
PenaltyTimeInputTxt.setText(penaltyInput.toString())
}
else {
Toast.makeText(this, "Penalty time cannot go over 99", Toast.LENGTH_SHORT).show()
}
}
is there a simple way of doing this? I saw something about onTouchListener.
EDIT ---
End result. Thanks to Tenfour04: include the whole fun view.doWhileHeld + this:
plusBtn.doWhileHeld(this.lifecycleScope) {
if(isEmpty(PenaltyTimeInputTxt.text)) {
PenaltyTimeInputTxt.setText("0")
}
penaltyInput = PenaltyTimeInputTxt.text.toString().toInt()
while (isActive) {
if(penaltyInput < 99) {
penaltyInput++
PenaltyTimeInputTxt.setText(penaltyInput.toString())
}
else {
Toast.makeText(this#PenaltyConfigureActivity, "Penalty time cannot go over 99", Toast.LENGTH_SHORT).show()
break
}
delay(200)
}
}
Here is a helper class and function for this, which lets you do whatever you want while the button is held down:
fun View.doWhileHeld(
coroutineScope: CoroutineScope,
block: suspend CoroutineScope.() -> Unit
) = setOnTouchListener(object : View.OnTouchListener {
var job: Job? = null
var pointerInBounds = false
#SuppressLint("ClickableViewAccessibility")
override fun onTouch(view: View, event: MotionEvent): Boolean {
if (!isEnabled) {
job?.cancel()
return false
}
when (event.action) {
MotionEvent.ACTION_DOWN -> {
job = coroutineScope.launch(block = block)
pointerInBounds = true
}
MotionEvent.ACTION_MOVE -> {
val movedInBounds = event.x.roundToInt() in 0..view.width
&& event.y.roundToInt() in 0..view.height
if (pointerInBounds != movedInBounds) {
pointerInBounds = movedInBounds
if (pointerInBounds) {
job = coroutineScope.launch(block = block)
} else {
job?.cancel()
}
}
}
MotionEvent.ACTION_UP -> {
job?.cancel()
}
}
return false // allow click interactions
}
})
It runs a coroutine that restarts every time you click and hold. It also stops the coroutine and restarts it if you drag off the button and then back on, which is a conventional UI behavior.
To use it for your behavior, you can use a while loop:
plusBtn.doWhileHeld(viewLifecycleOwner.lifecycleScope) {
if(isEmpty(PenaltyTimeInputTxt.text)) {
PenaltyTimeInputTxt.setText("0")
}
penaltyInput = PenaltyTimeInputTxt.text.toString().toInt()
while (isActive) {
if(penaltyInput < 99) {
penaltyInput++
PenaltyTimeInputTxt.setText(penaltyInput.toString())
if (penaltyInput == 99) { // optional, might be nicer than showing toast
plusBtn.isEnabled = false
break
}
}
else {
Toast.makeText(this, "Penalty time cannot go over 99", Toast.LENGTH_SHORT).show()
break
}
delay(500) // adjust for how fast to increment the value
}
}
try below code may help
plusBtn.setOnTouchListener { _, event ->
if (event.action == MotionEvent.ACTION_DOWN) {
// button pressed
object : CountDownTimer(99000, 1000) {
override fun onTick(millisUntilFinished: Long) {
val temp = 99000 - (millisUntilFinished / 1000)
PenaltyTimeInputTxt.setText(""+temp)
}
override fun onFinish() {
Toast.makeText(this, "Penalty time cannot go over 99", Toast.LENGTH_SHORT).show()
}
}.start()
}
if (event.action == MotionEvent.ACTION_UP) {
// button released
}
true
}

ExoPlayer2 how to loop a part / segment of a video

I have a video of 10 seconds. I want to loop a segment from 2 seconds to 6 seconds. Starting the player at the right time is easy:
player?.seekTo(2000)
I don't think there is a functionality available in the ExoPlayer2 library to define an end position. So I tried to add a delay co-routine. With the method seekToPositionAndStartCounter. This works in a separate project. But used in the actual project, onPlayerStateChanged gets triggered a lot of times all of a sudden.
var elapsedTime = 0L // just for testing
private fun showVideoWhenDoneLoading() {
videoView?.player?.addListener(object : Player.EventListener {
override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
super.onPlayerStateChanged(playWhenReady, playbackState)
if (playbackState == Player.STATE_READY) {
if (playWhenReady) {
seekToPositionAndStartCounter()
} else {
cancelCounter()
}
}
}
})
}
private fun seekToPositionAndStartCounter() {
elapsedTime = System.currentTimeMillis()
cancelCounter()
job = GlobalScope.launch(Dispatchers.IO) {
Timber.d("starting Thread name = ${Thread.currentThread().name}")
player?.seekTo(startPosition)
if (duration != 0L) {
runBlocking {
delay(duration)
}
GlobalScope.launch(Dispatchers.Main) {
Timber.d("duration = $duration elapsedTime = ${System.currentTimeMillis()-elapsedTime} Thread name = ${Thread.currentThread().name}")
seekToPositionAndStartCounter()
}
}
}
}
private fun cancelCounter() {
job?.cancel()
job = null
}
If you don't try this then think about this.
getCurrentPosition of player and if player reach end position which you want then run player.seekto(2) it loops the video in specific segment of video.

How to make my progress bar animation smoother?

My progress bar is implemented to when the button is pressed down, the progress bar increments. When the button is released, the progress bar resets. The progress bar right now is very glitchy looking. I think it is because it gets called every second and so it jumps like that.
btn.setOnTouchListener{ view, motionEvent ->
Toast.makeText(context, "Long click detected", Toast.LENGTH_SHORT).show()
progressBar.visibility = View.VISIBLE
var i = 0
progressBar.progress = i
val countdownTimer = object: CountDownTimer(5000L, 500L){
override fun onTick(p0: Long) {
Log.d(TAG,"button up")
Log.d(TAG, "seconds: $p0")
if(motionEvent.action == MotionEvent.ACTION_UP){
i = 0
this.cancel()
}else{
i++
progressBar.progress = i*100/(5000/1000)
}
}
override fun onFinish() {
Log.d(TAG, "timer finished")
}
}.start()
true
}
Solution is to just decrease the time interval for the CountDownTimer so that the progress bar will update more frequently and progress smoother.
btn.setOnTouchListener{ view, motionEvent ->
progressBar.visibility = View.VISIBLE
progressBar.progress = 0
if(!isTimerRunning) {
object : CountDownTimer(5000L, 50L) {
override fun onTick(p0: Long) {
if (motionEvent.action == MotionEvent.ACTION_UP) {
this.cancel()
} else {
val progress = 100 - ((p0.toFloat() / 5000f) * 100f).toInt()
progressBar.progress = progress
}
}
override fun onFinish() {
progressBar.progress = 100
Log.d(TAG, "timer finished")
}
}.start()
}
true
}
I recommend using the built-in animation utilities to do this. They will let you specify whatever total duration you want, and take care of computing the right "tick" times and rates. You'll get smooth updates without having to do much manual work.
val animator = ObjectAnimator.ofInt(progressBar, "progress", 0, 100)
animator.interpolator = LinearInterpolator()
animator.duration = 5_000 // milliseconds
btn.setOnTouchListener { _, event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> {
animator.start()
true
}
MotionEvent.ACTION_UP -> {
animator.cancel()
progressBar.progress = 0
true
}
else -> false
}
}
This framework will also let you execute code when the progress is finished (or other events, like on animation start):
animator.addListener(object: AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator?) {
Toast.makeText(context, "done", Toast.LENGTH_SHORT).show()
}
})

Android AsyncTask is not updating Progress Bar

Hello I have a problem with asynctask.I play a song then I update duration to progressbar. But when I play a new song progressbar don't back to 0th position and progressbar is continuing with old value
Here is my code:
class Task(context: Context, progressBar: ProgressBar) : AsyncTask<Int, Int, String>() {
#SuppressLint("StaticFieldLeak")
private var progressBar: ProgressBar? = progressBar
private var count = 0
override fun doInBackground(vararg input: Int?): String {
while (count <= input[0]!!) {
count++
publishProgress(count)
Thread.sleep(1000)
if (isCancelled){
count=0
}
}
return "Task completed"
}
override fun onPreExecute() {
progressBar!!.progress = 0
}
override fun onProgressUpdate(vararg values: Int?) {
progressBar!!.progress = values[0]!!
}
}
when I play song :
override fun onItemClicked(position: Int, song: Song) {
val secondsDuration = song.duration!! / 1000
activity!!.pgbSong.max = secondsDuration
val task = Task(context!!, activity!!.pgbSong)
if (task.status == AsyncTask.Status.RUNNING) {
task.cancel(true)
}
task.execute(song.duration)
}
Well, what to say - you never cancel previous async tasks. Cause you're calling cancel(true) on just created async tasks every time:
val task = Task(context!!, activity!!.pgbSong)
if (task.status == AsyncTask.Status.RUNNING) {
task.cancel(true)
}
task.execute(song.duration)
Instead, you should save previous async task in an object variable (something like this):
private var asyncTask : AsyncTask<*,*,*>? = null
And after in the method call:
override fun onItemClicked(position: Int, song: Song) {
if (asyncTask.status == AsyncTask.Status.RUNNING) {
asyncTask.cancel(true)
}
val secondsDuration = song.duration!! / 1000
activity!!.pgbSong.max = secondsDuration
asyncTask = Task(context!!, activity!!.pgbSong)
asyncTask.execute(song.duration)
}
And, I guess, you should do a return in an AsyncTask when you're checking if it canceled or not.
But please don't use AsyncTask in this manner. Cause it holds links views and activity which can prevent those of being garbage collected and so cause a memory leak.
And please don't use !! with Kotlin. Instead use null check or provide default value if null. Examples:
val int = object?.int ?: 0
val context = activity ?: return
val view = activity?.pgbSong ?: return // or if (view != null)

Categories

Resources