I want to use verticalArrangement = Arrangement.SpaceBetween in Column to align view top and bottom. I did this before without any problem. Now I added AnimatedVisibility to look good animation when my condition true. But It overlapping the view. I can't understand that things.
PairContentStateLess
#Composable
fun PairContentStateLess(
viewModel: XyzPairViewModel,
scanning: State<Boolean>,
onResume: () -> Unit,
onPause: () -> Unit,
tryAgainAction: () -> Unit,
openSettingAction: () -> Unit,
onResumeScan: () -> Unit,
onPauseScan: () -> Unit,
) {
AnimatedVisibility(visible = true) {
AppBarScaffold() {
Column(
modifier = Modifier
.padding(10.dp)
.fillMaxSize()
.verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = if (viewModel.isBluetoothEnabled && scanning.value) {
Arrangement.Top
} else {
Arrangement.SpaceBetween
}
) {
PairScreenImage()
PairDeviceDescription()
if (viewModel.isBluetoothEnabled) {
PressAndHoldDescription()
WaitingToPair(scanning.value)
UnableToPair(scanning.value)
} else {
BluetoothTurnOnWarning()
Spacer(modifier = Modifier.weight(1f))
TryAgainButtonView { tryAgainAction() }
OpenDeviceSettingsButtonView { openSettingAction() }
}
}
}
}
}
I am only adding UnableToPair code now. If I am using it working fine without any problem.
UnableToPair
fun ColumnScope.UnableToPair(scanning: Boolean) {
if (!scanning) {
WarningBoxView()
Spacer(modifier = Modifier.weight(1f))
TryAgainButtonView {
}
}
}
UI look like this
Now main problems come
when I tried to use AnimatedVisibility. I don't know what happened in the WarningBoxView and TryAgainButtonView.
#Composable
fun ColumnScope.UnableToPair(scanning: Boolean) {
AnimatedVisibility (!scanning) {
WarningBoxView()
Spacer(modifier = Modifier.weight(1f))
TryAgainButtonView {
}
}
}
UI look like this and I hide sensitive data from black box in image. Please notice only button and warning box.
Thanks
use column inside animated visibility again
Related
I have AppBarNavGraph and RootNavGraph.
I'd like to know two methods, Full Screen Dialog and also Navigating to a new Screen.
What I am trying to is..
Column(
modifier = Modifier
.width(97.dp)
.clickable {
Dialog(onDismissResult = { /*TODO*/ }) {
}
}
,
horizontalAlignment = Alignment.CenterHorizontally
But in clickable, composable function is not callable.
So, What I tried is using this,
val isClicked = remember { mutableStateOf(false) }
Column(
modifier = Modifier
.width(97.dp)
.clickable {
isClicked.value = !isClicked.value
},
horizontalAlignment = Alignment.CenterHorizontally
)
if(isClicked.value){
// show full screen dialog or navigate to some screen.
}
Here are two problems.
I tried with Dialog function.
Dialog(
onDismissResult: () -> Unit,
properties: DialogProperties = DialogProperties(),
content: #Composable () -> Unit
)
but, this doesn't show full screen but just next item. (it's in GridLayout and when I click an item, the dialog needs to shows up to add the item.)
For navigation, I tried, varies ways, but I haven't found any clear solutions...
Could you please help me out? or any advices?
This should do the trick for a simple full screen popup
var showPopup by remember { mutableStateOf(false) }
Box(Modifier
.background(Color.Blue)
.fillMaxSize()) {
Button(onClick = { showPopup = true }) {
Text("Show popup")
}
}
if (showPopup) {
Popup(
onDismissRequest = { showPopup = false }
) {
Box(Modifier
.background(Color.Red)
.fillMaxSize()) {
Button(onClick = { showPopup = false },
modifier = Modifier.align(Alignment.Center)) {
Text("Hide popup")
}
}
}
}
Though if you want proper navigation you should probably use this
I want to make a button to bottom of screen, but I am seeing overlap with my button. I don't understand why this is happening in my application.
PairContentStateLess
#Composable
fun PairContentStateLess(
viewModel: XyzViewModel,
scanning: State<Boolean>,
onResume: () -> Unit,
onPause: () -> Unit,
tryAgainAction: () -> Unit,
openSettingAction: () -> Unit,
onResumeScan: () -> Unit,
onPauseScan: () -> Unit,
) {
AnimatedVisibility(visible = true) {
AppBarScaffold() {
Column(
modifier = Modifier
.padding(10.dp)
.fillMaxSize()
.verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = if (viewModel.isBluetoothEnabled) {
Arrangement.Top
} else {
Arrangement.SpaceBetween
}
) {
DisposeOnLifecycleManager(
onCreate = {
onResumeScan()
},
onResume = {
onResume()
},
onPause = {
onPause()
onPauseScan()
}
)
PairScreenImage()
PairDeviceDescription()
if (viewModel.isBluetoothEnabled) {
PressAndHoldDescription()
WaitingToPair(scanning.value)
UnableToPair(scanning.value)
} else {
BluetoothTurnOnWarning()
Spacer(modifier = Modifier.weight(1f))
TryAgainButtonView { tryAgainAction() }
OpenDeviceSettingsButtonView { openSettingAction() }
}
}
}
}
}
I divided all views into simple function. I am adding simple code in each function.
PairScreenImage
#Composable
fun PairScreenImage() {
// image only
}
PairDeviceDescription
#Composable
fun PairDeviceDescription() {
// text only
}
PressAndHoldDescription
#Composable
fun PressAndHoldDescription() {
// text only
}
WaitingToPair
#Composable
fun ColumnScope.WaitingToPair(scanning: Boolean) {
Spacer(modifier = Modifier.height(10.dp))
AnimatedVisibility(scanning) {
Row(
modifier = Modifier
.fillMaxWidth()
.background(OffWhite),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
// more items here
}
}
}
UnableToPair
I am adding this function code which is overlap through my view. Can anyone know why TryAgainButtonView view overlap with WarningBoxView()?
#Composable
fun ColumnScope.UnableToPair(scanning: Boolean) {
AnimatedVisibility(!scanning) {
WarningBoxView()
TryAgainButtonView {
}
}
}
View overlap look like this
I want TryAgainButtonView to bottom of screen.
My Component tree from Layout inspector
Anyone guide me what I am doing here wrong? Thanks
I believe this is because you are using Column and not correctly fitting everything on the screen. You hide the image and description composables and it is ok but I think you are giving static dp values in these composables. Rather than giving a size with either .width, .height or .size modifiers, use .padding and / or calculate width|height of the composables by using LocalConfiguration.current to properly size the composables. I believe you will see better results.
Additionally, your Column takes space of the whole screen and tries to fit every child on the possible available space. If you are ok with a scrolling screen, use LazyColumn.
Introduction
I made a wrapper which gives a Composable a disabled look and prevents click events from being passed to it.
#Composable
fun DisableOverlay(enabled: Boolean, alpha: Float = 0.45F, content: #Composable () -> Unit) {
if (enabled) {
content()
} else {
Box(
modifier = Modifier
.wrapContentWidth()
.wrapContentHeight()
.alpha(alpha)
) {
content()
Box(modifier = Modifier
.matchParentSize()
.clickable(enabled = false){
//absorb the clicks
}
)
}
}
}
Below is an example usage.
DisableOverlay(enabled = false) {
Column{
Text(text = "Some Text")
}
}
The Problem
It works fine but has some issues with accessibility reader (TalkBack) is on. The reader does not read the content Composable on some devices. It reads the text on Pixel devices but not Samsung. I noticed on the Pixel device it read the text content but does not honour any semantics set on it i.e. a contentDescription.
For example, the reader would not read "my content description" on this Text.
DisableOverlay(enabled = false) {
Column{
Text(text = "Some Text",
modifier = Modifier.semantics {
contentDescription = "my content description"
})
}
}
Attempted Solution 1 (FAILED)
I added clearAndSetSemantics on the Box that overlaps the content composable. My theory was the reader can ignore the element and just read out content. This did not work, the reader completely skips the content.
fun DisableOverlay(enabled: Boolean, alpha: Float = 0.45F, content: #Composable () -> Unit) {
if (enabled) {
content()
} else {
Box(
modifier = Modifier
.wrapContentWidth()
.wrapContentHeight()
.alpha(alpha)
) {
content()
Box(modifier = Modifier
.matchParentSize()
.clearAndSetSemantics {
}
.clickable(enabled = false){
//absorb the clicks
}
)
}
}
}
Attempted Solution 2 (FAILED)
I found a property called invisibleToUser(). This had the same effect as clearAndSetSemantics seen in Solution 1.
.semantics {
invisibleToUser()
}
Attempted Solution 3 (FAILED)
I looked up a different way to disable click events using a blank pointerInput implementation. Since there is no clickable modifier the accessibility reader reads out the content composable and honours its semantics.
There is a deal breaker though. While this blocked touch events the accessibility reader is still able to select it.
#Composable
fun DisableOverlay(enabled: Boolean, alpha: Float = 0.45F, content: #Composable () -> Unit) {
if (enabled) {
content()
} else {
Box(
modifier = Modifier
.wrapContentWidth()
.wrapContentHeight()
.alpha(alpha)
) {
content()
Box(modifier = Modifier
.matchParentSize()
.pointerInput(Unit){
//absorb the clicks
}
)
}
}
}
Idea 1
If I set contentDescription on the Box that overlaps content, the reader reads it as expected. Ideally, I don't want to pass through a contentDescription, I want to just extract the semantics from content and just read it out. Is this possible?
#Composable
fun DisableOverlay(enabled: Boolean, alpha: Float = 0.45F, content: #Composable () -> Unit) {
if (enabled) {
content()
} else {
Box(
modifier = Modifier
.wrapContentWidth()
.wrapContentHeight()
.alpha(alpha)
) {
content()
Box(modifier = Modifier
.matchParentSize()
.clickable(enabled = false){
//absorb the clicks
}
.semantics {
contentDescription = "test"
}
)
}
}
}
Idea 2
My overall goal is to allow a Composable to be disabled just by wrapping it in another Composable. If anyone has an alternative solution to the above, please let me know.
Thanks for reading my question.
I'm trying to convert my old XML layout to #Composable classes in a test app I made, but I encountered a problem with my "loading" screen.
The app has a button to fetch quotes from a free API and, when clicked, a loading screen should appear on top of the page, effectively blocking possible further interactions with the button.
The loading screen was previously RelativeLayout with a ProgressBar inside.
Now with Compose I cannot manage to have this loading screen to be "on top" because the buttons still show above it and remain clickable.
The same "wrong" behaviour can also be reproduced with XML layouts when using MaterialButtons, whereas with AppCompatButtons the issue is solved.
Is there a way to make this work in compose?
p.s. here is my solution with Compose
#Composable
fun QuoteButton(text: String, onClick: () -> Unit) {
Button(
onClick,
shape = RoundedCornerShape(20.dp),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp, vertical = 5.dp)
) {
Text(text = text)
}
}
#Composable
fun QuoteLoading(
isLoading: MutableState<Boolean>,
content: #Composable () -> Unit
) = if (isLoading.value) {
Box(
Modifier
.fillMaxSize()
.background(Color.Black.copy(alpha = 0.3f))
.pointerInput(Unit) {}
) {
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
}
content()
} else {
content()
}
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ComposeTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
QuoteLoading(isLoading = loadingState) {
Column {
QuoteDisplay(textState)
QuoteButton(getString(R.string.button_fetch_quote)) {
viewModel.setEvent(Event.GetQuote)
}
QuoteButton(getString(R.string.button_save_quote)) {
viewModel.setEvent(Event.SaveQuote)
}
QuoteButton(getString(R.string.button_clear_quotes)) {
viewModel.setEvent(Event.ClearQuote)
}
}
}
}
}
}
}
}
private val DarkColorPalette = darkColors(
primary = Color(0xFFBB86FC),
primaryVariant = Color(0xFF3700B3),
secondary = Color(0xFF03DAC5)
)
private val LightColorPalette = lightColors(
primary = Color(0xFF6200EE),
primaryVariant = Color(0xFF3700B3),
secondary = Color(0xFF03DAC5)
)
#Composable
fun ComposeTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: #Composable () -> Unit) {
MaterialTheme(
colors = if (darkTheme) DarkColorPalette else LightColorPalette,
content = content
)
}
First of all put your progress bar in a dialogue that is not cancellable by any input except loading has been finished.
#Composable
fun QuoteLoading(
isLoading: MutableState<Boolean>,
content: #Composable () -> Unit
) = if (isLoading.value) {
Box(
Modifier
.fillMaxSize()
.background(Color.Black.copy(alpha = 0.3f))
.pointerInput(Unit) {}
) {
Dialog(
onDismissRequest = onDismiss,
properties = DialogProperties(dismissOnBackPress = false,
dismissOnClickOutside = false),
content = {
CircularProgressIndicator()
}
)
}
content()
} else {
content()
}
here's my situation: I have to show in my app a detail of a record I receive from API. Inside this view, I may or may not need to show some data coming from another viewmodel, based on a field.
Here my code:
#OptIn(ExperimentalMaterial3Api::class)
#Composable
fun ViewDetail(viewModel: MainViewModel, alias: String?, otherViewModel: OtherViewModel) {
viewModel.get(alias)
Scaffold {
val isLoading by viewModel.isLoading.collectAsState()
val details by viewModel.details.collectAsState()
when {
isLoading -> LoadingUi()
else -> Details(details, otherViewModel)
}
}
}
#OptIn(ExperimentalMaterial3Api::class)
#Composable
private fun Details(details: Foo?, otherViewModel: OtherViewModel) {
details?.let { sh ->
val scrollState = rememberScrollState()
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(scrollState),
) {
Text(sh.title, fontSize = 24.sp, lineHeight = 30.sp)
Text(text = sh.description)
if (sh.other.isNotEmpty()) {
otherViewModel.load(sh.other)
val others by otherViewModel.list.collectAsState()
Others(others)
}
}
}
}
#OptIn(ExperimentalMaterial3Api::class)
#Composable
private fun Others(others: Flow<PagingData<Other>>) {
val items: LazyPagingItems<Other> = others.collectAsLazyPagingItems()
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight(),
contentPadding = PaddingValues(16.dp),
) {
items(items = items) { item ->
if (item != null) {
Text(text = item.title, fontSize = 24.sp)
Spacer(modifier = Modifier.height(4.dp))
Text(text = item.description)
}
}
if (items.itemCount == 0) {
item { EmptyContent() }
}
}
}
All the description here may be very long, both on the main Details body or in the Others (when present), so here's why the scroll behaviour requested.
Problem: I get this error:
Vertically scrollable component was measured with an infinity maximum height constraints, which is disallowed. One of the common reasons is nesting layouts like LazyColumn and Column(Modifier.verticalScroll()).
I hoped that .wrapContentHeight() inside LazyColumn would do the trick, but to no avail.
Is this the right way to do it?
Context: all packages are updated to the latest versions available on maven
The main idea here is to merge your Column with LazyColumn.
As your code is not runnable, I'm giving more a pseudo code, which should theoretically work.
Also calling otherViewModel.load(sh.other) directly from Composable builder is a mistake. According to thinking in compose, to get best performance your view should be side effects free. To solve this issue Compose have special side effect functions. Right now your code is gonna be called on each recomposition.
if (sh.other.isNotEmpty()) {
LaunchedEffect(Unit) {
otherViewModel.load(sh.other)
}
}
val others by otherViewModel.list.collectAsState()
LazyColumn(
modifier = Modifier
.fillMaxSize()
.wrapContentHeight(),
contentPadding = PaddingValues(16.dp),
) {
item {
Text(sh.title, fontSize = 24.sp, lineHeight = 30.sp)
Text(text = sh.description)
}
items(items = items) { item ->
if (item != null) {
Text(text = item.title, fontSize = 24.sp)
Spacer(modifier = Modifier.height(4.dp))
Text(text = item.description)
}
}
if (items.itemCount == 0) {
item { EmptyContent() }
}
}
You can use a system like the following
#Composable
fun Test() {
Box(Modifier.systemBarsPadding()) {
Details()
}
}
#Composable
fun Details() {
LazyColumn(Modifier.fillMaxSize()) {
item {
Box(Modifier.background(Color.Cyan).padding(16.dp)) {
Text(text = "Hello World!")
}
}
item {
Box(Modifier.background(Color.Yellow).padding(16.dp)) {
Text(text = "Another data")
}
}
item {
Others()
}
}
}
#Composable
fun Others() {
val values = MutableList(50) { it }
values.forEach {
Box(
Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Text(text = "Value = $it")
}
}
}
The result with scroll is: