InputField 中的芯片类似于 Jetpack Compose 中的 Gmail

Chips in InputField like Gmail in Jetpack Compose

我想知道在 Jetpack Compose 中是否有可能获得类似 Gmail 的行为, 有人以前做过类似的事情并想分享他们的解决方案吗?

我只是想让用户有机会在上传之前为他们的内容添加标签,我不需要像下面的 gif 中那样的弹出建议。

只是 InputField 中的简单筹码。

这是我的做法。基本上我只是使用了流行和一个文本字段

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