Alqueraf
Alqueraf

Reputation: 1388

Jetpack Navigation Animation Complete Listener

I'm trying to get a callback, using the Jetpack Navigation library, when a new navigation event is completed in order to change the status bar color.

So far I've found navController.addOnDestinationChangedListener which notifies me when a new navigation starts but not when it completes, meaning that the enter/exit transitions have completed.

Is there any method to know when all navigation transitions have completed?

Upvotes: 15

Views: 2347

Answers (4)

A. Petrov
A. Petrov

Reputation: 998

The answer is simple as an "abc": For doing some job after transition ends, add a listener on a Fragment.View

For example, if we need to jump to another Destination only after current Fragment is drawn and presented on the screen, just place this hook in onResume callback:

override fun onResume() {
    super.onResume()
    view?.animation?.setAnimationListener(object: Animation.AnimationListener {
        override fun onAnimationRepeat(animation: Animation?) {}
        override fun onAnimationStart(animation: Animation?) {}
        override fun onAnimationEnd(animation: Animation?) {
            // Do the navigation or whatever you want
        }
    })
}

Upvotes: 0

kokoko
kokoko

Reputation: 3982

you can refer this answer to achieve it. briefly, as a work around, you can add some delay to your animation when transaction is started.

as mentioned in the answer; fragment replacement is starting when you click to "navigate to X fragment" button. so the delay is only in fragment transaction animation not in replacement

add delay to animation by adding android:startOffset="300" in your_anim_name.xml file on your anim folder which is under res folder.

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:ordering="together">
<alpha
    android:startOffset="300"
    android:fromAlpha="0.0"
    android:toAlpha="1.0"
    android:duration="400"
    android:interpolator="@android:interpolator/accelerate_decelerate"
    />
<translate
    android:startOffset="300"
    android:fromYDelta="100%"
    android:toYDelta="0%"
    android:duration="400"
    android:interpolator="@android:interpolator/accelerate_decelerate"
    />

Upvotes: 0

Victor Cold
Victor Cold

Reputation: 653

I faced the same problem and finally found a workaround.

The idea is that when the exit animation of the current fragment ends, onDestroyView of that fragment is called.

So as long as the duration of the enter and exit animations are the same, we can use this method as a callback.

class BaseFragment() {
    private var leavingDestination = false

    fun navigateNext() {
        leavingDestination = true
        findNavController().navigate(blaBlaBla)
    }

    fun onNavigationAnimationEnds() {
        TODO()
    }

    // We just need to add an extra check, to be sure that onNavigationAnimationEnds is not called on configuration change
    override fun onDestroyView() {
        if(leavingDestination) {
            onNavigationAnimationEnds()
            leavingDestination = false
        }
    }
}

I use the described approach to call some code in the back/exiting fragment. If you need a callback in the fragment you navigate to, you can just use shared ViewModel. Like the ActivityViewModel or NavGraph scoped ViewModel.

Something like this:

class FragmentA() {
    private val mainActivityViewModel by activityViewModels<MainActivityViewModel> { viewModelFactory }
    ...
    override fun onDestroyView() {
        if(leavingDestination) {
            mainActivityViewModel.onNavigationAnimationEnds()
            leavingDestination = false
        }
    }
}
...
class MainActivity() {
    private val _navigationAnimationEndsEvent = MutableLiveData(Event())
    val navigationAnimationEndsEvent: LiveData<Event>; get() = _navigationAnimationEndsEvent

    fun onNavigationAnimationEnds() {
        _navigationAnimationEndsEvent.value = Event()
    }
}
...
class FragmentB() {
    private val mainActivityViewModel by activityViewModels<MainActivityViewModel> { viewModelFactory }

    override fun onActivityCreated() {
        ...
        mainActivityViewModel.navigationAnimationEndsEvent.observe(viewLifecycleOwner, Observer {
            it.getContentIfNotHandled()?.let {
                onNavigationAnimationsEnds()
            }    
        })
    }

    private fun onNavigationAnimationEnds() {
        TODO()
    }    
}

Event class I used:

open class Event<out T>(private val content: T) {

    var hasBeenHandled = false; private set

    fun getContentIfNotHandled() = if (hasBeenHandled) null else {
        hasBeenHandled = true
        content
    }

    fun peekContent(): T = content
}

P.S.: I tried overriding onCreateAnimation/onCreateAnimator functions and add listeners to them, but they were never called.

Upvotes: -1

dmytro19
dmytro19

Reputation: 21

You can use something like this:

val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment)
navHostFragment?.childFragmentManager?.addOnBackStackChangedListener {
    val currentFragment = navHostFragment.childFragmentManager.fragments.firstOrNull()
    if (currentFragment is YourFragment) {
        // your code here  
    } 
}

Upvotes: 2

Related Questions