Feb 12, 2013

Android Chips EditText , Token EditText and Bubble EditText - [Part 1]

Chips EditText, Token EditText, Bubble EditText, Spannable EditText and etc.. There are many names of this control. Whatever we call it but this control is not available in Android. I found many questions and solutions for this control. 

Few popular questions & solutions 
1] Roman Nurik : https://plus.google.com/113735310430199015092/posts/WUd7GrfZfiZ 
2] Android EditText Gmail like Field : http://stackoverflow.com/questions/13747809/android-edittext-gmail-like-to-field 
3] Android Labels or Bubbles in Edit Text : http://stackoverflow.com/questions/8090711/android-labels-or-bubbles-in-edittext/8128848#8128848 
4] Contact Bubble Edit Text : http://stackoverflow.com/questions/10812316/contact-bubble-edittext 

While I was reading above post, I thought that, I should develop Chips Edit Text control, which should be easy to understand and integrated in project. Here, My control is named as "Chips Edit Text". Before going to technical details, I would like to show few screenshots.

 

 


Step 1: Prerequisite

Android provides bunch of Span classes, Span classes are used to format text. I suggest to read following articles for helpful detail and in-depth understanding.

1] Introduction to Span
http://blog.stylingandroid.com/archives/177

2] Easy Method for Formatting Android
http://www.androidengineer.com/2010/08/easy-method-for-formatting-android.html

3] API Classes : Spannable, SpannableString, SpannableStringBuilder, ImageSpan ,ClickableSpan 

Step 2 : Logic

Our goal is to display background (border & fill with color) and image at right side of every chip. Android spannable classes allow us to set background color only. It does not support any drawable at background. Another problem is right side image. Android has ImageSpan class, we can display image at right side with ImageSpan but problem is that ImageSpan will consider as separate entity and it will display outside of background.

I have implemented following logic to overcome above two problems

1. Extends MultiAutoCompleteTextView to create chips edit text.
2. Generate TextView dynamically in code and set style & drawable at right side.
3. Capture image/bitmap of newly generated TextView.
4. Set ImageSpan instead-of country name. 

  
Step 3 : Create Android Project

Create Android project named "Chips Edit Text" 

Step 4 : Create class named "ChipsMultiAutoCompleteTextview.java"


package com.kpbird.chipsedittext;

import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.drawable.BitmapDrawable;
import android.text.Editable;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.TextWatcher;
import android.text.style.ImageSpan;
import android.util.AttributeSet;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.MultiAutoCompleteTextView;
import android.widget.TextView;

public class ChipsMultiAutoCompleteTextview extends MultiAutoCompleteTextView implements OnItemClickListener {

 private final String TAG = "ChipsMultiAutoCompleteTextview";
 
 // Constructor 
 public ChipsMultiAutoCompleteTextview(Context context) {
  super(context);
  init(context);
 }
 // Constructor 
 public ChipsMultiAutoCompleteTextview(Context context, AttributeSet attrs) {
  super(context, attrs);
  init(context);
 }
 // Constructor 
 public ChipsMultiAutoCompleteTextview(Context context, AttributeSet attrs,
   int defStyle) {
  super(context, attrs, defStyle);
  init(context);
 }
 // set listeners for item click and text change 
 public void init(Context context){
  setOnItemClickListener(this);
  addTextChangedListener(textWather);
 }
 // TextWatcher, If user type any country name and press comma then following code will regenerate chips 
 private TextWatcher textWather = new TextWatcher() {
  
  @Override
  public void onTextChanged(CharSequence s, int start, int before, int count) {
   if(count >=1){
    if(s.charAt(start) == ',')
     setChips(); // generate chips
   }
  }
  @Override
  public void beforeTextChanged(CharSequence s, int start, int count,int after) {}
  @Override
  public void afterTextChanged(Editable s) {}
 };
 //This function has whole logic for chips generate
 public void setChips(){
  if(getText().toString().contains(",")) // check comman in string
  {
   
   SpannableStringBuilder ssb = new SpannableStringBuilder(getText());
   // split string wich comma
   String chips[] = getText().toString().trim().split(",");
   int x =0;
   // loop will generate ImageSpan for every country name separated by comma
   for(String c : chips){
    // inflate chips_edittext layout 
    LayoutInflater lf = (LayoutInflater) getContext().getSystemService(Activity.LAYOUT_INFLATER_SERVICE);
    TextView textView = (TextView) lf.inflate(R.layout.chips_edittext, null);
    textView.setText(c); // set text
    setFlags(textView, c); // set flag image
    // capture bitmapt of genreated textview
    int spec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
    textView.measure(spec, spec);
    textView.layout(0, 0, textView.getMeasuredWidth(), textView.getMeasuredHeight());
    Bitmap b = Bitmap.createBitmap(textView.getWidth(), textView.getHeight(),Bitmap.Config.ARGB_8888);
    Canvas canvas = new Canvas(b);
    canvas.translate(-textView.getScrollX(), -textView.getScrollY());
    textView.draw(canvas);
    textView.setDrawingCacheEnabled(true);
    Bitmap cacheBmp = textView.getDrawingCache();
    Bitmap viewBmp = cacheBmp.copy(Bitmap.Config.ARGB_8888, true);
    textView.destroyDrawingCache();  // destory drawable
    // create bitmap drawable for imagespan
    BitmapDrawable bmpDrawable = new BitmapDrawable(viewBmp);
    bmpDrawable.setBounds(0, 0,bmpDrawable.getIntrinsicWidth(),bmpDrawable.getIntrinsicHeight());
    // create and set imagespan 
    ssb.setSpan(new ImageSpan(bmpDrawable),x ,x + c.length() , Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
    x = x+ c.length() +1;
   }
   // set chips span 
   setText(ssb);
   // move cursor to last 
   setSelection(getText().length());
  }
  
  
 }
 @Override
 public void onItemClick(AdapterView parent, View view, int position, long id) {
  setChips(); // call generate chips when user select any item from auto complete
 }
 
 // this method set country flag image in textview's drawable component, this logic is not optimize, you need to change as per your requirement
 public void setFlags(TextView textView,String country){
  country = country.trim();
  if(country.equals("India")){
   textView.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.india, 0);
  }
  else if(country.equals("United States")){
   textView.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.unitedstates, 0);
  }
  else if(country.equals("Canada")){
   textView.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.canada, 0);
  }
  else if(country.equals("Australia")){
   textView.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.australia, 0);
  }
  else if(country.equals("United Kingdom")){
   textView.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.unitedkingdom, 0);
  }
  else if(country.equals("Philippines")){
   textView.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.philippines, 0);
  }
  else if(country.equals("Japan")){
   textView.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.japan, 0);
  }
  else if(country.equals("Italy")){
   textView.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.japan, 0);
  }
  else if(country.equals("Germany")){
   textView.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.germany, 0);
  }
  else if(country.equals("Russia")){
   textView.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.russia, 0);
  }
  else if(country.equals("Malaysia")){
   textView.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.malaysia, 0);
  }
  else if(country.equals("France")){
   textView.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.france, 0);
  }
  else if(country.equals("Sweden")){
   textView.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.sweden, 0);
  }
  else if(country.equals("New Zealand")){
   textView.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.newzealand, 0);
  }
  else if(country.equals("Singapore")){
   textView.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.singapore, 0);
  }
  
  
 }
 
}

Step 5 : Create drawable shape for chip named "chips_edittext_bg.xml" in drawable folder.


<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" >

    <stroke
        android:width="1dp"
        android:color="#A6B0B8" />

    <solid android:color="#E5E5E6" />

    <corners
        android:bottomLeftRadius="3dp"
        android:bottomRightRadius="3dp"
        android:topLeftRadius="3dp"
        android:topRightRadius="3dp" >
    </corners>

</shape>

Step 6 : Create TextView layout for chip named "chips_edittext.xml" in layout folder


 <?xml version="1.0" encoding="UTF-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/edtTxt1"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="@drawable/chips_edittext_gb"
    android:drawablePadding="2dp"
    android:drawableRight="@drawable/android"
    android:padding="8dp"
    android:shadowColor="#FFFFFF"
    android:shadowDy="1"
    android:shadowRadius="0.01"
    android:textColor="#000000"
    android:textSize="18sp"
    android:textStyle="bold" />

Step 7 : Create string array for multi select data named "country" in strings.xml 


    <string-array name="country">
        <item >India</item>
        <item>United States</item>
        <item>Canada</item>
        <item>Australia</item>
        <item>United Kingdom</item>
        <item>Philippines</item>
        <item >Japan</item>
        <item>Italy</item>
        <item>Germany</item>
        <item>Russia</item>
        <item>Malaysia</item>
        <item>France</item>
        <item>Sweden</item>
        <item>New Zealand</item>
        <item>Singapore</item>
    </string-array>

Step 8 : Edit layout of main_activity.xml file


<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity" >
    <com.kpbird.chipsedittext.ChipsMultiAutoCompleteTextview
         android:id="@+id/multiAutoCompleteTextView1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_alignParentRight="true"
        android:layout_alignParentTop="true"
        android:ems="10"
         />
    <Button
        android:id="@+id/button1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_alignParentRight="true"
        android:layout_below="@+id/multiAutoCompleteTextView1"
        android:onClick="buttonClicked"
        android:text="@string/getvalue" />
    <TextView
        android:id="@+id/textView1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_alignParentRight="true"
        android:layout_below="@+id/button1"
        android:text="@string/selectedcountries"
        android:textAppearance="?android:attr/textAppearanceLarge" />

</RelativeLayout>

Step 9 : Edit code of mainactivity.java file


package com.kpbird.chipsedittext;

import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.MultiAutoCompleteTextView;
import android.widget.TextView;

public class MainActivity extends Activity {

 ChipsMultiAutoCompleteTextview mu;

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

  mu = (ChipsMultiAutoCompleteTextview) findViewById(R.id.multiAutoCompleteTextView1);

  String[] item = getResources().getStringArray(R.array.country);

  Log.i("", "Country Count : " + item.length);
  mu.setAdapter(new ArrayAdapter(this,
    android.R.layout.simple_dropdown_item_1line, item));
  mu.setTokenizer(new MultiAutoCompleteTextView.CommaTokenizer());
 }

 public void buttonClicked(View v) {
  TextView tv = (TextView) findViewById(R.id.textView1);
  tv.setText(mu.getText().toString());
 }

}





Source code : Github Link