Reputation: 477
I've seen that in android-P google add new material components library which contains material chips:
Material components for android
So I decided to add material input chips to my project, but unfortunately didn't find any tutorial how to make that. I want to create something like Gmail chips but without image on the start.
Because I'm using appcompat library I tried to use material chips by android.support.design.chip.Chip
and android.support.design.chip.ChipGroup
.
But result was just chips without any input field. Also I tried to create a Standalone ChipDrawable and then add it to EditText
using
Editable text = editText.getText();
text.setSpan(span, 0, text.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
But I got empty EditText without any chips. So how can I create chips input like in Gmail using this material components library? Maybe someone has expreience or knows any tutorials where I could see how to create this?
Thanks in advance!
Upvotes: 27
Views: 31541
Reputation: 883
chipGroupView.attachTo(tagsInputEditText){ selection ->
// do something
}
fun ChipGroup.attachTo(editText: EditText, chipChangedCallback: (List<String>) -> Unit) {
editText.apply {
onFocusChangeListener = View.OnFocusChangeListener { _, hasFocus ->
if (hasFocus) {
if (text.toString() == " ") {
text.clear()
}
} else {
if (text.isNullOrEmpty() && childCount > 0) {
setText(" ")
}
}
}
fun getSelection(): List<String> = mutableListOf<String>().apply {
for(view in children){
add(view.findViewById<Chip>(R.id.chipItem).text.toString())
}
}
fun checkHashtag(hashtagInput: String) {
if(Pattern.matches(Constants.TWITTER_HASHTAG_REGEX, hashtagInput)){
addTag(hashtagInput){
chipChangedCallback(getSelection())
}
text.clear()
chipChangedCallback(getSelection())
}else{
VibrateUtils.vibrate(200)
}
}
setOnKeyListener { _, actionId, event ->
if (event.action == KeyEvent.ACTION_DOWN && event.keyCode == KeyEvent.KEYCODE_DEL) {
if (text.isNotEmpty() || childCount <= 0) return@setOnKeyListener false
val lastChip = getChildAt(childCount - 1) as LinearLayout
editText.append(lastChip.findViewById<Chip>(R.id.chipItem).text)
removeView(lastChip)
chipChangedCallback(getSelection())
}
else if(
actionId == EditorInfo.IME_ACTION_SEARCH
|| actionId == EditorInfo.IME_ACTION_DONE
|| event.action == KeyEvent.ACTION_DOWN
&& event.keyCode == KeyEvent.KEYCODE_ENTER){
checkHashtag(text.toString())
return@setOnKeyListener true
}
actionId == KeyEvent.KEYCODE_ENTER
}
addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun afterTextChanged(editable: Editable) {
val text = editable.toString()
val length = text.length
if(text == " "){
editable.delete(length - 1, length)
return
}
if (text.isNotEmpty() && text.endsWith(" ")) {
val hashtagInput = text.removeSuffix(" ")
checkHashtag(hashtagInput)
}
}
})
}
}
private fun ChipGroup.addTag(tag: String,onRemoved: (View)-> Unit = {}) {
addView(ChipBinding.inflate(LayoutInflater.from(context)).also { itemUi ->
itemUi.root.findViewById<Chip>(R.id.chipItem).apply {
text = tag
setOnClickListener {
[email protected](itemUi.root);
onRemoved(this)
}
}
}.root)
}
chip.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
xmlns:tools="http://schemas.android.com/tools">
<com.google.android.material.chip.Chip
android:id="@+id/chipItem"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_height="wrap_content"
app:chipIconSize="16dp"
app:iconStartPadding="6dp"
tools:text="Non-Fiction"
app:chipIcon="@drawable/ic_hash"
style="@style/Widget.MaterialComponents.Chip.Action"
app:closeIconEnabled="true"
android:layout_width="wrap_content" />
</LinearLayout>
Note: chipGroupView and tagsInputEditText are vertically wrapped in LinearLayout with a stroked background
Upvotes: 0
Reputation: 1353
chip_item.xml
<?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"
style="@style/Widget.MaterialComponents.Chip.Action"
app:closeIconEnabled="true"
android:layout_width="wrap_content" />
tags_input_layout.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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.material.textfield.TextInputLayout
android:id="@+id/i_input_v"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:hint="Tags">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:gravity="bottom"
android:paddingBottom="22dp"
android:layout_height="match_parent" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.chip.ChipGroup
android:id="@+id/i_flex_box"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:alignContent="space_around"
app:alignItems="stretch"
app:flexWrap="wrap"
android:layout_marginTop="12dp"
android:layout_marginBottom="50dp"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:showDivider="beginning|middle|end">
</com.google.android.material.chip.ChipGroup>
</androidx.constraintlayout.widget.ConstraintLayout>
TagInputView.kt
class TagInputView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : ConstraintLayout(context, attrs) {
val chipGroup: ChipGroup
init {
LayoutInflater.from(context).inflate(R.layout.tags_input_layout, this, true)
val inputLayout = findViewById<TextInputLayout>(R.id.i_input_v)
val editText = inputLayout.editText!!
chipGroup = findViewById(R.id.i_flex_box)
editText.onFocusChangeListener = OnFocusChangeListener { _, hasFocus ->
if (hasFocus) {
if (editText.text.toString() == " "){
editText.text.clear()
}
} else {
if (editText.text.isNullOrEmpty() && chipGroup.childCount > 0) {
editText.setText(" ")
}
}
}
editText.setOnKeyListener { _, _, event ->
if (event.action == KeyEvent.ACTION_DOWN) {
// onBackspacePressed, also edittext is empty
if (chipGroup.childCount <= 0) return@setOnKeyListener false
val lastChip = chipGroup.getChildAt(chipGroup.childCount - 1) as Chip
editText.append(lastChip.text)
chipGroup.removeView(lastChip)
}
false
}
editText.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun afterTextChanged(editable: Editable) {
val text = editable.toString()
if (text.isNotEmpty() && text.endsWith(",")) {
addNewChip(text.removeSuffix(","))
editable.clear()
}
}
})
}
private fun addNewChip(text: String) {
val newChip =
LayoutInflater.from(context).inflate(R.layout.chip_item, chipGroup, false) as Chip
newChip.id = ViewCompat.generateViewId()
newChip.text = text
newChip.setOnCloseIconClickListener {
chipGroup.removeView(newChip)
}
chipGroup.addView(newChip)
}
}
activity_main.xml
...
<com.shubhamgupta16.yourappid.utils.TagInputView
android:id="@+id/tagInputView"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
...
You can also access all Chip
inside ChipGroup
by using tagInputView.chipGroup
property.
Upvotes: 6
Reputation: 2545
Answer
No default input field for adding chips in android. They mentioned input chips but i didn't find any layout or viewgroup for input chips. So i do with Chipdrawable
method to add chips in edittext. Here am using AppCompatEdittext you can change to anyview which listening the text inputs.
Reference.
Step 1
Add chip xml resource. chip.xml
res -> xml -> chip.xml
<?xml version="1.0" encoding="utf-8"?>
<chip xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:textAppearance="@style/ChipTextAppearance"
app:chipBackgroundColor="@color/colorAccent"
app:chipIcon="@drawable/ic_call_white_24dp"
app:closeIconEnabled="true" <!--property for close icon if no need set to false. -->
app:closeIconTint="@android:color/white" />
Then add textappearance style in style.xml(For change textStyle)
<style name="ChipTextAppearance" parent="TextAppearance.MaterialComponents.Chip">
<item name="android:textColor">@android:color/white</item>
</style>
Step 2
Add your view here am using AppCompatEdittext
<android.support.v7.widget.AppCompatEditText
android:id="@+id/phone"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tvt_Contact" />
Step 3
Add this code to your view to get the desired behaviour.
private int SpannedLength = 0,chipLength = 4;
AppCompatEditText Phone = findViewById(R.id.phone);
Phone.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
}
@Override
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
if (charSequence.length() == SpannedLength - chipLength)
{
SpannedLength = charSequence.length();
}
}
@Override
public void afterTextChanged(Editable editable) {
if(editable.length() - SpannedLength == chipLength) {
ChipDrawable chip = ChipDrawable.createFromResource(getContext(), R.xml.chip);
chip.setChipText(editable.subSequence(SpannedLength,editable.length()));
chip.setBounds(0, 0, chip.getIntrinsicWidth(), chip.getIntrinsicHeight());
ImageSpan span = new ImageSpan(chip);
editable.setSpan(span, SpannedLength, editable.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
SpannedLength = editable.length();
}
}
});
Change chipLength according to your need when new chip need to be added in edittext.
OUTPUT
EDITED
You can find more about how to align center the text with span Here.
Here i added some code from the solution will fix for you..
public class VerticalImageSpan extends ImageSpan {
public VerticalImageSpan(Drawable drawable) {
super(drawable);
}
@Override
public int getSize(@NonNull Paint paint, CharSequence text, int start, int end,
Paint.FontMetricsInt fontMetricsInt) {
Drawable drawable = getDrawable();
Rect rect = drawable.getBounds();
if (fontMetricsInt != null) {
Paint.FontMetricsInt fmPaint = paint.getFontMetricsInt();
int fontHeight = fmPaint.descent - fmPaint.ascent;
int drHeight = rect.bottom - rect.top;
int centerY = fmPaint.ascent + fontHeight / 2;
fontMetricsInt.ascent = centerY - drHeight / 2;
fontMetricsInt.top = fontMetricsInt.ascent;
fontMetricsInt.bottom = centerY + drHeight / 2;
fontMetricsInt.descent = fontMetricsInt.bottom;
}
return rect.right;
}
@Override
public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end,
float x, int top, int y, int bottom, @NonNull Paint paint) {
Drawable drawable = getDrawable();
canvas.save();
Paint.FontMetricsInt fmPaint = paint.getFontMetricsInt();
int fontHeight = fmPaint.descent - fmPaint.ascent;
int centerY = y + fmPaint.descent - fontHeight / 2;
int transY = centerY - (drawable.getBounds().bottom - drawable.getBounds().top) / 2;
canvas.translate(x, transY);
drawable.draw(canvas);
canvas.restore();
}
}
And change your imagespan class like below
VerticalImageSpan span = new VerticalImageSpan(chip);
Upvotes: 33
Reputation: 1251
Not the ideal solution, but definitely easy
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.chip.Chip
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:chipMinTouchTargetSize="0dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:weightSum="10">
<EditText
android:id="@+id/add_frag_label_et_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginStart="8dp"
android:layout_weight="9"
android:background="@null"
android:hint="Enter Label"
android:inputType="text"
android:paddingStart="8dp"
android:paddingEnd="0dp"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
android:textColor="@color/black"
android:textSize="14sp" />
<ImageView
android:id="@+id/add_frag_label_iv_add"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_gravity="center"
app:tint="#6b6a6c"
android:layout_weight="1"
android:contentDescription="Add Label to note."
android:src="@mipmap/ic_plus_foreground" />
</LinearLayout>
</FrameLayout>
Upvotes: -1
Reputation: 1454
All of the previous solutions didn't work for me if you want to achieve a gmail like behaviour with chips on multiple lines. In order to do that I had to avoid using the ChipGroup and instead using a FlexboxLayout.
your_recipient_layout:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/recipient_label_TV"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:layout_gravity="center_vertical" />
<com.google.android.flexbox.FlexboxLayout
android:id="@+id/recipient_group_FL"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_gravity="center_vertical"
app:flexWrap="wrap"
app:alignItems="stretch"
app:alignContent="space_around"
app:showDivider="beginning|middle|end"
app:dividerDrawable="@drawable/divider">
<EditText
android:id="@+id/recipient_input_ET"
android:layout_width="wrap_content"
android:layout_height="32dp"
app:layout_flexGrow="1"
android:background="@android:color/transparent"
android:imeOptions="actionDone"
android:inputType="text"/>
</com.google.android.flexbox.FlexboxLayout>
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recipients_list_RV"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone" />
The trick now is adding a new chip to the group but as second last position. Something like this:
private fun addNewChip(person: String, chipGroup: FlexboxLayout) {
val chip = Chip(context)
chip.text = person
chip.chipIcon = ContextCompat.getDrawable(requireContext(), R.mipmap.ic_launcher_round)
chip.isCloseIconEnabled = true
chip.isClickable = true
chip.isCheckable = false
chipGroup.addView(chip as View, chipGroup.childCount - 1)
chip.setOnCloseIconClickListener { chipGroup.removeView(chip as View) }
}
Upvotes: 27
Reputation: 1693
You can use material chip "com.google.android.material.chip.Chip" and "implementation 'com.google.android.material:material:1.0.0' " add in build.gradle
Filter style="@style/Widget.MaterialComponents.Chip.Filter"
Choice Chips style="@style/Widget.MaterialComponents.Chip.Choice"
Entry input: style="@style/Widget.MaterialComponents.Chip.Entry"
Upvotes: 0
Reputation: 7557
We can do this by using material chips design itself without adding any extra styles.
Add it on app gradle For AndroidX
implementation 'com.google.android.material:material:1.0.0-beta01'
For earlier than AndroidX use this
implementation 'com.android.support:design:28.0.0'
Fragment
class EntryChipDemoFragment : Fragment() {
private lateinit var mView: View
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
mView = inflater.inflate(R.layout.fragment_entry_chip_demo, container, false)
mView.etValue.setOnEditorActionListener(TextView.OnEditorActionListener { v, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_DONE) {
val txtVal = v.text
if(!txtVal.isNullOrEmpty()) {
addChipToGroup(txtVal.toString(), mView.chipGroup2)
mView.etValue.setText("")
}
return@OnEditorActionListener true
}
false
})
return mView
}
private fun addChipToGroup(txt: String, chipGroup: ChipGroup) {
val chip = Chip(context)
chip.text = txt
// chip.chipIcon = ContextCompat.getDrawable(requireContext(), baseline_person_black_18)
chip.isCloseIconEnabled = true
chip.setChipIconTintResource(R.color.chipIconTint)
// necessary to get single selection working
chip.isClickable = false
chip.isCheckable = false
chipGroup.addView(chip as View)
chip.setOnCloseIconClickListener { chipGroup.removeView(chip as View) }
printChipsValue(chipGroup)
}
private fun printChipsValue(chipGroup: ChipGroup) {
for (i in 0 until chipGroup.childCount) {
val chipObj = chipGroup.getChildAt(i) as Chip
Log.d("Chips text :: " , chipObj.text.toString())
}
}
companion object {
@JvmStatic
fun newInstance() = EntryChipDemoFragment()
}
}
XML File:
<HorizontalScrollView
android:id="@+id/chipGroup2HorizontalView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="16dp"
android:scrollbars="none"
app:layout_constraintVertical_bias="0.62">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Skills: " />
<com.google.android.material.chip.ChipGroup
android:id="@+id/chipGroup2"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:duplicateParentState="false">
</com.google.android.material.chip.ChipGroup>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/textInputLayout"
android:layout_width="wrap_content"
android:layout_height="43dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="5dp"
android:minWidth="32dp"
android:visibility="visible"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/chipGroup2HorizontalView"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_min="32dp">
<androidx.appcompat.widget.AppCompatEditText
android:id="@+id/etValue"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/transparent"
android:imeOptions="actionDone"
android:maxLines="1"
android:singleLine="true" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
</HorizontalScrollView>
For more reference Click Here
Upvotes: 4