androidkotlinandroid-jetpack-composeandroid-jetpack-compose-material3android-chips

Jetpack compose: TextField with Chips


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;

Solution

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