Jetpack Compose wrapContentHeight/ heightIn is adding aditional space around the buttons - android

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
)
}

Related

Jetpack Compose - imePadding() for AlertDialog

The issue I faced was that I needed AlertDialog with some kind of List items (e. g. LazyColumn) and the TextField to search across these items. I wanted to display all the Dialog layout even when Keyboard is opened. But what I got is a Keyboard that cover some part of Dialog layout itself. I tried to use imePadding() for Dialog's Modifier but seems that Dialog ignoring that. I didn't find any solution for this on the Internet.
My code looks like so:
AlertDialog(
modifier = Modifier.fillMaxWidth()
.padding(AppTheme.margins.edge)
.imePadding(),
onDismissRequest = {
searchText = TextFieldValue("")
viewModel.clearSearchQuery()
dismissCallback?.invoke()
},
text = {
Column(
modifier = Modifier.wrapContentHeight()
) {
Text(
text = stringResource(R.string.dlg_select_content_title),
style = AppTheme.textStyles.hugeTitleText
)
OutlinedTextField(
modifier = Modifier
.fillMaxWidth()
.padding(top = AppTheme.margins.divRegular),
value = searchText,
placeholderText = stringResource(R.string.dlg_select_content_search_placeholder),
onValueChange = { newValue ->
searchText = newValue
viewModel.onSearchTextTyped(newValue.text)
}
)
RadioGroup(
modifier = Modifier
.verticalScroll(rememberScrollState()),
options = labels.map {
RadioOption(
title = it.name,
description = null,
selected = vmState.selectedLabel?.id == it.id,
tag = it.id
)
},
onOptionSelected = {
searchText = TextFieldValue("")
viewModel.clearSearchQuery()
viewModel.saveLabelSelection(it.tag as Int) {
dismissCallback?.invoke()
}
}
)
}
},
properties = DialogProperties(
usePlatformDefaultWidth = false
),
confirmButton = {
// Nothing
}
)
And the result:
I am not able to interact with several last items in list because Keyboard covers it.
I have implemented a solution for this issue. The solution is quite ugly, but working. If someone knows a more elegant solution, feel free to write it in an answer in this question.
Even though the dialog ignores the imePadding() we still can set the height. So, first of all we should to know what screen height available above keyboard.
#Composable
private fun TrickyHeight(
onHeightChanged: (Dp) -> Unit,
) {
val density = LocalDensity.current
Box(
modifier = Modifier
.fillMaxSize()
.imePadding()
.padding(bottom = 30.dp) // additional padding
.onSizeChanged {
onHeightChanged.invoke(with(density) { it.height.toDp() })
}
)
}
Next step is to create wrapper over AlertDialog:
#Composable
fun TrickyDialog(
onDismissRequest: () -> Unit,
confirmButton: #Composable () -> Unit,
dismissButton: #Composable (() -> Unit)? = null,
icon: #Composable (() -> Unit)? = null,
title: #Composable (() -> Unit)? = null,
text: #Composable (() -> Unit)? = null,
shape: Shape = AlertDialogDefaults.shape,
containerColor: Color = AppTheme.colors.surfaceColor,
iconContentColor: Color = AlertDialogDefaults.iconContentColor,
titleContentColor: Color = AlertDialogDefaults.titleContentColor,
textContentColor: Color = AlertDialogDefaults.textContentColor,
tonalElevation: Dp = AlertDialogDefaults.TonalElevation,
properties: DialogProperties = DialogProperties()
) {
val maxDialogHeight = remember { mutableStateOf(0.dp) }
TrickyHeight(onHeightChanged = { maxDialogHeight.value = it })
AlertDialog(
modifier = Modifier
.fillMaxWidth()
.heightIn(0.dp, maxDialogHeight.value)
.padding(AppTheme.margins.edge),
onDismissRequest = onDismissRequest,
confirmButton = confirmButton,
dismissButton = dismissButton,
icon = icon,
title = title,
text = text,
shape = shape,
containerColor = containerColor,
iconContentColor = iconContentColor,
titleContentColor = titleContentColor,
textContentColor = textContentColor,
tonalElevation = tonalElevation,
properties = properties
)
}
Also, do not forget to add correct android:windowSoftInputMode in Manifest: android:windowSoftInputMode="adjustResize"
Now you can use TrickyDialog instead of AlertDialog. Again, this solution is not elegant. But maybe it will be helpful for someone who faced the same issue. Also, this solution will not work properly for Landscape Screen Orientation.
As of Compose UI 1.3.0-beta01, you can set DialogProperties.decorFitsSystemWindows to false and imePadding() will work.
https://issuetracker.google.com/issues/229378542
https://developer.android.com/jetpack/androidx/releases/compose-ui#1.3.0-beta01
AlertDialog(
modifier = Modifier.imePadding(),
properties = DialogProperties(decorFitsSystemWindows = false),
onDismissRequest = {
// ...
},
title = {
// ...
},
text = {
// ...
},
confirmButton = {
// ..
},
)

Button changing size between text and CircularProgressIndicator

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
)
}
}
}
}

How to add a padding to elements inside a Chip without hidding elements inside in wear Compose?

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)
) }

Jetpack Compose onClick ripple is not propagating with a circular motion?

As can be seen in gif
when Column that contains of Text, Spacer, and LazyRowForIndexed is touched ripple is not propagating with circular motion. And it gets touched effect even when horizontal list is touched.
#Composable
fun Chip(modifier: Modifier = Modifier, text: String) {
Card(
modifier = modifier,
border = BorderStroke(color = Color.Black, width = Dp.Hairline),
shape = RoundedCornerShape(8.dp)
) {
Row(
modifier = Modifier.padding(start = 8.dp, top = 4.dp, end = 8.dp, bottom = 4.dp),
verticalAlignment = Alignment.CenterVertically
) {
Box(
modifier = Modifier.preferredSize(16.dp, 16.dp)
.background(color = MaterialTheme.colors.secondary)
)
Spacer(Modifier.preferredWidth(4.dp))
Text(text = text)
}
}
}
#Composable
fun TutorialSectionCard(model: TutorialSectionModel) {
Column(
modifier = Modifier
.padding(top = 8.dp)
.clickable(onClick = { /* Ignoring onClick */ })
.padding(16.dp)
) {
Text(text = model.title, fontWeight = FontWeight.Bold, style = MaterialTheme.typography.h6)
Spacer(Modifier.preferredHeight(8.dp))
Providers(AmbientContentAlpha provides ContentAlpha.medium) {
Text(model.description, style = MaterialTheme.typography.body2)
}
Spacer(Modifier.preferredHeight(16.dp))
LazyRowForIndexed(items = model.tags) { _: Int, item: String ->
Chip(text = item)
Spacer(Modifier.preferredWidth(4.dp))
}
}
}
#Preview
#Composable
fun TutorialSectionCardPreview() {
val model = TutorialSectionModel(
clazz = MainActivity::class.java,
title = "1-1 Column/Row Basics",
description = "Create Rows and Columns that adds elements in vertical order",
tags = listOf("Jetpack", "Compose", "Rows", "Columns", "Layouts", "Text", "Modifier")
)
Column {
TutorialSectionCard(model)
TutorialSectionCard(model)
TutorialSectionCard(model)
}
}
What should be done to have circular effect, but not when list itself or an item from list is touched, or scrolled?
You have to apply a Theme to your composable, which in turn provides a default ripple factory, or you have to set the ripple explicitly:
#Preview
#Composable
fun TutorialSectionCardPreview() {
MaterialTheme() {
Column {
TutorialSectionCard
...
}
}
}
or
Column(
modifier = Modifier
.padding(top = 8.dp)
.clickable(
onClick = { /* Ignoring onClick */ },
indication = rememberRipple(bounded = true)
)
.padding(16.dp)
) {
// content
}
(As of compose version 1.0.0-alpha09 there seems to be no way to prevent the ripple from showing when content is scrolled)
I'm using this approach:
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(
color = Color.Black
),
onClick = {
}
)
I also figured out how to keep ripple only for the card not the scrollable list that contains tags. To prevent ripple only move through cards use a Box which places it's children as a stack, and add clickable to section that contains header and text.
#Composable
fun TutorialSectionCard(
model: TutorialSectionModel,
onClick: ((TutorialSectionModel) -> Unit)? = null
) {
Card(
modifier = Modifier.padding(vertical = 3.dp, horizontal = 8.dp),
elevation = 1.dp,
shape = RoundedCornerShape(8.dp)
) {
Box(
contentAlignment = Alignment.BottomStart
) {
TutorialContentComponent(onClick, model)
TutorialTagsComponent(model)
}
}
}
#Composable
private fun TutorialContentComponent(
onClick: ((TutorialSectionModel) -> Unit)?,
model: TutorialSectionModel
) {
Column(Modifier
.clickable(
onClick = { onClick?.invoke(model) }
)
.padding(16.dp)
) {
Text(
text = model.title,
fontWeight = FontWeight.Bold,
style = MaterialTheme.typography.h6
)
// Vertical spacing
Spacer(Modifier.height(8.dp))
// Description text
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
Text(model.description, style = MaterialTheme.typography.body2)
}
// Vertical spacing
Spacer(Modifier.height(36.dp))
}
}
#Composable
private fun TutorialTagsComponent(model: TutorialSectionModel) {
Column(Modifier.padding(12.dp)) {
// Horizontal list for tags
LazyRow(content = {
items(model.tags) { tag ->
TutorialChip(text = tag)
Spacer(Modifier.width(8.dp))
}
})
}
}

Jetpack Compose UI: How to create SearchView?

I want to create SearchView using jetpack compose, but I can't found any example that could helps me. Thanks in Advance.
This is a complex but full implementation for a SearchView from scratch. And the result will be as in the gif below, you can customize or remove InitialResults or Suggestions if you don't want your initial Composable to be displayed when SearchView is not focused and empty
Full implementation is available in github repository.
1- Create search states with
/**
* Enum class with different values to set search state based on text, focus, initial state and
* results from search.
*
* **InitialResults** represents the initial state before search is initiated. This represents
* the whole screen
*
*/
enum class SearchDisplay {
InitialResults, Suggestions, Results, NoResults
}
2- Then create class where you define your search logic
#Stable
class SearchState(
query: TextFieldValue,
focused: Boolean,
searching: Boolean,
suggestions: List<SuggestionModel>,
searchResults: List<TutorialSectionModel>
) {
var query by mutableStateOf(query)
var focused by mutableStateOf(focused)
var searching by mutableStateOf(searching)
var suggestions by mutableStateOf(suggestions)
var searchResults by mutableStateOf(searchResults)
val searchDisplay: SearchDisplay
get() = when {
!focused && query.text.isEmpty() -> SearchDisplay.InitialResults
focused && query.text.isEmpty() -> SearchDisplay.Suggestions
searchResults.isEmpty() -> SearchDisplay.NoResults
else -> SearchDisplay.Results
}
override fun toString(): String {
return "🚀 State query: $query, focused: $focused, searching: $searching " +
"suggestions: ${suggestions.size}, " +
"searchResults: ${searchResults.size}, " +
" searchDisplay: $searchDisplay"
}
}
3- remember state to not update in every composition but only when our seach state changes
#Composable
fun rememberSearchState(
query: TextFieldValue = TextFieldValue(""),
focused: Boolean = false,
searching: Boolean = false,
suggestions: List<SuggestionModel> = emptyList(),
searchResults: List<TutorialSectionModel> = emptyList()
): SearchState {
return remember {
SearchState(
query = query,
focused = focused,
searching = searching,
suggestions = suggestions,
searchResults = searchResults
)
}
}
TutorialSectionModel is the model i used it can be generic type T or specific type you wish to display
4- Create a hint to be displayed when not focused
#Composable
private fun SearchHint(modifier: Modifier = Modifier) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxSize()
.then(modifier)
) {
Text(
color = Color(0xff757575),
text = "Search a Tag or Description",
)
}
}
I didn't use an Icon but if you wish you can add one
5- Create a SearchTextfield that can has cancel button, CircularProgressIndicator to display loading and BasicTextField to input
/**
* This is a stateless TextField for searching with a Hint when query is empty,
* and clear and loading [IconButton]s to clear query or show progress indicator when
* a query is in progress.
*/
#Composable
fun SearchTextField(
query: TextFieldValue,
onQueryChange: (TextFieldValue) -> Unit,
onSearchFocusChange: (Boolean) -> Unit,
onClearQuery: () -> Unit,
searching: Boolean,
focused: Boolean,
modifier: Modifier = Modifier
) {
val focusRequester = remember { FocusRequester() }
Surface(
modifier = modifier
.then(
Modifier
.height(56.dp)
.padding(
top = 8.dp,
bottom = 8.dp,
start = if (!focused) 16.dp else 0.dp,
end = 16.dp
)
),
color = Color(0xffF5F5F5),
shape = RoundedCornerShape(percent = 50),
) {
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
Box(
contentAlignment = Alignment.CenterStart,
modifier = modifier
) {
if (query.text.isEmpty()) {
SearchHint(modifier.padding(start = 24.dp, end = 8.dp))
}
Row(verticalAlignment = Alignment.CenterVertically) {
BasicTextField(
value = query,
onValueChange = onQueryChange,
modifier = Modifier
.fillMaxHeight()
.weight(1f)
.onFocusChanged {
onSearchFocusChange(it.isFocused)
}
.focusRequester(focusRequester)
.padding(top = 9.dp, bottom = 8.dp, start = 24.dp, end = 8.dp),
singleLine = true
)
when {
searching -> {
CircularProgressIndicator(
modifier = Modifier
.padding(horizontal = 6.dp)
.size(36.dp)
)
}
query.text.isNotEmpty() -> {
IconButton(onClick = onClearQuery) {
Icon(imageVector = Icons.Filled.Cancel, contentDescription = null)
}
}
}
}
}
}
}
}
You can remove CircularProgressBar or add Icon to Row which contains BasicTextField
6- SearchBar with SearchTextField above and back arrow to return back feature with. AnimatedVisibility makes sure arrow is animated when we focus BasicTextField in SearchTextField, it can also be used with Icon as magnifying glass.
#ExperimentalAnimationApi
#OptIn(ExperimentalComposeUiApi::class)
#Composable
fun SearchBar(
query: TextFieldValue,
onQueryChange: (TextFieldValue) -> Unit,
onSearchFocusChange: (Boolean) -> Unit,
onClearQuery: () -> Unit,
onBack: ()-> Unit,
searching: Boolean,
focused: Boolean,
modifier: Modifier = Modifier
) {
val focusManager = LocalFocusManager.current
val keyboardController = LocalSoftwareKeyboardController.current
Row(
modifier = modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
AnimatedVisibility(visible = focused) {
// Back button
IconButton(
modifier = Modifier.padding(start =2.dp),
onClick = {
focusManager.clearFocus()
keyboardController?.hide()
onBack()
}) {
Icon(imageVector = Icons.Default.ArrowBack, contentDescription = null)
}
}
SearchTextField(
query,
onQueryChange,
onSearchFocusChange,
onClearQuery,
searching,
focused,
modifier.weight(1f)
)
}
}
7- To use SearchBar create a rememberSearchState and update state as
Column is used here because rest of the screen is updated based on SearchState
LaunchedEffect or setting mutableState in ViewModel can be used to set query result or searching field of state to display loading
#Composable
fun HomeScreen(
modifier: Modifier = Modifier,
viewModel: HomeViewModel,
navigateToTutorial: (String) -> Unit,
state: SearchState = rememberSearchState()
) {
Column(
modifier = modifier.fillMaxSize()
) {
SearchBar(
query = state.query,
onQueryChange = { state.query = it },
onSearchFocusChange = { state.focused = it },
onClearQuery = { state.query = TextFieldValue("") },
onBack = { state.query = TextFieldValue("") },
searching = state.searching,
focused = state.focused,
modifier = modifier
)
LaunchedEffect(state.query.text) {
state.searching = true
delay(100)
state.searchResults = viewModel.getTutorials(state.query.text)
state.searching = false
}
when (state.searchDisplay) {
SearchDisplay.InitialResults -> {
}
SearchDisplay.NoResults -> {
}
SearchDisplay.Suggestions -> {
}
SearchDisplay.Results -> {
}
}
}
}
This is the SearchView you have in that image :
val (value, onValueChange) = remember { mutableStateOf("") }
TextField(
value = value,
onValueChange = onValueChange,
textStyle = TextStyle(fontSize = 17.sp),
leadingIcon = { Icon(Icons.Filled.Search, null, tint = Color.Gray) },
modifier = Modifier
.padding(10.dp)
.background(Color(0xFFE7F1F1), RoundedCornerShape(16.dp)),
placeholder = { Text(text = "Bun") },
colors = TextFieldDefaults.textFieldColors(
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
backgroundColor = Color.Transparent,
cursorColor = Color.DarkGray
)
)
TextField(
startingIcon = Icon(bitmap = searchIcon),
placeholder = { Text(...) }
)
Just create component, with FlexRow if you want to create UI like those.
FlexRow(crossAxisAlignment = CrossAxisAlignment.Start) {
inflexible {
drawImageResource(R.drawable.image_search)
}
expanded(1.0f) {
SingleLineEditText(
state,
keyboardType = KeyboardType.Text,
imeAction = ImeAction.Search,
editorStyle = EditorStyle(textStyle = TextStyle(fontSize = 16.sp)),
onImeActionPerformed = {
onSearch(state.value.text)
}
)
}
}

Categories

Resources