I'm coding an app in Android Compose.
I have a TextField with text in known language, say German.
When a user taps the TextField, the keyboard pops up.
I want the keyboard to pop up preset with German language (given it is present on the phone) to save the user few taps.
How can I do it?
I don't think there is an API now for this feature.
This is the signature for TextField,
#Composable
fun TextField(
value: TextFieldValue,
onValueChange: (TextFieldValue) -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
readOnly: Boolean = false,
textStyle: TextStyle = LocalTextStyle.current,
label: #Composable (() -> Unit)? = null,
placeholder: #Composable (() -> Unit)? = null,
leadingIcon: #Composable (() -> Unit)? = null,
trailingIcon: #Composable (() -> Unit)? = null,
isError: Boolean = false,
visualTransformation: VisualTransformation = VisualTransformation.None,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
keyboardActions: KeyboardActions = KeyboardActions(),
singleLine: Boolean = false,
maxLines: Int = Int.MAX_VALUE,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
shape: Shape =
MaterialTheme.shapes.small.copy(bottomEnd = ZeroCornerSize, bottomStart = ZeroCornerSize),
colors: TextFieldColors = TextFieldDefaults.textFieldColors()
)
The parameter related to Keyboard is keyboardOptions, and if you check the code for KeyboardOptions
class KeyboardOptions constructor(
val capitalization: KeyboardCapitalization = KeyboardCapitalization.None,
val autoCorrect: Boolean = true,
val keyboardType: KeyboardType = KeyboardType.Text,
val imeAction: ImeAction = ImeAction.Default
)
inline class KeyboardType internal constructor(#Suppress("unused") private val value: Int) {
override fun toString(): String {
return when (this) {
Text -> "Text"
Ascii -> "Ascii"
Number -> "Number"
Phone -> "Phone"
Uri -> "Uri"
Email -> "Email"
Password -> "Password"
NumberPassword -> "NumberPassword"
else -> "Invalid"
}
}
...
inline class ImeAction internal constructor(#Suppress("unused") private val value: Int) {
override fun toString(): String {
return when (this) {
None -> "None"
Default -> "Default"
Go -> "Go"
Search -> "Search"
Send -> "Send"
Previous -> "Previous"
Next -> "Next"
Done -> "Done"
else -> "Invalid"
}
}
...
You can only change the keyboard type and the action button, but there is no API to change the language at this time.
Related
I have some LazyColumn (or Column) in Composable fun:
#Composable
fun MyColumn(items: List<Item>) {
val width = 0.dp // max for all amounts
LazyColumn {
items(items) {
Row(modifier = Modifier.fillMaxWidth()) {
Text(text = it.title, modifier = Modifier.weight(1f))
Text(text = it.amount, modifier = Modifier.background(Color.Red).width(width))
}
}
}
}
class Item(val title: String, val amount: String)
How can I measure the width param? It must be equals the longest value for all amounts in items.
Finally I got some solution (I don't know is it correct or no, but it works):
I use SubcomposeLayout to measure width of whole separate column with amounts. Code of the measuring function:
#Composable
fun WithCalculateWidth(modifier: Modifier = Modifier,
contentForCalculate: #Composable () -> Unit,
dependentContent: #Composable (Dp) -> Unit) {
SubcomposeLayout(modifier = modifier) { constraints ->
val measuredWidth = subcompose("viewToMeasure", contentForCalculate)[0].measure(Constraints()).width.toDp()
val contentPlaceable = subcompose("content") { dependentContent(measuredWidth) }[0].measure(constraints)
layout(contentPlaceable.width, contentPlaceable.height) {
contentPlaceable.place(0, 0)
}
}
}
I use calculated width in my List:
#Composable
fun ListWithMeasuredAmounts(items: List<Item>) {
val allAmounts: #Composable () -> Unit = {
Column {
items.forEach {
AmountItem(amount = it.amount)
}
}
}
WithCalculateWidth(contentForCalculate = { allAmounts() }) { amountWidth ->
LazyColumn {
items(items) {
Row(modifier = Modifier.fillMaxWidth()) {
Text(text = it.title, modifier = Modifier.weight(1f))
AmountItem(amount = it.amount, modifier = Modifier.width(amountWidth))
}
}
}
}
}
#Composable
fun AmountItem(amount: String, modifier: Modifier = Modifier) {
Text(text = amount, modifier = modifier.background(Color.Red))
}
I'm writing example-screen with using lazyColumn. I encountered some performance issues on release build. Frame skipping happens when I fast-scroll the list.
All models and composables are stable. My code is below;
Screen record link -> https://imgur.com/a/cvlA8g0
viewModel:
#HiltViewModel
class ExampleViewModel #Inject constructor(
private val repo: ExampleRepository,
) : ViewModel() {
private val _viewState = MutableStateFlow(ItemsViewState())
val viewState = _viewState.asStateFlow()
init {
fetch()
}
private fun fetch() = viewModelScope.launch {
repo.getItems()
.onStart { _viewState.value = _viewState.value.copy(state = PageState.Loading) }
.onCompletion { _viewState.value = _viewState.value.copy(state = PageState.Content) }
.collect { _viewState.value = _viewState.value.copy(items = it.toImmutableList()) }
}
}
viewState and models:
data class ItemsViewState(
val items: ImmutableList<Item> = persistentListOf(),
val state: PageState = PageState.Loading,
)
data class Item(
val id: Int,
val imageUrl: String,
val name: String,
val rating: Double,
val campaignText: String,
val isChecked: Boolean = false,
)
data class ItemViewState(val item: Item) {
fun isRatingVisible(): Boolean = item.rating > 7.0
}
sealed class PageState {
object Content : PageState()
object Loading : PageState()
object Error : PageState()
}
and my composable functions:
#Composable
fun ExampleScreen(
viewModel: ExampleViewModel = hiltViewModel(),
) {
val viewState by viewModel.viewState.collectAsState()
when (viewState.state) {
PageState.Content -> {
ExampleList(viewState = viewState)
}
PageState.Loading -> LoadingScreen()
PageState.Error -> {}
}
}
#Composable
private fun ExampleList(
viewState: ItemsViewState,
) {
LazyColumn(
state = rememberLazyListState(),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.fillMaxSize()
) {
items(viewState.items, key = { it.id }) { item ->
ExampleListItem(item = item)
}
}
}
#Composable
private fun ExampleListItem(item: Item) {
val viewState = ItemViewState(item)
Card(
shape = RoundedCornerShape(8.dp),
backgroundColor = MaterialTheme.colors.background
) {
Row(
modifier = Modifier
.padding(8.dp)
.fillMaxWidth()
.wrapContentHeight()
) {
AsyncImage(
model = item.imageUrl,
contentDescription = viewState.item.name,
contentScale = ContentScale.FillHeight,
modifier = Modifier
.clip(RoundedCornerShape(10.dp))
.width(120.dp)
.height(120.dp),
)
Spacer(modifier = Modifier.width(8.dp))
Column(verticalArrangement = Arrangement.SpaceBetween) {
Row(horizontalArrangement = Arrangement.SpaceEvenly) {
Text(
text = viewState.item.name,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
modifier = Modifier.weight(1f),
)
Icon(imageVector = Icons.Default.List, contentDescription = null)
}
Spacer(modifier = Modifier.height(2.dp))
Row {
if (viewState.isRatingVisible()) {
Spacer(modifier = Modifier.width(4.dp))
Text(
text = viewState.item.rating.toString(),
overflow = TextOverflow.Ellipsis,
maxLines = 1,
)
}
}
Spacer(modifier = Modifier.height(2.dp))
CampaignRow(campaignText = viewState.item.campaignText)
}
}
}
}
#Composable
private fun CampaignRow(
modifier: Modifier = Modifier,
campaignText: String,
) = Row(modifier = modifier) {
Image(
painter = painterResource(androidx.ui.livedata.R.drawable.abc_ic_star_black_16dp),
contentDescription = "",
modifier = Modifier
.wrapContentSize()
.align(Alignment.CenterVertically)
.padding(end = 4.dp)
)
Text(
text = campaignText,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
)
}
I followed google guideline to fix performance issues.
I created Baseline profile
I tested on release build
I used key parameter on lazyColumn
but still happening performance issues. How can I prevent this?
I would like to create a widget in Jetpack Compose (Desktop) with similar functionality as the JSpinner in Swing, i.e. an editable text field and two buttons that increase/decrease the value in the text field. Also, I would like
the value to be validated and to be saved when the spinner loses its focus
the buttons not to be skipped in the navigation, so that the user can navigate directly between multiple spinner text fields
After a lot of trial and error I have figured out the following working version, but I wonder if there is a simpler or more elegant way to do this:
#Composable
fun TextFieldSpinner(
label: #Composable (() -> Unit)?,
lastText: String,
validateText: (String) -> Boolean,
commitText: (String) -> Unit,
onIncrement: () -> Unit,
onDecrement: () -> Unit
) {
val (isEditing, setEditing) = remember { mutableStateOf(false) }
// intermediateTextFieldValue is only used locally to store the temporary state of the TextField while editing
val (intermediateTextFieldValue, setIntermediateTextFieldValue) = remember { mutableStateOf(TextFieldValue(lastText))}
var isError by remember { mutableStateOf(!validateText(lastText)) }
val resetText = { setIntermediateTextFieldValue(TextFieldValue(lastText)) }
if (!isEditing && !isError && lastText != intermediateTextFieldValue.text) {
resetText()
}
val onCommit = {
if (validateText(intermediateTextFieldValue.text)) {
isError = false
commitText(intermediateTextFieldValue.text)
} else {
isError = true
}
}
val onLeaveTextField = {
setEditing(false)
onCommit()
isError = false
}
val onNewFocusState = { newFocusState: Boolean ->
setEditing(newFocusState)
if (!newFocusState)
onCommit()
}
}
TextFieldSpinnerUI(
label = label,
value = intermediateTextFieldValue,
onValueChange = { newTextFieldValue:TextFieldValue ->
setEditing(true)
setIntermediateTextFieldValue(newTextFieldValue)
// eager committing without showing error state
if (validateText(newTextFieldValue.text)) {
isError = false
commitText(newTextFieldValue.text)
}
},
modifier = modifier,
isError = isError,
onIncrement = {
onLeaveTextField()
onIncrement()
},
onDecrement = {
onLeaveTextField()
onDecrement()
},
onFocusChanged = { state ->
onNewFocusState(state.isFocused)
}
)
}
#OptIn(ExperimentalComposeUiApi::class)
#Composable
private fun TextFieldSpinnerUI(
label: #Composable (() -> Unit)?,
value: TextFieldValue,
onValueChange: (TextFieldValue) -> Unit,
onIncrement: () -> Unit,
onDecrement: () -> Unit,
isError: Boolean,
onFocusChanged: (FocusState) -> Unit
) {
Row() {
DecreaseButton(onDecrement)
TextField(
label = label,
value = value,
singleLine = true,
isError = isError,
onValueChange = onValueChange,
modifier = Modifier.onFocusChanged(onFocusChanged)
)
IncreaseButton(onIncrement)
}
}
#Composable
private fun DecreaseButton(
onClick: () -> Unit
) {
IconButton(
onClick = onClick,
modifier = Modifier.focusProperties {this.canFocus = false }
) {
Icon(
imageVector = Icons.Rounded.Remove
)
}
}
#Composable
private fun IncreaseButton(
onClick: () -> Unit
) {
IconButton(
onClick = onClick,
modifier = Modifier.focusProperties { this.canFocus = false }
) {
Icon(
imageVector = Icons.Rounded.Add
)
}
}
In particular, it seems to be hard to have the text field at the same time
to be editable
to be validated, saved and then recomposed with the new value
to have another widget such as the buttons change its value
I have the following composable bottomsheet.
I want to be able to close the bottomsheet either by dragging, clicking the background, and clicking the close button.
#Composable
fun CDSModelBottomSheet(toolBar: #Composable () -> Unit, content: #Composable () -> Unit) {
val modelBottomSheetState = rememberModalBottomSheetState(
initialValue = ModalBottomSheetValue.Expanded
)
ModalBottomSheetLayout(
sheetState = modelBottomSheetState,
sheetShape = RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp),
sheetContent = {
Column {
toolBar()
content()
}
}
) {}
}
#Composable
#Preview
fun PreviewCDSBottomSheet() {
CDSModelBottomSheet(
toolBar = { Toolbar(
title = "Select Account",
trailingIcon = {
IconButton(
modifier = Modifier.size(24.dp),
onClick = {
/* close bottom sheet */
}
) {
Icon(
imageVector = Icons.Filled.Close,
contentDescription = stringResource(R.string.close_bottom_sheet),
tint = Color.Black,
)
}
})},
content = {
LoginMode()
}
)
}
In the trailingIcon I have an onClick event. But not sure how to trigger the bottomsheet to close. Unless I have to pass in the rememberModelBottomSheetState which I don't want to do.
This is the preview
Create a lambda to hide ModalBottomSheet as
val coroutineScope = rememberCoroutineScope()
val hideModalBottomSheet: () -> Unit = { coroutineScope.launch { sheetState.hide()} }
And pass this lambda as parameter to your content by updating toolbar as
toolbar: #Composable (() -> Unit) -> Unit
Full function
#OptIn(ExperimentalMaterialApi::class)
#Composable
fun CDSModelBottomSheet(
toolBar: #Composable (() -> Unit) -> Unit,
content: #Composable () -> Unit
) {
val modelBottomSheetState = rememberModalBottomSheetState(
initialValue = ModalBottomSheetValue.Expanded
)
val coroutineScope = rememberCoroutineScope()
val hideModalBottomSheet: () -> Unit =
{ coroutineScope.launch { modelBottomSheetState.hide() } }
ModalBottomSheetLayout(
sheetState = modelBottomSheetState,
sheetShape = RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp),
sheetContent = {
Column {
toolBar(hideModalBottomSheet)
content()
}
}
) {}
}
And use it as
CDSModelBottomSheet(
toolBar = { hide: () -> Unit ->
Toolbar(
title = "Select Account",
trailingIcon = {
IconButton(
modifier = Modifier.size(24.dp),
onClick = hide
) {
Icon(
imageVector = Icons.Filled.Close,
contentDescription = "Close",
tint = Color.Black,
)
}
}
)
},
content = {
LoginMode()
}
)
I have a data class which provide information of a CarParking Place. This one:
data class ParkingSpace(
val id: Int,
val parkNumber: Int,
val parkState: String
)
fun getParkingSpace() = (1..4).map {
when (it) {
1 -> {
ParkingSpace(
id = it,
parkNumber = 150,
parkState = "Free $it"
)
}
2 -> {
ParkingSpace(
id = it,
parkNumber = 152,
parkState = "Free $it"
)
}
3 -> {
ParkingSpace(
id = it,
parkNumber = 153,
parkState = "Free $it"
)
}
4 -> {
ParkingSpace(
id = it,
parkNumber = 154,
parkState = "Free $it"
)
}
else -> {
ParkingSpace(
id = it,
parkNumber = 150,
parkState = "Free $it"
)
}
}
}
This information is populated in a LazyVerticalGrid through buttons
My goal is:
When you press the button, the Text content of the Button will change into an user name (now is just fixed data) - like saying this park place is reserved to one User
There are two separated buttons, to cancel Reservation. I'm trying to show a different content in the Text's button like "Free". I created this CancelButton for practicing interacting with the content of the buttons.
This is my main Activity with all composables.
class MainActivity : ComponentActivity() {
#ExperimentalCoilApi
#ExperimentalFoundationApi
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ZebraCarPark_ComposeTheme {
ParkingListLayout()
}
}
}
}
#ExperimentalFoundationApi
#ExperimentalCoilApi
#Composable
fun ParkingListLayout() {
Column() {
Box(
modifier = Modifier
.background(Color.LightGray)
.fillMaxWidth()
.padding(horizontal = 0.dp, vertical = 15.dp)
) {
LazyVerticalGrid(
contentPadding = PaddingValues(2.dp),
cells = GridCells.Fixed(4)
) {
items(getParkingSpace()) { parkSpc ->
ParkingPlace(parkSpc)
}
}
}
Divider(
color = Color.Black,
thickness = 2.dp
)
Box(
modifier = Modifier
.background(Color.Green)
) {
Column(
modifier = Modifier
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.SpaceEvenly
) {
Button(onClick = {}) {
Text(text = "Cancel Park Place 1")
}
Button(onClick = {}) {
Text(text = "Cancel Park Place 2")
}
}
}
}
}
#Composable
fun ParkingPlace(parkingSpace: ParkingSpace) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
Text(
modifier = Modifier.padding(5.dp),
text = "${parkingSpace.parkNumber}",
)
Button(
onClick = { },
colors = ButtonDefaults.buttonColors(Color.Green)
) {
Text(
text = parkingSpace.parkState,
maxLines = 1,
softWrap = false,
overflow = TextOverflow.Ellipsis,
modifier = Modifier
.padding(5.dp)
)
}
}
}
I was able to make some progress using State Hoisting, so pressing a button will change the content of the button with a new value and all of this stuff. The problem is when the cancel button enter in action. I'm not able to handle correctly the states of the content of the Text's button. I prefer to remove all that code to show you this cleaner.
Project result: