How to remove default transitions in Jetpack Compose Navigation - android

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.

Related

Jetpack Compose Using Same State Value For All Screens

I want to show circularProgressIndicator while an operation is working such as network call. I want to be able to change the indicator visibility state in all viewmodels and observe that value in NavHost because I want to show a transparant layout which has a circular indicator and prevent user from clicking another fields while network call is still going.
I tried to use baseviewmodel class and mutableStateOf(Boolean) in it but whenever I tried to access viewmodel from navhost it's instance is different than other.
Question is basically how can i create a single global mutableStateOf() object and change it's value from all inside of my viewmodels and observe it as a state inside of NavHost composable to change visibility of circularProgressIndicator?
Note: I am using hilt to get instance of viewmodel -> viewModel: MyViewModel = hiltViewModel()
#Composable
fun NavHost(modifier: Modifier = Modifier) {
val navController = rememberAnimatedNavController()
Box {
AnimatedNavHost(
navController = navController,
startDestination = Screen.EmailAndPassword.route,
modifier = modifier.fillMaxSize()
) {
composable(Screen.EmailAndPassword.route) {
EmailAndPasswordScreen {
navController.navigate(Screen.UserInformation.route) {
/*TODO*/
}
}
}
composable(Screen.UserInformation.route) { UserInformationScreen() }
}
LoadingScreen(isVisible = /* IndicatorState */)
}}
#Composable
fun LoadingScreen(modifier: Modifier = Modifier, isVisible: Boolean) {
if (isVisible){
Box(modifier = modifier.background(Color.Transparent).fillMaxSize(), contentAlignment = Alignment.Center) {
CircularProgressIndicator()
}
}}
I tried to use baseviewmodel class and mutableStateOf(Boolean) in it but whenever I tried to access viewmodel from navhost it's instance is different than other.
I use Compose-Destinations library and for sharing a ViewModel throughout the Activity, I do it like this (This is a sample from documentation):
#Composable
fun AppNavigation(
activity: Activity
) {
DestinationsNavHost(
//...
dependenciesContainerBuilder = { //this: DependenciesContainerBuilder<*>
// 👇 To tie SettingsViewModel to "settings" nested navigation graph,
// making it available to all screens that belong to it
dependency(NavGraphs.settings) {
val parentEntry = remember(navBackStackEntry) {
navController.getBackStackEntry(NavGraphs.settings.route)
}
hiltViewModel<SettingsViewModel>(parentEntry)
}
// 👇 To tie ActivityViewModel to the activity, making it available to all destinations
dependency(hiltViewModel<ActivityViewModel>(activity))
}
)
}

Bottom Nav Android Jetpack Compose issue

The below is an overview of the BottomNav implementation.The app shows the bottom Nav bar properly but when an item is selected, it calls the NavHost multiple times. I see a similar issue for Jetpack compose samples https://github.com/android/compose-samples/tree/main/Jetsnack. Is there any workaround to avoid multiple Navhost calls?
#Composable
fun MainScreen() {
val navController = rememberNavController()
Scaffold(
bottomBar = { BottomMenu(navController = navController) }
) {
BottomNavGraphBar(navController = navController)
}
}
// handling the click event
BottomNavigationItem(
onClick = {
navController.navigate(screen.route) {
popUpTo(navController.graph.findStartDestination().id)
launchSingleTop = true
}
}
)
//NavHost implementation
#Composable
fun BottomNavGraphBar(navController: NavHostController) {
NavHost(
navController = navController,
startDestination = Screen.Home.route
) {
composable(route = Screen.Home.route) {
Log.d("BottomNavGraph","BottomNavGraph->HomeScreen")
HomeScreen()
}
composable(route = Screen.Settings.route) {
Log.d("BottomNavGraph","BottomNavGraph->AppSettingsScreen")
AppSettingsScreen()
}
composable(route = Screen.Profile.route) {
Log.d("BottomNavGraph","BottomNavGraph->ProfileScreen")
ProfileScreen()
}
}
}
<!---LogCat-->
// When app is launched
BottomNavGraph->HomeScreen
BottomNavGraph->HomeScreen
// clicked on the profile.
BottomNavGraph->HomeScreen
BottomNavGraph->ProfileScreen
BottomNavGraph->HomeScreen
BottomNavGraph->ProfileScreen
Compose can (and will, depending on multiple things) call composable functions to "re-compose" them. Although it is smart and can cache the output of composable functions for their previous inputs, so that it does not have to recompute their results (e.g. their emitted UI).
In your example, the composable(..) { ... } might get recomposed, but if you use composables inside it (like a few Texts) it will use its cache from its last rendering.
You don't need to worry about your functions being called, but you do have to take care of your computations. This is why you'd want to use remember to calculate something and store it in the cache, so it is not re-calculated again.

Jetpack Compose: Make full-screen (absolutely positioned) component

How can I go about making a composable deep down within the render tree full screen, similar to how the Dialog composable works?
Say, for example, when a use clicks an image it shows a full-screen preview of the image without changing the current route.
I could do this in CSS with position: absolute or position: fixed but how would I go about doing this in Jetpack Compose? Is it even possible?
One solution would be to have a composable at the top of the tree that can be passed another composable as an argument from somewhere else in the tree, but this sounds kind of messy. Surely there is a better way.
From what I can tell you want to be able to draw from a nested hierarchy without being limited by the parent constraints.
We faced similar issues and looked at the implementation how Composables such as Popup, DropDown and Dialog function.
What they do is add an entirely new ComposeView to the Window.
Because of this they are basically starting from a blank canvas.
By making it transparent it looks like the Dialog/Popup/DropDown appears on top.
Unfortunately we could not find a Composable that provides us the functionality to just add a new ComposeView to the Window so we copied the relevant parts and made following.
#Composable
fun FullScreen(content: #Composable () -> Unit) {
val view = LocalView.current
val parentComposition = rememberCompositionContext()
val currentContent by rememberUpdatedState(content)
val id = rememberSaveable { UUID.randomUUID() }
val fullScreenLayout = remember {
FullScreenLayout(
view,
id
).apply {
setContent(parentComposition) {
currentContent()
}
}
}
DisposableEffect(fullScreenLayout) {
fullScreenLayout.show()
onDispose { fullScreenLayout.dismiss() }
}
}
#SuppressLint("ViewConstructor")
private class FullScreenLayout(
private val composeView: View,
uniqueId: UUID
) : AbstractComposeView(composeView.context) {
private val windowManager =
composeView.context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
private val params = createLayoutParams()
override var shouldCreateCompositionOnAttachedToWindow: Boolean = false
private set
init {
id = android.R.id.content
ViewTreeLifecycleOwner.set(this, ViewTreeLifecycleOwner.get(composeView))
ViewTreeViewModelStoreOwner.set(this, ViewTreeViewModelStoreOwner.get(composeView))
ViewTreeSavedStateRegistryOwner.set(this, ViewTreeSavedStateRegistryOwner.get(composeView))
setTag(R.id.compose_view_saveable_id_tag, "CustomLayout:$uniqueId")
}
private var content: #Composable () -> Unit by mutableStateOf({})
#Composable
override fun Content() {
content()
}
fun setContent(parent: CompositionContext, content: #Composable () -> Unit) {
setParentCompositionContext(parent)
this.content = content
shouldCreateCompositionOnAttachedToWindow = true
}
private fun createLayoutParams(): WindowManager.LayoutParams =
WindowManager.LayoutParams().apply {
type = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL
token = composeView.applicationWindowToken
width = WindowManager.LayoutParams.MATCH_PARENT
height = WindowManager.LayoutParams.MATCH_PARENT
format = PixelFormat.TRANSLUCENT
flags = WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS or
WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
}
fun show() {
windowManager.addView(this, params)
}
fun dismiss() {
disposeComposition()
ViewTreeLifecycleOwner.set(this, null)
windowManager.removeViewImmediate(this)
}
}
Here is an example how you can use it
#Composable
internal fun Screen() {
Column(
Modifier
.fillMaxSize()
.background(Color.Red)
) {
Text("Hello World")
Box(Modifier.size(100.dp).background(Color.Yellow)) {
DeeplyNestedComposable()
}
}
}
#Composable
fun DeeplyNestedComposable() {
var showFullScreenSomething by remember { mutableStateOf(false) }
TextButton(onClick = { showFullScreenSomething = true }) {
Text("Show full screen content")
}
if (showFullScreenSomething) {
FullScreen {
Box(
Modifier
.fillMaxSize()
.background(Color.Green)
) {
Text("Full screen text", Modifier.align(Alignment.Center))
TextButton(onClick = { showFullScreenSomething = false }) {
Text("Close")
}
}
}
}
}
The yellow box has set some constraints, which would prevent the Composables from inside to draw outside its bounds.
Using the Dialog composable, I have been able to get a proper fullscreen Composable in any nested one. It's quicker and easier than some of other answers.
Dialog(
onDismissRequest = { /* Do something when back button pressed */ },
properties = DialogProperties(dismissOnBackPress = true, dismissOnClickOutside = false, usePlatformDefaultWidth = false)
){
/* Your full screen content */
}
If I understand correctly you just don't want to navigate anywhere. Id something like this.
when (val viewType = viewModel.viewTypeGallery.get()) {
is GalleryViewModel.GalleryViewType.Gallery -> {
Gallery(viewModel, scope, installId, filePathModifier, fragment, setImageUploadType)
}
is GalleryViewModel.GalleryViewType.ImageViewer -> {
Row(Modifier.fillMaxWidth()) {
Image(
modifier = Modifier
.fillMaxSize(),
painter = rememberCoilPainter(viewType.imgUrl),
contentScale = ContentScale.Crop,
contentDescription = null
)
}
}
}
I just keep track of what type the view is meant to be. In my case I'm not displaying a dialog I'm removing my entire gallery and showing an image instead.
Alternatively you could just have an if(viewImage) condition below your call your and layer the 'dialog' on top of it.
After notice that, at least for now, we don't have any Composable to do "easy" fullscreen, I decided to implement mine one, mostly based on ideas from #foxtrotuniform6969 and #ntoskrnl. Also, I tried to do it most possible without to use platform dependent functions then I think this is very suiteable to Desktop/Android.
You can check the basic implementation in this GitHub repository.
By the way, the implementation idea was just:
Create a composable to wrap the target composables tree that can call an FullScreen composable;
Retrieve the full screen dimensions/size from a auxiliary Box matched to the root screen size using the .onGloballyPositioned() modifier;
Store the full screen size and all FullScreen composables created in the tree onto appropriated compositionLocalOf instances (see documentation).
I tried to use this in a Desktop project and seems to be working, however I didn't tested in Android yet. The repository also contains a example.
Feel free to navigate in the repository and sent a pull request if you can. :)

Jetpack Compose Crossfade broken in Alpha

My crossfade animations are no longer working since the release of Compose Alpha and I would really appreciate some help getting them working again. I am fairly new to Android/Compose. I understand that Crossfade is looking for a state change in its targetState to trigger the crossfade animation, but I am confused how to incorporate this. I am trying to wrap certain composables in the Crossfade animation.
Here are the official docs and helpful playground example, but I still cannot get it to work since the release of Alpha
https://developer.android.com/reference/kotlin/androidx/compose/animation/package-summary#crossfade
https://foso.github.io/Jetpack-Compose-Playground/animation/crossfade/
Here is my code, in this instance I was hoping to use the String current route itself as the targetState as a mutableStateOf object. I'm willing to use whatever will work though.
#Composable
fun ExampleComposable() {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute: String? = navBackStackEntry?.arguments?.getString(KEY_ROUTE)
val exampleRouteTargetState = remember { mutableStateOf(currentRoute)}
Scaffold(
...
NavHost(navController, startDestination = "Courses") {
composable("Route") {
Crossfade(targetState = exampleRouteTargetState, animationSpec = tween(2000)) {
ExampleComposable1()
}
}
composable("Other Route")
ExampleComposable2()
}
)
...
}
Shouldn't navigation trigger a state change of the "exampleRouteTargetState" variable and then trigger crossfade? I could also wrap the composable elsewhere if you think wrapping it inside the NavHost may create an issue. Thanks so much for the help!!
Lately Google Accompanist has added a library which provides Compose Animation support for Jetpack Navigation Compose.. Do check it out. 👍🏻
https://github.com/google/accompanist/tree/main/navigation-animation
Still haven't gotten Crossfade working again, but I was able to implement some transitions inside NavHost. Hope this helps someone. Here are the docs if you want to fine tune these high level animations:
https://developer.android.com/jetpack/compose/animation#animatedvisibility
#ExperimentalAnimationApi
#Composable
fun ExampleAnimation(content: #Composable () -> Unit) {
AnimatedVisibility(
visible = true,
enter = fadeIn(initialAlpha = 0.3f),
exit = fadeOut(),
content = content,
initiallyVisible = false
)
}
And then simply wrap your NavHost composable declarations with your animation like so
NavHost(navController, startDestination = "A Route") {
composable(Screen.YourObject.Route) {
ExampleAnimation {
YourComposable()
}
}

Android Compose setupWithNavController

I am looking for a Compose variant of setupWithNavController(Toolbar, NavController) to automatically update the up button in an AppBar whenever the Navigation destination changes.
So far I haven't found anything useful.
Is it considered a bad design in Compose? Or is there some simple way how to achieve the same thing that I am not seeing?
I've hacked up a solution, but I am not satisfied.
androidx.navigation:navigation-compose:1.0.0-alpha08 provides an extension function to observe the current back stack entry.
#Composable
fun NavController.currentBackStackEntryAsState(): State<NavBackStackEntry?>
I've created a similar extension to observe the previous back stack entry
/**
* Gets the previous navigation back stack entry as a [MutableState]. When the given navController
* changes the back stack due to a [NavController.navigate] or [NavController.popBackStack] this
* will trigger a recompose and return the second top entry on the back stack.
*
* #return a mutable state of the previous back stack entry
*/
#Composable
fun NavController.previousBackStackEntryAsState(): State<NavBackStackEntry?> {
val previousNavBackStackEntry = remember { mutableStateOf(previousBackStackEntry) }
// setup the onDestinationChangedListener responsible for detecting when the
// previous back stack entry changes
DisposableEffect(this) {
val callback = NavController.OnDestinationChangedListener { controller, _, _ ->
previousNavBackStackEntry.value = controller.previousBackStackEntry
}
addOnDestinationChangedListener(callback)
// remove the navController on dispose (i.e. when the composable is destroyed)
onDispose {
removeOnDestinationChangedListener(callback)
}
}
return previousNavBackStackEntry
}
and a composable for the back button
#Composable
fun NavigationIcon(navController: NavController): #Composable (() -> Unit)? {
val previousBackStackEntry: NavBackStackEntry? by navController.previousBackStackEntryAsState()
return previousBackStackEntry?.let {
{
IconButton(onClick = {
navController.popBackStack()
}) {
Icon(Icons.Default.ArrowBack, contentDescription = "Up button")
}
}
}
}
I had to return a #Composable (() -> Unit)? (instead of no return value common to composable functions) because the TopAppBar uses the nullability to check whether to offset the title by 16dp (without icon) or by 72dp (with icon).
Finally, the content looks something like this
#Composable
fun MainContent() {
val navController: NavHostController = rememberNavController()
Scaffold(
topBar = {
TopAppBar(
title = { Text("Weather") },
navigationIcon = NavigationIcon(navController)
)
},
) {
NavHost(
navController,
startDestination = "list"
) {
composable("list") {
...
}
composable("detail") {
...
}
}
}
}
List:
Detail:
It might be cleaner to create a custom NavigationTopAppBar composable and hoist the NavController out of NavigationIcon but the idea stays the same. I didn't bother tinkering further.
I've also attempted to automatically update the title according to the current NavGraph destination. Unfortunately, there is not a reliable way to set a label to destinations without extracting quite a big chunk of internal implementation out of androidx.navigation:navigation-compose library.

Categories

Resources