Using navigation components, I would like apply fade-in effect to FadeInContent during transition, respecting the following order:
Text1 -> Text2 transition (Done by applying R.transition.move in sharedElement)
FadeInContent fades-in after 1. transition
I had a look to this article that does exactly what I want, but doesn't use navigation components
https://medium.com/bynder-tech/how-to-use-material-transitions-in-fragment-transactions-5a62b9d0b26b, therefore I can't apply setStartDelay. I can't also apply NavOptions.Builder().setEnterAnim(R.anim.fade_in) because it applies to all the screen, and not just the FadeInContent.
AFAIK the navigation component can only handle the motion during the transition itself so you are rightly pointing out that there is no way to delay a transition.
Nonetheless, you might want to implement your fade-in animation with a scene transition (https://developer.android.com/training/transitions).
It looks like a cleaner way to handle the situation you are exposing.
Code Solution:
val transition = TransitionInflater.from(activity)
.inflateTransition(android.R.transition.move)
sharedElementEnterTransition = transition
setEnterSharedElementCallback(object : SharedElementCallback() {
override fun onMapSharedElements(
names: MutableList<String>?,
sharedElements: MutableMap<String, View>?
) {
super.onMapSharedElements(names, sharedElements)
fadeInContainer.loadAnimation(
activity,
R.anim.fade_in
)
}
})
Code Solution:
val transition = TransitionInflater.from(activity)
.inflateTransition(android.R.transition.move)
sharedElementEnterTransition = transition
setEnterSharedElementCallback(object : SharedElementCallback() {
override fun onMapSharedElements(
names: MutableList<String>?,
sharedElements: MutableMap<String, View>?
) {
super.onMapSharedElements(names, sharedElements)
fadeInContainer.loadAnimation(
activity,
R.anim.fade_in
)
}
})
Related
One can create a EnterTransition in jetpack compose by concatenating various types of transitions like slideIn() + fadeIn() etc. which then constructs the EnterTransition which contains all the transitions in a TransitionData object.
But the problem is that the TransitionData property inside the EnterTransition is marked as internal. I want to animate properties in the graphics layer such as alpha and translationX based on what transition are available.
Is there any other way to get the all the different types of transitions defined in a EnterTransition like this:
fun createAnimation(
enter: EnterTransition = slideInHorizontaly() + fadeIn()
) {
val fade = enter.data.fade ?: defaultFadeIn // not possible: data is internal
val slide = enter.data.slide ?: defaultSlideIn // not possible: data is internal
...
}
I'm using the following sippet of code to navigate from a composable to another one, but it has a default fade animation. How can I remove it? I tried using an empty anim resource but it doesn't work.
navHostController.navigate(
"destination_route",
navOptions {
popUpTo("this_route") {
inclusive = true
}
anim {
enter = R.anim.empty_animation
exit = R.anim.empty_animation
popEnter = R.anim.empty_animation
popExit = R.anim.empty_animation
}
}
)
R.anim.empty_animation:
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<!--Empty to disable animation-->
</set>
As of right now, as EpicPandaForce said it is not possible, but that is because it is in the works!
Currently this functionality is served under accompanist, in the accompanist-navigation-animation artifact. You can read more about it here or in a more detailed blogpost here where they talk a bit about the future of it too.
The gist of it is that with that dependency (and without it when it eventually gets merged to the normal navigation-compose library) you will be able to write something like this:
composable(
"profile/{id}",
enterTransition = { _, _ ->
// Whatever EnterTransition object you want, like:
fadeIn(animationSpec = tween(2000))
}
) { // Content }
Currently, there is no way to configure the animations in the NavHost offered by Navigation-Compose's current version (2.4.0-beta02).
#Composable
public fun NavHost(
navController: NavHostController,
graph: NavGraph,
modifier: Modifier = Modifier
) {
val backStackEntry = visibleTransitionsInProgress.lastOrNull() ?: visibleBackStack.lastOrNull()
var initialCrossfade by remember { mutableStateOf(true) }
if (backStackEntry != null) {
// while in the scope of the composable, we provide the navBackStackEntry as the
// ViewModelStoreOwner and LifecycleOwner
Crossfade(backStackEntry.id, modifier) { //// <<----- this
As Crossfade is not configurable, the transition cannot be changed.
To change the animation, you have to abandon using the NavHost provided by Navigation-Compose.
I have created an Empty Compose Activity template using Android Studio Canara 2020.03. Here is the code of the file " MainActivity. kt":
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ButtonPage()
}
}
}
#Composable
fun ButtonPage(){
Button(onClick = {}){Text("Click to go next")}
}
#Composable
fun TextPage(){
Text("Second Page")
}
How do I modify the code so that when you click on this button, it draws only text? (That is, you need that when you click the button, the program draws other content by deleting this one first).
Jetpack Compose version 1.0.0-alpha09, jdk version 15, android version 11
Thank you in advance
A simple solution would be to have a variable that dictates the current screen.
var showSecondScreen by remember { mutableStateOf(false) }
if (!showSecondScreen) {
Button(onClick = {showSecondScreen = true}){Text("Click to go next")}
} else {
Text("Second screen")
}
This doesn't have to be a boolean, you could declare var currentScreen by remember { mutableStateOf("homeScreen") } and use a when block for which screen to show.
#Composable fun MyApp(currentScreen: String) {
when (currentScreen) {
"homeScreen" -> HomeScreen()
"secondScreen" -> SecondScreen()
}
}
So you can think of it less as a transaction and more as a stateful navigation.
But you'll soon realize that this doesn't handle back navigation, so you'll need a navigation library like jetpack compose navigation, decompose, compose-router, etc. Here's more information on jetpack compose navigation.
I have a single activity app using the androidx navigation library. For one of the menu destinations I effectively have a fragment as destination with no view whatsoever that depending on the state of the user provided configuration either redirects to the real destination that should be there or to one of currently two different views that tell the user that either he needs to setup a configuration first or that there currently is no active configuration (deleted?) and he needs to select one of the available configurations.
Now, functionally this approach works perfectly fine. However, since androidx navigation ties menu items to destinations by id the menu item that gets you to that view is never selected as it matches the fragment destination with no view in it.
I tried to add a NavController.OnDestinationChangedListener to my Activity and added it to the navController navController.addOnDestinationChangedListener(this). But it seems to get overwritten by the navigation afterwards.
override fun onDestinationChanged(controller: NavController, destination: NavDestination, arguments: Bundle?) {
val destinations = listOf(R.id.destinationA, R.id.destinationB, R.id.destinationC)
if(destinations.contains(destination.id)) {
nav_view.menu.getItem(0).isChecked = true
}
}
It is deffinitely the right menu item. As when I change isChecked = true to isEnabled = false I can no longer click on it.
Also when I do this odd hack it works
GlobalScope.launch(Dispatchers.Main) {
delay(1000)
nav_view.menu.getItem(0).isChecked = true
}
Needless to say this is not a very good solution.
Anyone here knows how to overwride the default behaviour of androidx navigation in this regard?
I´ll come back to this later and report back if I find a proper solution to this.
Adding a listener to the drawer opening and setting the selected menu item then might be a good workaround for this if it is not possible to do currently.
Instead of using setupWithNavController(), as mentioned in the documentation, setup it up yourself.
As mentioned here, onNavDestinationSelected() helper method in NavigationUI is called when the menu item is clicked when you set it up using setupWithNavController(). So you could try something like this:
yourNavigationView.setNavigationItemSelectedListener { item: MenuItem ->
if(item.itemId == R.id.noViewFragmentId) {
val isConfigurationProvided = ...
if(!isConfigurationProvided) {
//Perform your actions (navigate to either of the two alternate views)
return#setNavigationItemSelectedListener true
}
}
val success = NavigationUI.onNavDestinationSelected(item, navController)
if(success) {
drawerLayout.closeDrawer(GravityCompat.START)
item.isChecked = true
}
success
}
I´ll add this as a possible solution and stick with it for the time being. I still feel like there should be a better way to do this, so I will not accept it as an awnswer.
It´s essentially the idea I got at the end of writing the question
Adding a listener to the drawer opening and setting the selected menu item then might be a good workaround for this if it is not possible to do currently.
class SetActiveMenuDrawerListener(
private val navController: NavController,
navigationView: NavigationView) : DrawerLayout.DrawerListener {
private var checked = false
private val destinations = listOf(R.id.destinationA, R.id.destinationB, R.id.destinationC)
private val menu = navigationView.menu.getItem(0)
init {
navController.addOnDestinationChangedListener { _, _, _ -> checked = false }
}
override fun onDrawerSlide(drawerView: View, slideOffset: Float) {
}
override fun onDrawerOpened(drawerView: View) {
}
override fun onDrawerClosed(drawerView: View) {
}
override fun onDrawerStateChanged(newState: Int) {
if(checked) return
val currentDestination = navController.currentDestination ?: return
if(destinations.contains(currentDestination.id)) {
menu.isChecked = true
}
checked = true
}
}
Then add this to the DrawerLayout
drawer_layout.addDrawerListener(SetActiveMenuDrawerListener(navController, nav_view))
I did add the code into the onDrawerStateChanged instead onDrawerOpened, because onDrawerOpened gets called a bit late if clicking the drawer and not at all while dragging it.
It´s not the pretties thing to look at, but it gets the job done.
Given 2 Fragments A and B, A moves to B (so A -> B), via navigation component action with enter animation has been added. How to prevent views in Fragment B being clickable while enter animation is running? I've found this question How to add listener to android Navigation Architecture Component action animation but unfortunately there're no answers.
What I found in the documentation is that I could get resource ID of that animation through NavOptions object hooked onto the NavAction, but not the Animation object itself.
You can start by having your views as disabled in xml android:enabled="false" then in your fragment's onViewCreated you can set a delay with the animation duration using coroutines:
override fun onViewCreated(view: View, savedState: Bundle?) {
super.onViewCreated(view, savedState)
// Initialize views here.
lifecycleScope.launch {
delay(resources.getInteger(R.integer.anim_duration).toLong())
// Enable views here
myView.isEnabled = true
}
}
While I originally solved the problem using coroutines I faced the same problem
once again :] so I investigated a bit and stumbled upon this topic Disable clicks when fragment adding animation playing that helped me to figure out the right solution.
Apparently those action animations added through the navigation graph are
set by FragmentTransaction.setCustomAnimation(enter, exit, popEnter, popExit)
and these can be accessed by overriding onCreateAnimation(transit: Int, enter: Boolean, nextAnim: Int). Where nextAnim actually represents the action animations we added. For the fragment A it would be either exit or popEnter and for the fragment B it would be either enter or popExit.
The problem of views being clicked happens when fragment is entering (either enter or popEnter) so one can use an if statement to check enter and if true create Animation based on the nextAnim and then one can set listener to it. In case of home (starting) fragment one should exclude the case of nextAnim = 0 since it's also entering animation.
override fun onCreateAnimation(transit: Int, enter: Boolean, nextAnim: Int): Animation? {
if (nextAnim == 0 || !enter) return super.onCreateAnimation(transit, enter, nextAnim)
else {
return AnimationUtils.loadAnimation(requireContext(), nextAnim).apply {
setAnimationListener(object : Animation.AnimationListener {
override fun onAnimationStart(animation: Animation?) {
disableClicking()
}
override fun onAnimationEnd(animation: Animation?) {
enableClicking()
}
override fun onAnimationRepeat(animation: Animation?) {
}
})
}
}
}
EDIT: For non-home fragments to avoid disabling clicks at the start of the animation, we can start with views being unclickable in xml layout and only enable clicking when the animation ends. To remove a bug where views remain unclickable if a device rotation happens we can introduce a boolean variable that we will set to true when animation ends and preserve it by overriding onSaveInstanceState(outState: Bundle) and reinstating it in onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?) and check if it was true before device rotation to re-enable clicking once again.