I was wondering if it is possible in Jetpack Compose to get a Gmail like behavior, anyone did something like this before and want to share their solution?
I just want to give the user the opportunity to add tags to their content before uploading, I don't need those pop-up suggestions like in the gif below.
Just simple chips in a InputField
.
CodePudding user response:
Here is how I did it. Basically I just used flow row along with a text field
KoohaHashTagEditor(
textFieldValue = hashTagTextValue,
onValueChanged = {
hashTagError = null
val values = FormUtil.splitPerSpaceOrNewLine(it.text)
if (values.size >= 2) {
if (!FormUtil.isFilled(values[0])) {
hashTagError = "At least 2 characters per tag."
} else if (!FormUtil.checkTagMinimumCharacter(values[0])) {
hashTagError = "At least 2 characters per tag."
} else if (!FormUtil.checkTagMaximumCharacter(values[0])) {
hashTagError = "Up to 50 characters per tag."
}
if (hashTagError == null) {
addHashTag(values[0])
hashTagTextValue = hashTagTextValue.copy(text = "")
}
} else {
hashTagTextValue = it
}
},
focusRequester = hashTagFocusRequester,
focusedFlow = hashTagFocusedFlow.value,
textFieldInteraction = hashTagInteraction,
label = null,
placeholder = "To add a tag, hit the enter or space bar on your keypad after each tag.",
rowInteraction = rowInteraction,
errorMessage = hashTagError,
listOfChips = uiState.hashtags,
modifier = Modifier.onKeyEvent {
if (it.key.keyCode == Key.Backspace.keyCode) {
removeLastTag()
}
false
},
onChipClick = { chipIndex ->
removeTagOnIndex(chipIndex)
}
)
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun KoohaHashTagEditor(
modifier: Modifier = Modifier,
textFieldValue: TextFieldValue,
onValueChanged: (TextFieldValue) -> Unit,
focusRequester: FocusRequester,
focusedFlow: Boolean,
textFieldInteraction: MutableInteractionSource,
label: String?,
placeholder: String,
readOnly: Boolean = false,
message: String? = null,
errorMessage: String? = null,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Text,
imeAction = ImeAction.Default
),
rowInteraction: MutableInteractionSource,
listOfChips: List<String> = emptyList(),
onChipClick: (Int) -> Unit
) {
val isLight = MaterialTheme.colors.isLight
val focusManager = LocalFocusManager.current
val keyboardManager = LocalSoftwareKeyboardController.current
Row(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(
vertical = 10.dp,
horizontal = 20.dp
)
.clickable(
indication = null,
interactionSource = rowInteraction,
onClick = {
focusRequester.requestFocus()
keyboardManager?.show()
}
)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
) {
Column(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight(),
verticalArrangement = Arrangement.Center
) {
if (label != null) {
Text(
text = "$label:",
style = MaterialTheme.typography.body1.copy(
fontWeight = FontWeight.Bold,
color = if (focusedFlow) MaterialTheme.colors.secondary else if (isLight) Color.Gray else Color.White
)
)
}
TextFieldContent(
textFieldValue = textFieldValue,
placeholder = placeholder,
onValueChanged = onValueChanged,
focusRequester = focusRequester,
textFieldInteraction = textFieldInteraction,
readOnly = readOnly,
keyboardOptions = keyboardOptions,
focusManager = focusManager,
listOfChips = listOfChips,
modifier = modifier,
emphasizePlaceHolder = false,
onChipClick = onChipClick
)
}
ErrorSection(
message = message,
errorMessage = errorMessage
)
}
}
}
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun TextFieldContent(
textFieldValue: TextFieldValue,
placeholder: String,
onValueChanged: (TextFieldValue) -> Unit,
focusRequester: FocusRequester,
textFieldInteraction: MutableInteractionSource,
readOnly: Boolean,
keyboardOptions: KeyboardOptions,
focusManager: FocusManager,
listOfChips: List<String>,
emphasizePlaceHolder: Boolean = false,
modifier: Modifier,
onChipClick: (Int) -> Unit
) {
Box {
val isFocused = textFieldInteraction.collectIsFocusedAsState()
if (textFieldValue.text.isEmpty() && listOfChips.isEmpty()) {
Text(
text = placeholder,
color = if (emphasizePlaceHolder && !isFocused.value) {
MaterialTheme.colors.onSurface
} else {
if (MaterialTheme.colors.isLight) {
LocalCustomColors.current.muted
} else {
Color.Gray
}
},
modifier = Modifier.align(alignment = Alignment.CenterStart)
)
}
FlowRow(
modifier = Modifier
.wrapContentHeight()
.fillMaxWidth(),
mainAxisSpacing = 5.dp
) {
repeat(times = listOfChips.size) { index ->
Chip(
onClick = { onChipClick(index) },
modifier = Modifier.wrapContentWidth(),
trailingIcon = {
Box(
modifier = Modifier
.clip(CircleShape)
.background(MaterialTheme.colors.primary)
.padding(3.dp)
) {
Icon(
painter = rememberVectorPainter(image = Icons.Default.Close),
contentDescription = null,
modifier = Modifier.size(12.dp),
tint = if (MaterialTheme.colors.isLight) {
Color.White
} else {
Color.Black
}
)
}
},
colors = ChipDefaults
.chipColors(
backgroundColor = MaterialTheme.colors.secondary,
contentColor = MaterialTheme.colors.onSecondary
)
) {
Text(text = listOfChips[index])
}
}
BasicTextField(
value = textFieldValue,
onValueChange = onValueChanged,
modifier = modifier
.focusRequester(focusRequester)
.width(IntrinsicSize.Min),
singleLine = false,
textStyle = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onSurface),
decorationBox = { innerTextField ->
Row(
modifier = Modifier
.wrapContentWidth()
.defaultMinSize(minHeight = 48.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start
) {
Box(
modifier = Modifier.wrapContentWidth(),
contentAlignment = Alignment.CenterStart
) {
Row(
modifier = Modifier
.defaultMinSize(minWidth = 4.dp)
.wrapContentWidth(),
) {
innerTextField()
}
}
}
},
interactionSource = textFieldInteraction,
cursorBrush = SolidColor(MaterialTheme.colors.secondary),
readOnly = readOnly,
keyboardOptions = keyboardOptions,
keyboardActions = KeyboardActions(
onDone = {
focusManager.clearFocus()
}
)
)
}
}
}
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun ErrorSection(
message: String?,
errorMessage: String?
) {
Row(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
) {
Column(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight(),
horizontalAlignment = Alignment.End
) {
if (message != null) {
val color = if (MaterialTheme.colors.isLight) {
Color.Gray
} else {
Color.White
}
Text(
text = message,
fontStyle = FontStyle.Italic,
style = MaterialTheme.typography.body1.copy(color = color)
)
}
if (errorMessage != null) {
Chip(
onClick = {
},
colors = ChipDefaults
.chipColors(
backgroundColor = Color.Red,
contentColor = Color.White
),
leadingIcon = {
Icon(
painter = rememberVectorPainter(image = Icons.Default.Info),
contentDescription = null
)
}
) {
Text(
text = errorMessage,
style = MaterialTheme.typography.body1.copy(fontSize = 12.sp),
modifier = Modifier.padding(2.dp)
)
}
}
}
}
}