I'm currently applying what I've learnt on compose with a little app, and I wanted to implement a button that has a loader replacing the text when the action is loading.
To do that, I want to implement a button that has either a Text composable or a CircularProgressIndicator, whether the data is loading or not, so the 2 composables are never in the button at the same time.
My problem is that with my implementation, only one of them exists at a time, considering I use a state to define if the button is loading or not.
Screenshot of the idle state:
Screenshot of the loading state (same scale):
Has anybody already encountered this kind of problem? I could put the CircularProgressIndicator at the end of the text but if I can, I would prefer to display one or the other.
Composable:
#Composable
fun ButtonWithLoader(
modifier: Modifier,
isLoading: Boolean,
title: String,
onClickAction: (() -> Unit)? = null
) {
Button(
modifier = modifier,
shape = RoundedCornerShape(50),
onClick = { onClickAction?.invoke() }
) {
if (isLoading) {
CircularProgressIndicator(
modifier = Modifier
.padding(MajorDimens.normal),
color = MajorColor.White
)
} else {
Text(
modifier = Modifier
.padding(MajorDimens.normal),
text = title,
style = MajorFonts.buttonText
)
}
}
}
This is because by default minimum touch target size for CircularProgressIndicator is 48.dp. When it's in composition when isLoading true content height of Button is calculated as 48.dp
You can set a default height for your content so it won't change unless Text height is bigger than 48.dp.
#Composable
fun ButtonWithLoader(
modifier: Modifier,
isLoading: Boolean,
title: String,
onClickAction: (() -> Unit)? = null
) {
Button(
modifier = modifier,
shape = RoundedCornerShape(50),
onClick = { onClickAction?.invoke() }
) {
Box(
modifier = Modifier.height(48.dp),
contentAlignment = Alignment.Center
) {
if (isLoading) {
CircularProgressIndicator(
modifier = Modifier,
color = Color.White
)
} else {
Text(
modifier = Modifier,
text = title,
)
}
}
}
}
If Text can be bigger than 48.dp you can set minimum height so it will be set to height of bigger one and won't change
#Composable
fun ButtonWithLoader(
modifier: Modifier,
isLoading: Boolean,
title: String,
onClickAction: (() -> Unit)? = null
) {
Button(
modifier = modifier,
shape = RoundedCornerShape(50),
onClick = { onClickAction?.invoke() }
) {
Box(
modifier = Modifier.heightIn(min = 48.dp),
contentAlignment = Alignment.Center
) {
if (isLoading) {
CircularProgressIndicator(
modifier = Modifier,
color = Color.White
)
} else {
Text(
modifier = Modifier,
text = title,
fontSize = 40.sp
)
}
}
}
}
Related
I am learning Jetpack Compose, and I've created a few set of buttons as a practice.
This is the button
#Composable
fun MyButton(
text: String,
modifier: Modifier = Modifier,
isEnabled: Boolean = true,
onClick: () -> Unit,
) {
Button(
enabled = isEnabled,
onClick = { onClick() },
modifier = modifier.width(270.dp).wrapContentHeight(),
) {
Text(
text = text,
style = MaterialTheme.typography.button
)
}
}
The problem is, that if i set the height of the button to wrapContentHeight or use heightIn with different max and min values, compose automatically adds a space around the button as seen here
But if i remove WrapContent, and use a fixed height, or define same min and max height for heightIn this probblem does not appear
#Composable
fun MyButton(
text: String,
modifier: Modifier = Modifier,
isEnabled: Boolean = true,
onClick: () -> Unit,
) {
Button(
enabled = isEnabled,
onClick = { onClick() },
modifier = modifier.width(270.dp).height(36.dp),
) {
Text(
text = text,
style = MaterialTheme.typography.button
)
}
}
And this is the code used for the column/preview of the functions:
#Composable
private fun SampleScreen() {
MyTheme{
Surface(modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background,){
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(10.dp, Alignment.CenterVertically),
modifier = Modifier.padding(20.dp)
) {
var isEnabled by remember { mutableStateOf(false) }
MyButton("Enable/Disable") {
isEnabled = !isEnabled
}
MyButton("Button") {}
MyButton(text = "Disabled Button", isEnabled = isEnabled) {}
}
}
}
}
Even if I remove the spacedBy operator from column the same issue appears.
I have tried to search for an explanation to this, but I did not manage to find anything.
Any help or resource with explanations is appreciated.
This is because Minimum dimension of Composables touch area is 48.dp by default for accessibility. However you can override this by using
CompositionLocalProvider(LocalMinimumTouchTargetEnforcement provides false) {
Button(
enabled = isEnabled,
onClick = { onClick() },
modifier = modifier.heightIn(min = 20.dp)
.widthIn(min = 20.dp),
) {
Text(
text = text,
style = MaterialTheme.typography.button
)
}
}
Or something like
Button(modifier = Modifier.absoluteOffset((-12).dp, 0.dp)){
Text(
text = text,
style = MaterialTheme.typography.button
)
}
I am working with Jetpack Compose and I have a page with several TextFields, where I want that, in several of them, when I click on the input, instead of appearing the keyboard, a ModalSheetLayout appears with different layouts that I have. Is this possible? I'm still not able to do this, all I can do is simple things when the focus changes. Can anyone help me?
The below sample should give a basic idea of how to do this.
Code
#OptIn(ExperimentalMaterialApi::class)
#Composable
fun BottomSheetSelectionDemo() {
val coroutineScope: CoroutineScope = rememberCoroutineScope()
val modalBottomSheetState: ModalBottomSheetState = rememberModalBottomSheetState(
initialValue = ModalBottomSheetValue.Hidden,
)
val colors = arrayListOf("Red", "Green", "Blue", "White", "Black")
val (value, setValue) = remember {
mutableStateOf(colors[0])
}
val toggleModalBottomSheetState = {
coroutineScope.launch {
if (!modalBottomSheetState.isAnimationRunning) {
if (modalBottomSheetState.isVisible) {
modalBottomSheetState.hide()
} else {
modalBottomSheetState.show()
}
}
}
}
ModalBottomSheetLayout(
sheetState = modalBottomSheetState,
sheetContent = {
LazyColumn {
items(colors) {
Text(
text = it,
modifier = Modifier
.fillMaxWidth()
.clickable {
setValue(it)
toggleModalBottomSheetState()
}
.padding(
horizontal = 16.dp,
vertical = 12.dp,
),
)
}
}
},
) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize(),
) {
MyReadOnlyTextField(
value = value,
label = "Select a color",
onClick = {
toggleModalBottomSheetState()
},
modifier = Modifier
.fillMaxWidth()
.padding(
horizontal = 16.dp,
vertical = 4.dp,
),
)
}
}
}
#Composable
fun MyReadOnlyTextField(
modifier: Modifier = Modifier,
value: String,
label: String,
onClick: () -> Unit,
) {
Box(
modifier = modifier,
) {
androidx.compose.material3.OutlinedTextField(
value = value,
onValueChange = {},
modifier = Modifier
.fillMaxWidth(),
label = {
Text(
text = label,
)
},
)
Box(
modifier = Modifier
.matchParentSize()
.alpha(0f)
.clickable(
onClick = onClick,
),
)
}
}
I created custom button and when I apply elevation with disable state it has because of shadow some small box in middle and it looks like this. Here is also my code:
#Composable
fun PrimaryButton(
modifier: Modifier = Modifier,
enabled: Boolean = false,
text: String,
onClick: () -> Unit,
) {
Button(
onClick = onClick,
Modifier
.height(44.dp)
.shadow(
elevation = 4.dp,
shape = RoundedCornerShape(22.dp),
)
.then(modifier),
colors = ButtonDefaults.buttonColors(
backgroundColor = Style.colors.buttonPrimary,
disabledBackgroundColor = Style.colors.buttonPrimary.copy(0.4f),
),
shape = RoundedCornerShape(22.dp),
enabled = enabled,
) {
Text(
text = text,
style = Style.typography.phoenixTitle,
color = Color.White
)
}
}
fun ButtonSelectedCount(
modifier: Modifier = Modifier,
text: String,
enable: Boolean,
onClick: () -> Unit
) {
Surface(
shape = RoundedCornerShape(8.dp),
modifier = modifier,
elevation = if (enable) 9.dp else 0.dp
) {
Text(
modifier = Modifier
.fillMaxSize()
.background(color = if (enable) Color.Red else Color.Red.copy(alpha = 0.1f))
.clickable(enable) {
onClick()
}
.wrapContentHeight(),
text = text,
style = Typography.button.copy(color = Color.White),
textAlign = TextAlign.Center
)
}
}
I Think, This is what you are looking for. Wish you early success
The title isn't very clear but if you try this code, you'll see that there is no text (for isLoading = false).
#Composable
fun RefreshButton(
isLoading: Boolean,
onClick: () -> Unit) {
Chip(
label = {
if (isLoading) {
CircularProgressIndicator(
modifier = Modifier
.fillMaxSize()
.wrapContentSize(Alignment.Center)
.padding(32.dp)
)
} else {
Text(
text = "Refresh", modifier = Modifier
.fillMaxSize()
.wrapContentSize(Alignment.Center)
.padding(32.dp)
)
}
},
onClick = { onClick() },
modifier = Modifier.width(intrinsicSize = IntrinsicSize.Max)
)
}
It works without the padding but I would like to add some padding.
I use wear compose and wear compose-foundation 1.0.0-alpha20.
My answer below was wrong. Compose components tend to have hardcoded values like size and padding. This is true for Chip.kt. Here's an answer explaining a related question.
bylazy is correct. You should apply padding to the parent, not the
content
isLoading: Boolean,
onClick: () -> Unit) {
Chip(
label = {
if (isLoading) {
CircularProgressIndicator(
modifier = Modifier
.fillMaxSize()
.wrapContentSize(Alignment.Center)
)
} else {
Text(
text = "Refresh", modifier = Modifier
.fillMaxSize()
.wrapContentSize(Alignment.Center)
)
}
},
onClick = { onClick() },
modifier = Modifier
.width(intrinsicSize = IntrinsicSize.Max)
.padding(32.dp)
) }
I have a Dropdown Composable View that can use as a selection.
#Composable
fun <T> DropdownDemo(modifier: Modifier, items: List<T>, selected: MutableState<T>) {
var expanded by remember { mutableStateOf(false) }
Box(modifier = modifier.fillMaxWidth()) {
Text(
selected.value.toString(),
modifier = Modifier
.background(Color.LightGray)
.fillMaxWidth()
.clickable(
onClick = { expanded = true })
.padding(16.dp, 0.dp)
)
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false },
modifier = Modifier.fillMaxWidth(),
) {
items.forEachIndexed { index, select ->
DropdownMenuItem(
onClick = {
selected.value = items[index]
expanded = false
},
modifier = Modifier.fillMaxWidth(),
) {
Text(text = select.toString(), modifier = Modifier.fillMaxWidth())
}
}
}
}
}
When no click on, the Text shown is fill up the entire width. However, when clicked and expanded, the pop up cannot be expanded fully, even though I set Modifier.fillMaxWidth() in the DropdownMenu and DropdownMenuItem.
How can I get the pop up of the DropdownMenu wider?
You can copy DropdownMenuItemContent and modify it to allow passing in the min and max width
#Composable
fun AdjustableWidthDropdownMenuItem(
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
contentPadding: PaddingValues = MenuDefaults.DropdownMenuItemContentPadding,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
minWidth: Dp = 112.dp,
maxWidth: Dp = 280.dp,
content: #Composable RowScope.() -> Unit,
) {
Row(
modifier = modifier
.clickable(
enabled = enabled,
onClick = onClick,
interactionSource = interactionSource,
indication = rememberRipple(true)
)
.fillMaxWidth()
.sizeIn(
minWidth = minWidth,
maxWidth = maxWidth,
minHeight = 48.dp
)
.padding(contentPadding),
verticalAlignment = Alignment.CenterVertically
) {
val typography = MaterialTheme.typography
ProvideTextStyle(typography.subtitle1) {
val contentAlpha = if (enabled) ContentAlpha.high else ContentAlpha.disabled
CompositionLocalProvider(LocalContentAlpha provides contentAlpha) {
content()
}
}
}
}