Jetpack compose: TextField with Chips

877 views Asked by At

Text Field with Chips in jetpack compose

I am trying to achieve a behaviour similar with the photo. A library that can do the same thing will be useful as well. I already tried https://github.com/dokar3/ChipTextField but run into some issues

java.lang.NoClassDefFoundError: Failed resolution of: Landroidx/compose/ui/platform/LocalSoftwareKeyboardController;
1

There are 1 answers

0
Thracian On BEST ANSWER

You can implement it using FlowRow, Chip and BasicTextField.

enter image description here

1- Create a data class that hold Uri and String

@Immutable
data class ChipData(
    val uri: Uri,
    val text: String,
    val id: String = UUID.randomUUID().toString()
)

2- Create custom chip that displays image, string. I used Coil library for painter to get Painter from Uri.

@Composable
private fun MyChip(
    backgroundColor: Color,
    data: ChipData,
    onDeleteClick: () -> Unit
) {
    Chip(
        modifier = Modifier,
        shape = RoundedCornerShape(50),
        enabled = false,
        onClick = {},
        border = BorderStroke(1.dp, Green400.copy(alpha = .9f)),
        colors = ChipDefaults.chipColors(
            disabledBackgroundColor = backgroundColor,
            disabledContentColor = Color.White
        ),
        leadingIcon = {
            Image(
                painter = rememberAsyncImagePainter(data.uri),
                modifier = Modifier
                    .padding(vertical = 4.dp)
                    .size(34.dp)
                    .clip(CircleShape),
                contentScale = ContentScale.FillBounds,
                contentDescription = null
            )
        }
    ) {
        Text(
            text = data.text,
            modifier = Modifier.weight(1f, fill = false),
            overflow = TextOverflow.Ellipsis,
            maxLines = 1
        )
        Spacer(modifier = Modifier.width(ButtonDefaults.IconSpacing))
        Icon(
            modifier = Modifier
                .clip(CircleShape)
                .clickable {
                    onDeleteClick()
                }
                .background(Color.Black.copy(alpha = .4f))
                .size(16.dp)
                .padding(2.dp),
            imageVector = Icons.Filled.Close,
            tint = Color(0xFFE0E0E0),
            contentDescription = null
        )
    }
}

3- Use FlowRow to align chips and put a BasicTextField to last item.

Also i used rememberLauncherForActivityResult for selecting image that you can add to gradle with

implementation("com.google.modernstorage:modernstorage-photopicker:1.0.0-alpha06")

You can use another or default image picker with SAF if you want to

@OptIn(ExperimentalLayoutApi::class)
@Composable
fun ChipAndTextFieldLayout(
    modifier: Modifier = Modifier,
    backgroundColor: Color,
    list: List<ChipData> = emptyList(),
    onChipCreated: (ChipData) -> Unit,
    chip: @Composable (data: ChipData, index: Int) -> Unit
) {

    var text by remember {
        mutableStateOf("")
    }

    val focusRequester = remember {
        FocusRequester()
    }

    val keyboardController: SoftwareKeyboardController? = LocalSoftwareKeyboardController.current

    val photoPicker =
        rememberLauncherForActivityResult(PhotoPicker()) { uris ->
            uris.firstOrNull()?.let { uri: Uri ->
                onChipCreated(
                    ChipData(
                        uri = uri,
                        text = text
                    )
                )
                text = ""
                // Open keyboard after new chip is added
                keyboardController?.show()
            }
        }

    LaunchedEffect(Unit) {
        delay(100)
        focusRequester.requestFocus()
    }

    FlowRow(
        modifier = modifier
            .drawWithContent {
                drawContent()
                drawLine(
                    Green400.copy(alpha = .6f),
                    start = Offset(0f, size.height),
                    end = Offset(size.width, size.height),
                    strokeWidth = 4.dp.toPx()
                )
            },
        horizontalArrangement = Arrangement.spacedBy(6.dp)
    ) {

        list.forEachIndexed { index, item ->
            key(item.id) {
                chip(item, index)
            }
        }

        Box(
            modifier = Modifier.height(54.dp)
                // This minimum width that TextField can have
                // if remaining space in same row is smaller it's moved to next line
                .widthIn(min = 80.dp)
                // TextField can grow as big as Composable width
                .weight(1f),
            contentAlignment = Alignment.CenterStart
        ) {
            BasicTextField(
                modifier = Modifier.focusRequester(focusRequester),
                value = text,
                textStyle = TextStyle(
                    fontSize = 20.sp
                ),
                cursorBrush = SolidColor(backgroundColor),
                singleLine = true,
                onValueChange = { text = it },
                keyboardOptions = KeyboardOptions(
                    imeAction = ImeAction.Done
                ),
                keyboardActions = KeyboardActions(
                    onDone = {
                        if (text.isNotEmpty()) {
                            keyboardController?.hide()
                            photoPicker.launch(
                                PhotoPicker.Args(
                                    PhotoPicker.Type.IMAGES_ONLY, 1
                                )
                            )
                        }
                    }
                )
            )
        }
    }
}

Usage

@Preview
@Composable
private fun ChipSampleAndTextLayoutSample() {

    val backgroundColor = Green400.copy(alpha = .6f)

    val chipDataSnapshotStateList = remember {
        mutableStateListOf<ChipData>()
    }

    ChipAndTextFieldLayout(
        modifier = Modifier.fillMaxWidth().padding(8.dp),
        list = chipDataSnapshotStateList,
        backgroundColor = backgroundColor,
        onChipCreated = {
            chipDataSnapshotStateList.add(it)
        },

        chip = { data: ChipData, index: Int->
            MyChip(backgroundColor, data){
                chipDataSnapshotStateList.removeAt(index)
            }
        }
    )
}