Android Tag Input (Show Material Chip as Tag on Left of EditText)

July 7, 2020

Layout

Why use FlexboxLayout? It allows tags/chip to flow to next row, while AutoCompleteTextView / EditText will fill up the remaining space of the row.

Material Chip with EditText

Material Chip with EditText

Why not use ChipGroup? Chip cannot flow on the same row as AutoCompleteTextView / EditText within ChipGroup.

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    >
    <com.google.android.flexbox.FlexboxLayout
        android:id="@+id/tagsChipGroup"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:flexWrap="wrap"
        app:alignItems="stretch"
        app:alignContent="stretch" >

        <AutoCompleteTextView
            android:hint="Tags"
            android:id="@+id/tagsAutoCompleteTextView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:imeOptions="actionDone"
            android:maxLines="1"
            android:singleLine="true"
            android:inputType="text|textCapWords"
            app:layout_flexGrow="1" />

    </com.google.android.flexbox.FlexboxLayout>
</FrameLayout>

Code

val editText = tagsAutoCompleteTextView

// load autocomplete
val allTags = listOf("Love", "Passion", "Peace", "Hello", "Test")
val adapter = ArrayAdapter<String>(context,
    android.R.layout.simple_dropdown_item_1line,
    allTags)
editText.setAdapter(adapter)


// load tags
val tags = listOf("Hello", "This Is A Big World", "Test Multiple Row")

for (name in tags) {
    tagsChipGroup.addChip(name) {
        // TODO: handle when tag removed
    }
}

editText.setOnAddTagListener { name ->
    // TODO: handle when tag is created
    val tags = tagsChipGroup.getAllChips().map { it.text.toString() }

    editText.text = null
}

Kotlin Extensions

fun AutoCompleteTextView.setOnAddTagListener(callback: (name: String) -> Unit) {
    // select from adapter list
    setOnItemClickListener { adapterView, _, position, _ ->
        val name = adapterView.getItemAtPosition(position) as String
        callback(name)
    }

    // done pressed
    setOnEditorActionListener { textView, actionId, _ ->
        if (actionId == EditorInfo.IME_ACTION_DONE) {
            val name = textView.text.toString()

            callback(name)

            true
        } else false
    }

    doAfterTextChanged { text ->
        if (text != null && text.isEmpty()) {
            return@doAfterTextChanged
        }

        // comma is detected (optional space)
        if (text?.last() == ',') { //  || text?.last() == ' '
            val name = text.substring(0, text.length - 1)

            callback(name)
        }
    }
}


fun FlexboxLayout.getAllChips(): List<Chip> {
    return (0 until childCount).mapNotNull { index ->
        getChildAt(index) as? Chip
    }
}

fun FlexboxLayout.clearChips() {
    val chipViews = (0 until childCount).mapNotNull { index ->
        val view = getChildAt(index)
        if (view is Chip) view else null
    }
    chipViews.forEach { removeView(it) }
}

private fun ViewGroup.addChip(text: String, removeCallback: (message: String) -> Unit) {
    // val chip = Chip(context)
    val inflater = LayoutInflater.from(context)
    val chip = inflater.inflate(R.layout.view_chip,  null) as Chip
    chip.text = text
    /*
    test_chip.isCloseIconVisible = true
    // necessary to get single selection working
    test_chip.isClickable = true
    test_chip.isCheckable = false
     */

    // val layoutParams = test_chip.layoutParams as ViewGroup.MarginLayoutParams
    val layoutParams = ViewGroup.MarginLayoutParams(ViewGroup.MarginLayoutParams.WRAP_CONTENT, ViewGroup.MarginLayoutParams.WRAP_CONTENT)
    layoutParams.rightMargin = 4.dpToPx(context.resources.displayMetrics)
    addView(chip as View, childCount - 1, layoutParams)
    chip.setOnCloseIconClickListener {
        removeView(chip as View)
        removeCallback(chip.text.toString())
    }
}

R.drawable.view_chip

<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.chip.Chip xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_height="wrap_content"
    android:layout_width="wrap_content"
    app:closeIconEnabled="true"
     />
This work is licensed under a
Creative Commons Attribution-NonCommercial 4.0 International License.