I find myself in this situation quite often. I have some value like plates in the sample below, and I want to show/hide it depending on if its null or not. But hiding it always fails since nothing is rendered whenever its null, and the animation just snaps to empty nothingness.
How can I make this work? Id like to keep plates around until the animation finishes.
AnimatedVisibility(
visible = plates != null,
content = {
if (plates != null) {
// Render plates
} else {
// The animation snaps to nothingness, as opposed to animating out
}
})
This is what I ended up doing, and it works as expected!
#Composable
inline fun <T> AnimatedValueVisibility(
value: T?,
enter: EnterTransition = fadeIn(
animationSpec = FloatSpec
) + expandVertically(
animationSpec = SizeSpec,
clip = false
),
exit: ExitTransition = fadeOut(
animationSpec = FloatSpec
) + shrinkVertically(
animationSpec = SizeSpec,
clip = false
),
crossinline content: #Composable (T) -> Unit
) {
val ref = remember {
Ref<T>()
}
ref.value = value ?: ref.value
AnimatedVisibility(
visible = value != null,
enter = enter,
exit = exit,
content = {
ref.value?.let { value ->
content(value)
}
}
)
}
Animation always requires content even when there is nothing to show. Just display a Surface when the content is null (or empty):
AnimatedVisibility(
visible = plates != null,
content = {
if (plates != null) {
// Render plates
} else {
Surface(modifier = Modifier.fillMaxSize()) { }
}
})
Related
I made LazyColumn with vertical scroll bar and it's work good, but when i scroll mouse, column just jumping(not smooth), but when I scroll vert. bar, it's smooth
#ExperimentalFoundationApi
#OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class)
#Composable
fun App() {
Box(modifier = Modifier.fillMaxSize().padding(10.dp)
) {
val animatedpr = remember { androidx.compose.animation.core.Animatable(initialValue = 0.8f) }
val stateVertical = rememberLazyListState(0)
LaunchedEffect(Unit){animatedpr.animateTo(targetValue = 1f, animationSpec = tween(300, easing = LinearEasing))}
LazyColumn(modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, state = stateVertical) {
items(1100) {
OutlinedCard(modifier = Modifier.size(500.dp, 100.dp).padding(20.dp).animateItemPlacement().graphicsLayer(scaleY = animatedpr.value, scaleX = animatedpr.value)) {
}
}
}
VerticalScrollbar(
modifier = Modifier.align(Alignment.CenterEnd).fillMaxHeight(),
adapter = rememberScrollbarAdapter(
scrollState = stateVertical
)
)
}
}
better sollution
if(it.changes.first().scrollDelta.y == 1.0f){
scope.launch { stateVertical.animateScrollBy(200.0f) }
}
else{
scope.launch {
scope.launch { stateVertical.animateScrollBy(-200.0f) }
}
}
The problem is that you use a single scroll state for two different scroll views. This is causing the jumping while recomposing.
I fixed it by adding "scrollhandler"(onPointerEvent(PointerEventType.Scroll))
val scope = rememberCoroutineScope() // coroutine for scroling(idk, i know coroutines very bad)
val stateVertical = rememberLazyListState(0)
.....
LazyColumn(modifier = Modifier.fillMaxSize().onPointerEvent(PointerEventType.Scroll){
var currentItem = stateVertical.layoutInfo.visibleItemsInfo[0].index
val itemsToScrolling = stateVertical.layoutInfo.visibleItemsInfo.size/2 // how many items we scrolling
if(it.changes.first().scrollDelta.y == 1.0f){ // scroll down
scope.launch { stateVertical.animateScrollToItem(currentItem+itemsToScrolling) }
}
else{ // scroll up
if(currentItem < itemsToScrolling){currentItem = itemsToScrolling} // because we cannot animate to negative number
scope.launch { stateVertical.animateScrollToItem(currentItem-itemsToScrolling) }
}
}, state = stateVertical){
*items*
}
How can I properly handle number in text component in jetpack compose (with MVVM pattern)
Please note that the price can be null or have a value (maybe 0 btw)
I have a poor implementation for now, I changed the keyboard like this :
OutlinedTextField(
value = if (vm.viewState.collectAsState().value.price != null) vm.viewState.collectAsState().value.price.toString() else "",
onValueChange = { vm.onProductPriceChange(it) },
label = { Text(stringResource(id = R.string.price)) },
keyboardOptions = KeyboardOptions(
capitalization = KeyboardCapitalization.None,
autoCorrect = true,
keyboardType = KeyboardType.Number
),
)
and for onValueChange :
fun onProductPriceChange(it: Any) {
if (it.toString() == "") {
_viewState.value = _viewState.value.copy(price = null)
} else {
try
{
_viewState.value = _viewState.value.copy(price = it.toString().toDouble())
}
catch (e: NumberFormatException)
{ // dismiss the bad entries
}
}
}
there can be multiple bad output of the user for example write 22..0 (I dismissed them which is a workaround acceptable)
but there are bad behaviour, when you want to write 10, it will convert it to 10.0. it is not huge but it has backwards
when you delete number in the EditText, 10.0 will become 10.0 and then 100.0 and then 10.0 and finally 1.0. btw it is impossible to go back to the null value (for this case, I can consider 0.0 = no value)
I saw that VisualTransformation (https://medium.com/google-developer-experts/hands-on-jetpack-compose-visualtransformation-to-create-a-phone-number-formatter-99b0347fc4f6) could handle my case but the documentation seems complicated
class DoubleVisualTransformation : VisualTransformation {
override fun filter(str: AnnotatedString): TransformedText {
val strNullDouble = str.text.toBigDecimalOrNull()
var transformedString: String
if (str.text == "" || strNullDouble == null)
return TransformedText(AnnotatedString(""), OffsetMapping.Identity)
else if (strNullDouble.toDouble() % 1 == 0.0 && str.text.last() != '.')
transformedString = strNullDouble.toInt().toString()
else
transformedString = str.text
return TransformedText(
text = AnnotatedString(transformedString),
offsetMapping = object : OffsetMapping {
override fun originalToTransformed(offset: Int): Int {
return offset
}
override fun transformedToOriginal(offset: Int): Int {
return offset
}
}
)
}
}
how can I improve the behavior ?
What about not returning a double to your TextField but just the String?
fun onProductPriceChange(it: String) {
if (it == "") {
_viewState.value = _viewState.value.copy(price = null)
} else {
if (it.toDoubleOrNull() != null) {
_viewState.value = _viewState.value.copy(price = it)
}
}
}
I am having an issue where the accessibility focus is going to controls behind the PopUp Window when using a ExposedDropdownMenuBox
If there is a single ExposedDropdownMenuBox everything works as expected, but when I add a second ExposedDropdownMenuBox or another control the focus goes to the second ExposedDropdownMenuBox before going to the PopUp Window.
GIF of single dropdown behavior
https://giphy.com/gifs/gapy0XK1CGmbyltJxU
GIF of two dropdowns on the same screen
https://giphy.com/gifs/WkL5TcMWlumfcGHPmD
Source
#Composable
fun Screen() {
Column (
modifier = Modifier
.wrapContentSize(Alignment.TopCenter)
.padding(top = 48.dp)
) {
Text(
text = stringResource(id = R.string.greeting),
fontSize = 30.sp,
modifier = Modifier.padding(bottom = 24.dp)
)
LocaleDropdownMenu()
Spacer(modifier = Modifier.height(8.dp))
// LocaleDropdownMenu()
}
}
#OptIn(ExperimentalMaterialApi::class)
#Composable
fun LocaleDropdownMenu() {
val localeOptions = mapOf(
R.string.en to "en",
R.string.fr to "fr",
R.string.hi to "hi",
R.string.ja to "ja"
).mapKeys { stringResource(it.key) }
// boilerplate: https://developer.android.com/reference/kotlin/androidx/compose/material/package-summary#ExposedDropdownMenuBox(kotlin.Boolean,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Function1)
var expanded by remember { mutableStateOf(false) }
ExposedDropdownMenuBox(
expanded = expanded,
onExpandedChange = {
expanded = !expanded
}
) {
TextField(
readOnly = true,
value = stringResource(R.string.language),
onValueChange = { },
trailingIcon = {
ExposedDropdownMenuDefaults.TrailingIcon(
expanded = expanded
)
}
)
ExposedDropdownMenu(
expanded = expanded,
onDismissRequest = {
expanded = false
}
) {
localeOptions.keys.forEach { selectionLocale ->
DropdownMenuItem(
onClick = {
expanded = false
// set app locale given the user's selected locale
AppCompatDelegate.setApplicationLocales(
LocaleListCompat.forLanguageTags(
localeOptions[selectionLocale]
)
)
},
content = { Text(selectionLocale) }
)
}
}
}
}
A repository that reproduces this issue is here:
https://github.com/dazza5000/ExposedDropdownMenuBox-accessibility-issue
I have dialog in compose:
#Composable
fun testgtt() {
val saveDialogState = remember { mutableStateOf(false) }
Button(onClick = { saveDialogState.value = true }, modifier = Modifier.testTag(PLACE_TAG)) {
Text(text = "helllow")
}
Dialog(onDismissRequest = { saveDialogState.value = false }) {
Text(text = "helllow",modifier = Modifier.testTag(BUTTON_TAG))
}
}
and want to test it:
#Test
fun das(){
composeTestRule.setContent {
TerTheme {
testgtt()
}
}
composeTestRule.onRoot(useUnmergedTree = true).printToLog("currentLabelExists")
composeTestRule.onNodeWithTag(PLACE_TAG).performClick()
composeTestRule.onNodeWithTag(BUTTON_TAG).assertIsDisplayed()
}
but I get this error:
java.lang.AssertionError: Failed: assertExists.
Reason: Expected exactly '1' node but found '2' nodes that satisfy: (isRoot)
Nodes found:
1) Node #1 at (l=0.0, t=0.0, r=206.0, b=126.0)px
Has 1 child
2) Node #78 at (l=0.0, t=0.0, r=116.0, b=49.0)px
Has 1 child
Inspite of the fact that I see the Dialog itself.
The reason for this error is the line: composeTestRule.onRoot(useUnmergedTree = true).printToLog("currentLabelExists")
onRoot expects a single node, but i suspect both the containing view and the dialog each return their own root (Speculation)
A possible workaround is to instead print both root trees using something like
composeTestRule.onAllNodes(isRoot()).printToLog("currentLabelExists")
use navigation component:
#Composable
fun de(){
val navController = rememberNavController()
Scaffold { innerPadding ->
NavHost(navController, "home", Modifier.padding(innerPadding)) {
composable("home") {
// This content fills the area provided to the NavHost
val saveDialogState = remember { mutableStateOf(true) }
Button(onClick = {
navController.navigate("detail_dialog")
}, modifier = Modifier.testTag(PLACE_TAG)) {
Text(text = "helllow")
}
}
dialog("detail_dialog") {
// This content will be automatically added to a Dialog() composable
// and appear above the HomeScreen or other composable destinations
Dialog(onDismissRequest = {navController.navigate("home")}) {
Card(
shape = RoundedCornerShape(10.dp),
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
// .padding(horizontal = 16.dp)
.padding(vertical = 8.dp),
elevation = 8.dp
){
Text(text = "helllow", modifier = Modifier.testTag(BUTTON_TAG))
}
}
}
}
}
}
As #DanielO said you can use the isRoot() selector, see below. That however prints out the same message as before.
A possible workaround is to instead print both root trees using something like
composeTestRule.onAllNodes(isRoot()).printToLog("currentLabelExists")
You have to distinctivly select which root you are looking for. By using the selectors:
.get( index )
.onFirst()
.onLast()
When added it should look like this:
composeTestRule.onAllNodes(isRoot()).get(1).printToLog("T:")
composeTestRule.onAllNodes(isRoot()).onFirst().printToLog("T:")
composeTestRule.onAllNodes(isRoot()).onLast().printToLog("T:")
The following is my code snippet. I pass in editClick that adds a data class object into chargingViewModel.contractSelfPay, which is observed as itemList state. When I click the icon, I can tell itemList state receives update by having more edit icons that are spaced evenly. However, BasicGrid Row's height is not stretched with Intrinsic.Min.
If I remove IntrinsicSize.Min, even though row's height is stretched, dividers no longer can fillMaxHeight as well as icon columns. without Intrinsic.Min
#Composable
fun ContractSelfPay(chargingViewModel: ChargingViewModel, editClick: () -> Unit = {}) {
val itemList by chargingViewModel.contractSelfPay.observeAsState()
val composeList: List<#Composable () -> Unit> = itemList?.map {
#Composable {
Row {
TempFunc { StyledText(text = it.itemTitle) }
TempFunc { StyledText(text = it.originalPrice.toString()) }
TempFunc { StyledText(text = it.selfPay.toString(), color = self_pay_blue) }
TempFunc { StyledText(text = it.count.toString()) }
TempFunc { StyledText(text = (it.selfPay * it.count).toString()) }
}
}
} ?: listOf()
val total = itemList?.map { (it.selfPay.toInt() * it.count.toInt()) }?.sum() ?: 0
BasicGrid("全自費", composeList, total = total.toString(), editClick = editClick)
}
#Composable
fun BasicGrid(
gridTitle: String,
itemList: List<#Composable () -> Unit>,
total: String = "0",
editClick: () -> Unit = {}
) {
Row(modifier = Modifier.height(IntrinsicSize.Min), verticalAlignment = Alignment.CenterVertically) {
StyledTextBold(text = gridTitle, modifier = Modifier.weight(15f).wrapContentWidth())
VerticalDivider()
Column(
modifier = Modifier.weight(60f)
) {
itemList.forEachIndexed { index, compose ->
compose()
if (index != itemList.size - 1)
HorizontalDivider()
}
if (itemList.isEmpty())
StyledText(text = "尚未有任何紀錄", modifier = Modifier.weight(1f).wrapContentSize())
}
VerticalDivider()
StyledTextBold(text = total, modifier = Modifier.weight(15f).wrapContentWidth())
VerticalDivider()
Column(
modifier = Modifier
.weight(10f)
.fillMaxHeight(),
verticalArrangement = Arrangement.SpaceEvenly
) {
itemList.forEachIndexed { index, detail ->
Image(
painter = painterResource(R.drawable.icon_mode_edit),
contentDescription = "",
modifier = Modifier
.align(Alignment.CenterHorizontally)
.clickable { editClick() },
)
if (itemList.isNotEmpty() && index != itemList.size - 1)
HorizontalDivider()
}
}
}
}
I have created issue here https://issuetracker.google.com/issues/217910352. Hopefully it gets solved.
One of the work-arounds I could think of is keeping track of height and removing IntrinsicSize.Min.
As in:
// _key_ is something that causes change of the height of the row
var height by remember(_key_) { mutableStateOf(0) }
Row(Modifier.onSizeChanged { height = it.height }) {
VerticalDivider(Modifier.height(height))
}
In your case I suppose key would be size of itemList.
Thank you Majkeee. It's been a while. The way I fixed it at the time was with custom layout modifier. Not sure if it still works today though.
fun Modifier.expandHeight() = this.then(layout { measurable, constraints ->
val placeable =
measurable.measure(constraints.copy(maxHeight = Constraints.Infinity))
layout(placeable.width, placeable.height) {
placeable.placeRelative(0, 0)
}
})
and to use it you can do
Column(modifier = Modifier.expandHeight())