Android Material Design component: Chip - Part 2: AutoCompleteTextView with chips (like Gmail)

Android Material Design component: Chip - Part 2: AutoCompleteTextView with chips (like Gmail)

    In Part 1, you've learned how to make a chip layout by customizing in XML resources. The fact that the most popular apply of this component is putting into an AutoCompleteTextview to perform hint/selected information. For example, we can see this design in Gmail application:
    In this post, I will present the way to make this design by using a third-pary library!

Adding library dependencies

    As noted above, because this design is very popular so there are a lot of libraries are available on Github which able to help us to do this work easily. I will use TokenAutoComplete, in my opinion, this is good library and has a clearly document. So, in order to use it in your project, please add it's dependency to your application level build.gradle first:
compile "com.splitwise:tokenautocomplete:2.0.8@aar"

Custom an AutoCompleteTextView

    First of all, suppose we have a POJO class (contact data) simple like this:
SimpleContact.java
package info.devexchanges.chipedittext;

public class SimpleContact {
    private int drawableId;
    private String name;
    private String email;

    public SimpleContact(int drawableId, String name, String email) {
        this.drawableId = drawableId;
        this.name = name;
        this.email = email;
    }

    public int getDrawableId() {
        return drawableId;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getEmail() {
        return email;
    }

    @Override
    public String toString() {
        return getName() + "|" + getEmail();
    }
}
    We now must create a subclass of TokenCompleteTextView<T> to make a layout for "chips item" inside the EditText. For simplicity, just make a class look like ContactsCompletionView.java in the library sample module. In this project, T is SimpleContact:
ContactsCompletionView.java
package info.devexchanges.chipedittext;

import android.app.Activity;
import android.content.Context;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;

import com.tokenautocomplete.TokenCompleteTextView;

/**
 * Sample token completion view for basic contact info
 * <p>
 * Created on 9/12/13.
 *
 * @author mgod
 */
public class ContactsCompletionView extends TokenCompleteTextView<SimpleContact> {

    public ContactsCompletionView(Context context) {
        super(context);
    }

    public ContactsCompletionView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public ContactsCompletionView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    @Override
    protected View getViewForObject(SimpleContact contact) {
        LayoutInflater l = (LayoutInflater) getContext().getSystemService(Activity.LAYOUT_INFLATER_SERVICE);
        View tokenView = l.inflate(R.layout.item_autocomplete_contact, (ViewGroup) getParent(), false);
        TokenTextView textView = (TokenTextView) tokenView.findViewById(R.id.token_text);
        ImageView icon = (ImageView) tokenView.findViewById(R.id.icon);
        textView.setText(contact.getName());
        icon.setImageResource(contact.getDrawableId());

        return tokenView;
    }

    @Override
    protected SimpleContact defaultObject(String completionText) {
        //Stupid simple example of guessing if we have an email or not
        int index = completionText.indexOf('@');
        if (index == -1) {
            return new SimpleContact(R.drawable.male, completionText, completionText.replace(" ", "") + "@example.com");
        } else {
            return new SimpleContact(R.drawable.female, completionText.substring(0, index), completionText);
        }
    }
}
    And this is each chip layout (XML) file:
item_autocomplete_contact.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@drawable/chip_drawable"
    android:orientation="vertical">

    <ImageView
        android:id="@+id/icon"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerVertical="true"
        android:contentDescription="@null" />

    <info.devexchanges.chipedittext.TokenTextView
        android:id="@+id/token_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textColor="@android:color/white"
        android:layout_centerVertical="true"
        android:layout_toRightOf="@id/icon"
        android:paddingLeft="8dp"
        android:paddingRight="8dp"
        android:textStyle="bold" />

</RelativeLayout>
    As you can see, there is an class named TokenTextView, this is a subclass of TextView which onSelected() method was overridden to set it's state when user selected/clicked:
TokenTextView.java
package info.devexchanges.chipedittext;

import android.content.Context;
import android.util.AttributeSet;
import android.widget.TextView;

/**
 * Created by mgod on 5/27/15.
 *
 * Simple custom view example to show how to get selected events from the token
 * view. See ContactsCompletionView and contact_token.xml for usage
 */
public class TokenTextView extends TextView {

    public TokenTextView(Context context) {
        super(context);
    }

    public TokenTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public void setSelected(boolean selected) {
        super.setSelected(selected);
        setCompoundDrawablesWithIntrinsicBounds(0, 0, selected ? R.drawable.ic_clear_white_18dp : 0, 0);
    }
}
    I took this class from original file in the library sample module. This is the background drawable for the root view of item_autocomplete_contact.xml file:
res/drawable/chip_drawable.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">

    <solid android:color="@android:color/darker_gray" />
    <corners android:radius="30dp" />
</shape>

Putting the AutoCompleteTextView to activity layout

    Up to now, we created a auto completed view object named ContactsCompletionView, so put an instance to the main activity layout file like this:
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="info.devexchanges.chipedittext.MainActivity">

    <info.devexchanges.chipedittext.ContactsCompletionView
        android:id="@+id/autocomplete_textview"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:focusableInTouchMode="true"
        android:imeOptions="actionDone"
        android:inputType="text|textNoSuggestions|textMultiLine"
        android:nextFocusDown="@+id/editText"
        android:textColor="@android:color/darker_gray"
        android:textSize="19sp" />

    <Button
        android:id="@+id/btn_get"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="@dimen/activity_horizontal_margin"
        android:text="Get Input Data" />

    <TextView
        android:id="@+id/input_content"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="@dimen/activity_horizontal_margin" />
</LinearLayout>

Create adapter class for AutoCompleteTextView

    The next work is making a filter adapter for the ContactsCompletionView by making a subclass of FilteredArrayAdapter, I'll create a my own adapter class by overriding getView() and keepObject() methods:
FilterAdapter.java
package info.devexchanges.chipedittext;

import android.app.Activity;
import android.content.Context;
import android.support.annotation.NonNull;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;

import com.tokenautocomplete.FilteredArrayAdapter;

import java.util.List;

public class FilterAdapter extends FilteredArrayAdapter<SimpleContact> {

    public FilterAdapter(Context context, int resource, List<SimpleContact> objects) {
        super(context, resource,  objects);
    }

    @NonNull
    @Override
    public View getView(int position, View convertView, @NonNull ViewGroup parent) {
        if (convertView == null) {

            LayoutInflater layoutInflater = (LayoutInflater) getContext().getSystemService(Activity.LAYOUT_INFLATER_SERVICE);
            convertView = layoutInflater.inflate(R.layout.item_contact, parent, false);
        }

        SimpleContact contact = getItem(position);
        ((TextView) convertView.findViewById(R.id.name)).setText(contact != null ? contact.getName() : null);
        ((TextView) convertView.findViewById(R.id.email)).setText(contact != null ? contact.getEmail() : null);
        assert contact != null;
        ((ImageView) convertView.findViewById(R.id.icon)).setImageResource(contact.getDrawableId());

        return convertView;
    }

    @Override
    protected boolean keepObject(SimpleContact person, String mask) {
        mask = mask.toLowerCase();
        return person.getName().toLowerCase().startsWith(mask) || person.getEmail().toLowerCase().startsWith(mask);
    }
}
    As you can see at the adapter class code, each "hint row" of auto completion view layout was inflated from item_contact (XML) file. This is it's code:
item_contact.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:padding="5dp">

    <ImageView
        android:id="@+id/icon"
        android:src="@drawable/male"
        android:layout_centerVertical="true"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <TextView
        android:id="@+id/name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_toRightOf="@id/icon"
        android:text="Large Text"
        android:textAppearance="?android:attr/textAppearanceMedium" />

    <TextView
        android:id="@+id/email"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/name"
        android:layout_toRightOf="@id/icon"
        android:text="Small Text"
        android:textAppearance="?android:attr/textAppearanceSmall" />
</RelativeLayout>

Activity programmatically code configuration

    In order to make a responding to user selections in the auto completion view items (chip object), your Activity must implements TokenListener interface. By this, there are 2 methods you must override:
  • onTokenAdded(): called when a chip item added
  • onTokenRemoved: called when user remove a chip item from auto completion view
    This is simple source code the main activity:
MainActivity.java
package info.devexchanges.chipedittext;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.TextView;

import com.tokenautocomplete.TokenCompleteTextView;

import java.util.ArrayList;
import java.util.List;

public class MainActivity extends AppCompatActivity implements TokenCompleteTextView.TokenListener<SimpleContact> {

    private ArrayList<SimpleContact> contacts;
    private FilterAdapter filterAdapter;
    private ContactsCompletionView autoCompleteTextView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);
        setSampleContact();

        autoCompleteTextView = (ContactsCompletionView) findViewById(R.id.autocomplete_textview);

        //Initializing and attaching adapter for AutocompleteTextView
        filterAdapter = new FilterAdapter(this, R.layout.item_contact, contacts);
        autoCompleteTextView.setAdapter(filterAdapter);

        //Set the listener that will be notified of changes in the Tokenlist
        autoCompleteTextView.setTokenListener(this);

        //Set the action to be taken when a Token is clicked
        autoCompleteTextView.setTokenClickStyle(TokenCompleteTextView.TokenClickStyle.Select);

        final TextView inputContent = (TextView) findViewById(R.id.input_content);
        View btnGet = findViewById(R.id.btn_get);
        btnGet.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                List<SimpleContact> tokens = autoCompleteTextView.getObjects();
                StringBuilder content = new StringBuilder();
                for (int i = 0; i < tokens.size(); i++) {
                    content.append(tokens.get(i)).append("; ");
                }
                inputContent.setText(String.format("You choose: %s", content.toString()));
            }
        });
    }

    private void setSampleContact() {
        contacts = new ArrayList<>();
        contacts.add(new SimpleContact(R.drawable.female, "Thanh Ngan", "ngan@gmail.com"));
        contacts.add(new SimpleContact(R.drawable.male, "Quang Minh", "minh@gmail.com"));
        contacts.add(new SimpleContact(R.drawable.male, "Tran Tinh", "thanh_67@gmail.com"));
        contacts.add(new SimpleContact(R.drawable.female, "Phan Hoa", "hoa@gmail.com"));
        contacts.add(new SimpleContact(R.drawable.female, "Pham Trang", "trang@gmail.com"));
        contacts.add(new SimpleContact(R.drawable.male, "Dinh Tuan", "dtuan@gmail.com"));
        contacts.add(new SimpleContact(R.drawable.female, "Kim Chi", "kimchi@gmail.com"));
        contacts.add(new SimpleContact(R.drawable.male, "Quoc Cuong", "cuong@gmail.com"));
        contacts.add(new SimpleContact(R.drawable.female, "Hai Yen", "hai_yen@gmail.com"));
    }

    @Override
    public void onTokenAdded(SimpleContact token) {
        Log.d("Main", "A Token added");
    }

    @Override
    public void onTokenRemoved(SimpleContact token) {
        Log.d("Main", "A Token removed");
    }
}

Running the application - some screen shots

    A chip in the auto completion view is simple like this:
    When you are typing in the auto completion view, suggestion results will be displayed:
    When user select and delete a chip item:
    After click "Get Input Data" button:

Conclusions

    By using a third-party library, we now can creating an auto completion view with chip, a good design that you can see at Gmail Android app. For more details, you can read at this library document. By searching on the Internet, you can find out that there are a lot of other libraries which able to resolved this problem easily you can try. For examples:
Android Material Design component: Chip - Part 1: Creating chips layout

Android Material Design component: Chip - Part 1: Creating chips layout

    Chip is a Material Design component which presented by Google developers. It represents complex entities in small blocks, such as a contact. From guideline on Google design, a chip may:
  • contain entities such as a photo, text, rules, an icon, or a contact.
  • represent contact information in a compact way.
    For a popular example, we can see this component on Google Play:

    In Android SDK and Design Support library, there is no official widget to make a chip layout, so we must custom it ourselves. Moreover, there are tons of third-party libraries can help us to deal with this problem.
    Now, in this post, I will present a solution to make some chip styles by custom in xml files. For making a Chip EditText, please read Part 2.

Declaring some dimension resources

    Just take a glance at specs entry of the chip guideline, you will noticed some required dimensions when making a chip layout. So, before starting, I provide some dimension resources first:
dimens.xml
<resources>
    <!-- Default screen margins, per the Android Design guidelines. -->
    <dimen name="activity_horizontal_margin">16dp</dimen>
    <dimen name="activity_vertical_margin">16dp</dimen>
    <dimen name="chip_padding">12dp</dimen>
    <dimen name="icon_height">36dp</dimen>
    <dimen name="remove_icon_dimen">24dp</dimen>
    <dimen name="remove_icon_margin">4dp</dimen>
    <dimen name="deletable_chip_padding">12dp</dimen>
    <dimen name="contact_chip_left_padding">8dp</dimen>
    <dimen name="contact_chip_right_padding">12dp</dimen>

    <dimen name="padding_top">8dp</dimen>
</resources>

Creating a simple chip layout (only text label)

    There are nothing unfamiliar here at all, we just only make a background with round corner for TextView to make a chip. Firstly, defining a new drawable resource file with shape as the root attribute:
res\drawable\shape_chip_simple_drawable.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">

    <solid android:color="@color/colorAccent" />

    <padding
        android:bottom="@dimen/chip_padding"
        android:left="@dimen/chip_padding"
        android:right="@dimen/chip_padding"
        android:top="@dimen/chip_padding" />

    <corners android:radius="30dp" />
</shape>
    In your activity layout file, just define this drawable resource file as the TextView background like this:
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="info.devexchanges.chiplayout.SimpleChipActivity">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:background="@drawable/shape_chip_simple_drawable"
        android:text="Hello, I'm a simple Chip!"
        android:textColor="@android:color/white"
        android:textStyle="bold" />
</RelativeLayout>
    Running this activity, we'll have this simple chip layout:

Creating a deletable chip

    A deleteable chip contains a delete icon on the right side. When click at this icon, the chip will be deleted. In order to design this layout, wrapping a TextView and an ImageView in a RelativeLayout like this:
activity_cross_chips.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <RelativeLayout
        android:id="@+id/chip_layout"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:background="@drawable/shape_chip_drawable"
        android:paddingBottom="@dimen/padding_top"
        android:paddingTop="@dimen/padding_top">

        <TextView
            android:id="@+id/text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerVertical="true"
            android:paddingLeft="@dimen/chip_padding"
            android:text="Example chip"
            android:textStyle="bold" />

        <ImageView
            android:id="@+id/delete"
            android:layout_width="@dimen/remove_icon_dimen"
            android:layout_height="@dimen/remove_icon_dimen"
            android:layout_centerVertical="true"
            android:layout_toRightOf="@id/text"
            android:contentDescription="@null"
            android:padding="@dimen/remove_icon_margin"
            android:layout_marginRight="@dimen/remove_icon_margin"
            android:src="@drawable/delete" />
    </RelativeLayout>

</RelativeLayout>
    And this is drawable resource file to set as TextView background:
shape_chip_drawable.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">

    <solid android:color="@color/gray_light" />

    <corners android:radius="30dp" />
</shape>
    NOTE: You should use this design instead of defining a TextView with a drawableRight because you must handle delete icon click event in programmatically code:
DeleteChipActivity.java
package info.devexchanges.chiplayout;

import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.view.View;

public class DeleteChipActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_cross_chip);

        View deleteIcon = findViewById(R.id.delete);
        final View chipLayout = findViewById(R.id.chip_layout);
        deleteIcon.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                //gone chip layout
                //if it a list or an adapter, please remove it!
                chipLayout.setVisibility(View.GONE);
            }
        });
    }
}
    Running this activity, we'll have this output:
    When click the delete icon:

Chip with text and icon (image)

    It may be called contact chip, which have an icon on the left side of label. Moreover, it may have a remove icon on the right. The icon is always a round ImageView, wrapping them in a RelativeLayout like this:
activity_icon_chip.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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:id="@+id/activity_icon_chip"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="info.devexchanges.chiplayout.IconChipActivity">

    <RelativeLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:background="@drawable/shape_chip_icon_drawable">

        <de.hdodenhof.circleimageview.CircleImageView
            android:id="@+id/icon"
            android:layout_width="@dimen/icon_height"
            android:layout_height="@dimen/icon_height"
            android:src="@drawable/midu"
            app:civ_border_color="#FF000000" />

        <TextView
            android:id="@+id/text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerVertical="true"
            android:layout_toRightOf="@id/icon"
            android:paddingTop="@dimen/padding_top"
            android:paddingLeft="@dimen/padding_top"
            android:paddingBottom="@dimen/padding_top"
            android:text="Đặng Thị Mỹ Dung"
            android:textStyle="bold" />

        <ImageView
            android:id="@+id/delete"
            android:layout_width="@dimen/remove_icon_dimen"
            android:layout_height="@dimen/remove_icon_dimen"
            android:layout_centerVertical="true"
            android:layout_toRightOf="@id/text"
            android:padding="@dimen/remove_icon_margin"
            android:layout_marginLeft="@dimen/remove_icon_margin"
            android:layout_marginRight="@dimen/remove_icon_margin"
            android:contentDescription="@null"
            android:src="@drawable/delete" />
    </RelativeLayout>

</RelativeLayout>
    The round ImageView can be created by a third-party library. In this example, I use CircleImageView, so please add this dependency to dependencies scope in your application level build.gradle:
compile 'de.hdodenhof:circleimageview:2.1.0'
    The hardest work is fixing sizes for each widget, I use dimension resources like noted above. Running this activity, we'll have this output:

Conclusions

    Chip component can be created easily with some configuration in the resources and layout files. From now, you can use HorizontalScrollView to display a row of chips (like GooglePlay application) or use StaggeredGridLayoutManager to build a staggered grid of chips layout (named tag layout),...
    Up to Part 2, I will talk about putting chip into EditText (AutoCompleteTextView with chips inside) like Gmail app does, this is popular design that you can see on many applications (coming soon!).
    References:
  • Chip component guideline on Google Design
  • Round ImageView library page on Github
  • Read more: my post about using RecyclerView with StaggeredGridLayoutManager to build a staggered grid layout.