I have this Text:
Text(
text = stringResource(id = R.string.hello)
)
How can I show and hide this component?
I'm using Jetpack Compose version '1.0.0-alpha03'
As CommonsWare stated, compose being a declarative toolkit you tie your component to a state (for ex: isVisible), then compose will intelligently decide which composables depend on that state and recompose them. For ex:
#Composable
fun MyText(isVisible: Boolean){
if(isVisible){
Text(text = stringResource(id = R.string.hello))
}
}
Also you could use the AnimatedVisibility() composable for animations.
You can simply add a condition like:
if(isVisible){
Text("....")
}
Something like:
var visible by remember { mutableStateOf(true) }
Column() {
if (visible) {
Text("Text")
}
Button(onClick = { visible = !visible }) { Text("Toggle") }
}
If you want to animate the appearance and disappearance of its content you can use the AnimatedVisibility
var visible by remember { mutableStateOf(true) }
Column() {
AnimatedVisibility(
visible = visible,
enter = fadeIn(
// Overwrites the initial value of alpha to 0.4f for fade in, 0 by default
initialAlpha = 0.4f
),
exit = fadeOut(
// Overwrites the default animation with tween
animationSpec = tween(durationMillis = 250)
)
) {
// Content that needs to appear/disappear goes here:
Text("....")
}
Button(onClick = { visible = !visible }) { Text("Toggle") }
}
As stated above, you could use AnimatedVisibility like:
AnimatedVisibility(visible = yourCondition) { Text(text = getString(R.string.yourString)) }
/**
* #param visible if false content is invisible ie. space is still occupied
*/
#Composable
fun Visibility(
visible: Boolean,
content: #Composable () -> Unit
) {
val contentSize = remember { mutableStateOf(IntSize.Zero) }
Box(modifier = Modifier
.onSizeChanged { size -> contentSize.value = size }) {
if (visible || contentSize.value == IntSize.Zero) {
content()
} else {
Spacer(modifier = Modifier.size(contentSize.value.width.pxToDp(), contentSize.value.height.pxToDp()))
}
}
}
fun Int.pxToDp(): Dp {
return (this / getSystem().displayMetrics.density).dp
}
usage:
Visibility(text.value.isNotEmpty()) {
IconButton(
onClick = { text.value = "" },
modifier = Modifier
.padding(bottom = 8.dp)
.height(30.dp),
) {
Icon(Icons.Filled.Close, contentDescription = "Clear text")
}
}
Related
I'm trying to make a swipe to reveal component using compose, but I want the width of the card that will appear after the swipe to grow to the size of the wrap content without using it, but I don't understand how to calculate the wrap content size.
var width by remember {
mutableStateOf(0.dp)
}
val lowerTransition = updateTransition(transitionState, "lowerCardTransition")
val lowerOffsetTransition by lowerTransition.animateFloat(
label = "lowerCardOffsetTransition",
transitionSpec = { tween(durationMillis = ANIMATION_DURATION) },
targetValueByState = { if (isRevealed) width.value else 0f },
)
How do I equate the width value used here to the wrap content value?
I'm trying to make the resulting delete button appear all without using a constant value
Try using AnimatedVisibility. For demo purpose I used OnClick, replace it with OnSwipe.
#Preview
#Composable
fun AnimateVisibility2() {
var visible by remember {
mutableStateOf(false)
}
Row(
modifier = Modifier.fillMaxSize(), horizontalArrangement = Arrangement.Center
) {
AnimatedVisibility(
visible = visible, enter = expandHorizontally(), exit = shrinkHorizontally()
) {
IconButton(onClick = { /*TODO*/ }) {
Icon(Icons.Default.Phone, contentDescription = null)
}
}
Button(onClick = { visible = !visible }, Modifier.weight(1f)) {
Text("Click Me")
}
}
}
I have a text which need to be animated to show and hide with the value is null or not. it would have been straight forward if the visibility is separately handle, but this is what I got.
In the bellow code the enter animation works but the exit animation dont as the text value is null.
I can think of something with remembering the old value but not sure how.
#Composable
fun ShowAnimatedText(
text : String?
) {
Column(
modifier = Modifier.fillMaxWidth()
) {
AnimatedVisibility(
visible = text != null,
enter = fadeIn(animationSpec = tween(2000)),
exit = fadeOut(animationSpec = tween(2000))
) {
text?.let {
Text(text = it)
}
}
}
}
I think the fade-out animation is actually working "per-se".
I suspect the parameter text: String? is a value coming from a hoisted "state" somewhere up above ShowAnimatedText, and since you are directly observing it inside the animating scope, when you change it to null it instantly removes the Text composable, and your'e not witnessing a slow fade out.
AnimatedVisibility(
...
) {
text?.let { // your'e directly observing a state over here
Text(text = it)
}
}
This is my attempt completing your snippet based on my assumption and making it work, the fade-in works, but the desired fade-out is instantly happening.
#Composable
fun SomeScreen() {
var text by remember {
mutableStateOf<String?>("Initial Value")
}
Row(
modifier = Modifier.fillMaxWidth()
) {
Button(onClick = {
text = "New Value"
}) {
Text("Set New Value")
}
Button(onClick = {
text = null
}) {
Text("Remove Value")
}
AnimatedText(text = text)
}
}
#Composable
fun ShowAnimatedText(
text : String?
) {
Column(
modifier = Modifier.fillMaxWidth()
) {
AnimatedVisibility(
visible = text != null,
enter = fadeIn(animationSpec = tween(2000)),
exit = fadeOut(animationSpec = tween(2000))
) {
text?.let {
Text(text = it)
}
}
}
}
You can solve it by modifying the text to a non-state value and change your visibility logic from using a nullability check to some "business logic" that would require it to be visible or hidden, modifying the codes above like this.
#Composable
fun SomeScreen() {
var show by remember {
mutableStateOf(true)
}
Row(
modifier = Modifier.fillMaxWidth()
) {
Button(onClick = {
show = !show
}) {
Text("Set New Value")
}
AnimatedText(text = "Just A Value", show)
}
}
#Composable
fun ShowAnimatedText(
text : String?,
show: Boolean
) {
Column(
modifier = Modifier.fillMaxWidth()
) {
AnimatedVisibility(
visible = show,
enter = fadeIn(animationSpec = tween(2000)),
exit = fadeOut(animationSpec = tween(2000))
) {
text?.let {
Text(text = it)
}
}
}
}
I fixed it by remembering the previous state (Or don't set the null value) until the exit animation is finished, if the text is null.
Thank you z.y for your suggestion.
#Composable
fun ShowAnimatedText(
text : String?,
show: Boolean
) {
var localText by remember {
mutableStateOf<String?>(null)
}
AnimatedContent(show, localText)
LaunchedEffect(key1 = text, block = {
if(text == null){
delay(2000)
}
localText = text
})
}
#Composable
private fun AnimatedContent(show: Boolean, localText: String?) {
Column(
modifier = Modifier.fillMaxWidth()
) {
AnimatedVisibility(
visible = show,
enter = fadeIn(animationSpec = tween(2000)),
exit = fadeOut(animationSpec = tween(2000))
) {
localText?.let {
Text(text = it)
}
}
}
}
How to do this Scroll hide fab button in Jetpack Compose with transaction
Like this I need it:
You need to listen to the scroll state and apply AnimatedVisibiltiy. Here is an example using LazyColumn with LazyListState (you could also use Column with ScrollState)
#Composable
fun Screen() {
val listState = rememberLazyListState()
val fabVisibility by derivedStateOf {
listState.firstVisibleItemIndex == 0
}
Box(modifier = Modifier.fillMaxSize()) {
LazyColumn(
Modifier.fillMaxSize(),
state = listState,
) {
items(count = 100, key = { it.toString() }) {
Text(modifier = Modifier.fillMaxWidth(),
text = "Hello $it!")
}
}
AddPaymentFab(
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(bottom = 40.dp),
isVisibleBecauseOfScrolling = fabVisibility
)
}
}
#Composable
private fun AddPaymentFab(
modifier: Modifier,
isVisibleBecauseOfScrolling: Boolean,
) {
val density = LocalDensity.current
AnimatedVisibility(
modifier = modifier,
visible = isVisibleBecauseOfScrolling,
enter = slideInVertically {
with(density) { 40.dp.roundToPx() }
} + fadeIn(),
exit = fadeOut(
animationSpec = keyframes {
this.durationMillis = 120
}
)
) {
ExtendedFloatingActionButton(
text = { Text(text = "Add Payment") },
onClick = { },
icon = { Icon(Icons.Filled.Add, "Add Payment") }
)
}
}
It may be late, but after struggling with this issue for a while, I was able to find the right solution from the Animation Codelab sourcecode.
The difference between this and the previous answer is that in this way, as soon as the page is scrolled up, the Fab is displayed and there is no need to reach the first item of the page to display the Fab.
step one: getting an instance of the lazyListState class inside LazyColumn
val lazyListState = rememberLazyListState()
Step two: Creating a top level variable to hold the scroll state so that recompositions do not change the state value unintentionally.
var isScrollingUp by mutableStateOf(false)
Step three: just copy this composable Extension Function inside the file
#Composable
private fun LazyListState.isScrollingUp(): Boolean {
var previousIndex by remember(this) { mutableStateOf(firstVisibleItemIndex) }
var previousScrollOffset by remember(this) { mutableStateOf(firstVisibleItemScrollOffset) }
return remember(this) {
derivedStateOf {
if (previousIndex != firstVisibleItemIndex) {
previousIndex > firstVisibleItemIndex
} else {
previousScrollOffset >= firstVisibleItemScrollOffset
}.also {
previousIndex = firstVisibleItemIndex
previousScrollOffset = firstVisibleItemScrollOffset
}
}
}.value
}
Step four: Open an AnimatedVisibility block and pass the isScrollingUp variable as its first parameter. And finally, making the Fab and placing it inside the AnimatedVisibility
AnimatedVisibility(visible = isScrollingUp) {
FloatingActionButton(onClick = { /*TODO*/ }) {
// your code
}
}
have fun!
So in XML you were able to structure menu items and nest them like this.
But in jetpack compose, I am unable to figure out how this would work.
I already read and built a simple drop down menu from here. But trying to do the same as XML in jetpack compose doesn't make much sense. The menus are created separately and independent. I am looking for something simpler and better than that.
How about something like this?
Create a composable for your main menu, and one for your nested menu.
Main Menu Composable
#Composable
fun MainMenu(
menuSelection: MutableState<MenuSelection>,
expandedMain: MutableState<Boolean>,
expandedNested: MutableState<Boolean>
) {
DropdownMenu(
expanded = expandedMain.value,
onDismissRequest = { expandedMain.value = false },
) {
DropdownMenuItem(
onClick = {
expandedMain.value = false // hide main menu
expandedNested.value = true // show nested menu
menuSelection.value = MenuSelection.NESTED
}
) {
Text("Nested Options \u25B6")
}
Divider()
DropdownMenuItem(
onClick = {
// close main menu
expandedMain.value = false
menuSelection.value = MenuSelection.SETTINGS
}
) {
Text("Settings")
}
Divider()
DropdownMenuItem(
onClick = {
// close main menu
expandedMain.value = false
menuSelection.value = MenuSelection.ABOUT
}
) {
Text("About")
}
}
}
Nested Menu Composable
#Composable
fun NestedMenu(
expandedNested: MutableState<Boolean>,
nestedMenuSelection: MutableState<NestedMenuSelection>
) {
DropdownMenu(
expanded = expandedNested.value,
onDismissRequest = { expandedNested.value = false }
) {
DropdownMenuItem(
onClick = {
// close nested menu
expandedNested.value = false
nestedMenuSelection.value = NestedMenuSelection.FIRST
}
) {
Text("First")
}
DropdownMenuItem(
onClick = {
// close nested menu
expandedNested.value = false
nestedMenuSelection.value = NestedMenuSelection.SECOND
}
) {
Text("Second")
}
}
}
Then place them in your main drop down menu composable.
Top Bar Composable
#Composable
fun TopAppBarDropdownMenu(
menuSelection: MutableState<MenuSelection>,
nestedMenuSelection: MutableState<NestedMenuSelection>
) {
val expandedMain = remember { mutableStateOf(false) }
val expandedNested = remember { mutableStateOf(false) }
// Three Dot icon
Box(
Modifier
.wrapContentSize(Alignment.TopEnd)
) {
IconButton(
onClick = {
// Expand the main menu on three dots icon click
// and hide the nested menu.
expandedMain.value = true
expandedNested.value = false
}
) {
Icon(
Icons.Filled.MoreVert,
contentDescription = "More Menu"
)
}
}
MainMenu(
menuSelection = menuSelection,
expandedMain = expandedMain,
expandedNested = expandedNested
)
NestedMenu(
expandedNested = expandedNested,
nestedMenuSelection = nestedMenuSelection
)
}
The menuSelection and nestedMenuSelection parameters of TopAppBarDropdownMenu would allow implementing actions in the TopAppBarDropdownMenu host depending on which state is activated from the menu. Something like this:
#Composable
fun MainScreen(
/* some params */
) {
val menuSelection = remember { mutableStateOf(MenuSelection.NONE) }
val nestedMenuSelection = remember { mutableStateOf(NestedMenuSelection.DEFAULT) }
Scaffold(
topBar = {
TopAppBar(
title = { Text(text = stringResource(R.string.app_name)) },
actions = {
TopAppBarDropdownMenu(
menuSelection = menuSelection,
nestedMenuSelection= nestedMenuSelection
)
}
)
}
)
}
Not sure it's the "right" way to do it but it works for me.
You could potentially make it easier to instantiate nested menus by creating a NestedMenu composable with more parameters, which would make it easier to reuse. Just keep in mind that Material Design guidelines recommend using Cascading Menus only on desktop.
I am wondering this question as well.
At present, I compose this nested Menu like this:
#Composable
fun ExpandableDropdownItem(
text: String,
dropDownItems: #Composable () -> Unit
) {
var expanded by remember {
mutableStateOf(false)
}
DropdownMenuItem(onClick = {
expanded = true
}) {
Text(text = text)
}
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
dropDownItems()
}
}
This can be used with other DropdownItems.
And that's not the best solution : the place of the nested Menu is strange.
I made a nested menu variation for sorting that seems to work well using AnimatedVisibility.
When expanding, it switches the top level menus visibility to the submenus, and can even add a back button if needed, but more levels might get confusing with the switches.
#Composable
fun NestedSortMenu(
onSortClick: (SortOrder) -> Unit
) {
var expanded by remember { mutableStateOf(false) }
var orderType: OrderType by remember { mutableStateOf(OrderType.Ascending) }
AnimatedVisibility(visible = !expanded) {
Column {
DropdownMenuItem(onClick = {
orderType = OrderType.Ascending
expanded = !expanded
}) {
Text(text = stringResource(R.string.ascending))
Icon(
Icons.Filled.NavigateNext,
contentDescription = stringResource(R.string.ascending),
modifier = Modifier.size(20.dp)
)
}
DropdownMenuItem(onClick = {
orderType = OrderType.Descending
expanded = !expanded
}) {
Text(text = stringResource(R.string.descending))
Icon(
Icons.Filled.NavigateNext,
contentDescription = stringResource(R.string.descending),
modifier = Modifier.size(20.dp)
)
}
}
}
AnimatedVisibility(visible = expanded) {
Column {
DropdownMenuItem(onClick = {
expanded = !expanded
onSortClick(SortOrder.Title(orderType))
}) {
Text(text = stringResource(R.string.title_menu))
}
DropdownMenuItem(onClick = {
expanded = !expanded
onSortClick(SortOrder.Format(orderType))
}) {
Text(text = stringResource(R.string.format_menu))
}
DropdownMenuItem(onClick = {
expanded = !expanded
onSortClick(SortOrder.DateAndTime(orderType))
}) {
Text(text = stringResource(R.string.date_and_time))
}
}
}
}
I want to implement a screen which can show two different bottom sheets.
Since ModalBottomSheetLayout only has a slot for one sheet I decided to change the sheetContent of the ModalBottomSheetLayout dynamically using a selected state when I want to show either of the two sheets (full code).
val sheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden)
val (selected, setSelected) = remember(calculation = { mutableStateOf(0) })
ModalBottomSheetLayout(sheetState = sheetState, sheetContent = {
when (selected) {
0 -> Layout1()
1 -> Layout2()
}
}) {
Content(sheetState = sheetState, setSelected = setSelected)
}
This works fine for very similar sheets, but as soon as you add more complexity to either of the two sheet layouts the sheet will not show when the button is pressed for the first time, it will only show after the button is pressed twice as you can see here:
Here you can find a reproducible example
I had a similar usecase, where I needed to show 2-3 stacked bottomsheets.
I ended up copying large part of Compose BottomSheet and added the desired behavior:
enum class BottomSheetValue { SHOWING, HIDDEN }
#Composable
fun BottomSheet(
parentHeight: Int,
topOffset: Dp = 0.dp,
fillMaxHeight: Boolean = false,
sheetState: SwipeableState<BottomSheetValue>,
shape: Shape = bottomSheetShape,
backgroundColor: Color = MaterialTheme.colors.background,
contentColor: Color = contentColorFor(backgroundColor),
elevation: Dp = 0.dp,
content: #Composable () -> Unit
) {
val topOffsetPx = with(LocalDensity.current) { topOffset.roundToPx() }
var bottomSheetHeight by remember { mutableStateOf(parentHeight.toFloat())}
val scrollConnection = sheetState.PreUpPostDownNestedScrollConnection
BottomSheetLayout(
maxHeight = parentHeight - topOffsetPx,
fillMaxHeight = fillMaxHeight
) {
val swipeable = Modifier.swipeable(
state = sheetState,
anchors = mapOf(
parentHeight.toFloat() to BottomSheetValue.HIDDEN,
parentHeight - bottomSheetHeight to BottomSheetValue.SHOWING
),
orientation = Orientation.Vertical,
resistance = null
)
Surface(
shape = shape,
color = backgroundColor,
contentColor = contentColor,
elevation = elevation,
modifier = Modifier
.nestedScroll(scrollConnection)
.offset { IntOffset(0, sheetState.offset.value.roundToInt()) }
.then(swipeable)
.onGloballyPositioned {
bottomSheetHeight = it.size.height.toFloat()
},
) {
content()
}
}
}
#Composable
private fun BottomSheetLayout(
maxHeight: Int,
fillMaxHeight: Boolean,
content: #Composable () -> Unit
) {
Layout(content = content) { measurables, constraints ->
val sheetConstraints =
if (fillMaxHeight) {
constraints.copy(minHeight = maxHeight, maxHeight = maxHeight)
} else {
constraints.copy(maxHeight = maxHeight)
}
val placeable = measurables.first().measure(sheetConstraints)
layout(placeable.width, placeable.height) {
placeable.placeRelative(0, 0)
}
}
}
TopOffset e.g. allows to place the bottomSheet below the AppBar:
BoxWithConstraints {
BottomSheet(
parentHeight = constraints.maxHeight,
topOffset = with(LocalDensity.current) {56.toDp()}
fillMaxHeight = true,
sheetState = yourSheetState,
) {
content()
}
}
I wanted to implement the same thing and because of the big soln, I wrote a post on dev.to that solves this problem, Here is the link
I implemented it like this. It looks pretty simple, but I still could not figure out how to pass the argument to "mutableStateOf ()" directly, I had to create a variable "content"
fun Screen() {
val bottomSheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
val scope = rememberCoroutineScope()
val content: #Composable (() -> Unit) = { Text("NULL") }
var customSheetContent by remember { mutableStateOf(content) }
ModalBottomSheetLayout(
sheetState = bottomSheetState,
sheetContent = {
customSheetContent()
}
) {
Column {
Button(
onClick = {
customSheetContent = { SomeComposable1() }
scope.launch { bottomSheetState.show() }
}) {
Text("First Button")
}
Button(
onClick = {
customSheetContent = { SomeComposable2() }
scope.launch { bottomSheetState.show() }
}) {
Text("Second Button")
}
}
}
I just tried your code. I am not sure but looks like when you click first time, since selected state changes, Content function tries to recompose itself and it somehow blocks sheetState. Because i can see that when i click first time, bottom sheet shows up a little and disappears immediately. But second time i click same button, since selected state doesnt change, sheetState works properly.