ZOFTINO.COM android and web dev tutorials

Android AutoCompleteTextView Custom Layout and Adapter

AutoCompleteTextView allows you to implement suggestion drop down so that user can fill into text view without entering complete text. In order for AutoCompleteTextView to show drop down with suggestions, you need to provide data adapter which not only provides data but also takes care of item layout.

In my previous posts, AutoCompleteTextView tutorial and populating auto complete dropdown with data from web services, we used android provided ArrayAdapter and android.R.layout.simple_dropdown_item_1line item layout for AutoCompleteTextView auto suggestion drop down..

In this post, you can learn how to create custom AutoCompleteTextView adapter, custom filter and custom item layout with item divider with examples.

Android AutoCompleteTextView Custom Layout

Below layout is our custom layout which will be supplied to our custom adapter and used to display auto complete suggestions drop down of AutoCompleteTextView. The layout contains a TextView to show data item and a button used as item divider by setting custom drawable.

android autocompletetextview custom layout with divider and custom adapter example

AutoCompleteTextView custom layout

 <?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/textView"
        style="?android:attr/dropDownItemStyle"
        android:layout_width="match_parent"
        android:layout_height="?android:attr/listPreferredItemHeight"
        android:ellipsize="marquee"
        android:singleLine="true"
        android:textAppearance="?android:attr/textAppearanceLargePopupMenu"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/button"
        android:layout_width="match_parent"
        android:layout_height="6dp"
        android:enabled="false"
        android:background="@drawable/divider"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textView" />

</android.support.constraint.ConstraintLayout> 

Custom drawable

 <?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:tint="#42a5f5"
    android:shape="rectangle">
    <corners
        android:radius="4dp"/>
    <size
        android:height="6dp" />
    <solid android:color="#42a5f5" />
</shape> 

Android AutoCompleteTextView Custom Adapter

Android provided ArrayAdapter takes data in the form of array. We are going to create an adapter which can take List as input and display data in custom layout. The main functionality that needs to be implemented in an adapter is to provide item view for each data item and to filter data based on user entered text in AutoCompleteTextView. Below is the step by step explanation of custom adapter.

Create custom adapter class by extending ArrayAdapter or by extending BaseAdapter and implementing Filterable interface. Custom adapter should be Filterable as it needs to perform filter function for AutoCompleteTextView.

 public class CustomListAdapter extends ArrayAdapter { 

Create a constructor which takes context, item layout resource id, data as input.

 private List<String> dataList;
private Context mContext;
private int itemLayout;

public CustomListAdapter(Context context, int resource, List<String> storeDataLst) {
    super(context, resource, storeDataLst);
    dataList = storeDataLst;
    mContext = context;
    itemLayout = resource;
} 

Implement getCount and getItem methods as shown below.

@Override
public int getCount() {
    return dataList.size();
}

@Override
public String getItem(int position) {
    Log.d("CustomListAdapter",
            dataList.get(position));
    return dataList.get(position);
}

Implement getView method which will inflate item layout and populate the layout with the item data.

@Override
public View getView(int position, View view, @NonNull ViewGroup parent) {

    if (view == null) {
        view = LayoutInflater.from(parent.getContext())
                .inflate(itemLayout, parent, false);
    }

    TextView strName = (TextView) view.findViewById(R.id.textView);
    strName.setText(getItem(position));
    return view;
}

You need to override getFilter method and return an instance of our custom filter which is explained in the following section. AutoCompleteTextView will use this filter with user entered search string to get results from the supplied data.

@Override
public Filter getFilter() {
    return listFilter;
}

Android Filter and Filterable

Filterable classes which contain data need to provider Filter classes so that data can be filtered, by calling filter methods on Filter objects, by clients of filterable class.

Filter contains two abstract methods performFiltering and publishResults which need to be implemented by filterable class which in our case is our custom adapter for AutoCompleteTextView.

Method performFiltering is passed search string as input. It should perform the search and return results as FilterResults. Since Filter is defined as inner class of adapter, it can access data list and perform the search by comparing search string with each item in the data list.

Synchronized blocks are used to make sure that only one thread at a time executes those instructions to avoid data inconsistency.

 @Override
protected FilterResults performFiltering(CharSequence prefix) {
    FilterResults results = new FilterResults();
    if (dataListAllItems == null) {
        synchronized (lock) {
            dataListAllItems = new ArrayList<String>(dataList);
        }
    }

    if (prefix == null || prefix.length() == 0) {
        synchronized (lock) {
            results.values = dataListAllItems;
            results.count = dataListAllItems.size();
        }
    } else {
        final String searchStrLowerCase = prefix.toString().toLowerCase();

        ArrayList<String> matchValues = new ArrayList<String>();

        for (String dataItem : dataListAllItems) {
            if (dataItem.toLowerCase().startsWith(searchStrLowerCase)) {
                matchValues.add(dataItem);
            }
        }

        results.values = matchValues;
        results.count = matchValues.size();
    }

    return results;
} 

Method publishResults displays FilterResults in UI by calling notifyDataSetChanged or notifyDataSetInvalidated.

Since AutoCompleteTextView uses original data list variable for showing result in drop down, we need to assign search results to the same variable and call notifyDataSetChanged. As we need original data list, we need to create a copy of original data list.

@Override
protected void publishResults(CharSequence constraint, FilterResults results) {
    if (results.values != null) {
        dataList = (ArrayList<String>)results.values;
    } else {
        dataList = null;
    }
    if (results.count > 0) {
        notifyDataSetChanged();
    } else {
        notifyDataSetInvalidated();
    }
} 

CustomListAdapter

 import android.content.Context;
import android.support.annotation.NonNull;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.Filter;
import android.widget.TextView;

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

public class CustomListAdapter extends ArrayAdapter {

    private List<String> dataList;
    private Context mContext;
    private int itemLayout;

    private ListFilter listFilter = new ListFilter();
    private List<String> dataListAllItems;



    public CustomListAdapter(Context context, int resource, List<String> storeDataLst) {
        super(context, resource, storeDataLst);
        dataList = storeDataLst;
        mContext = context;
        itemLayout = resource;
    }

    @Override
    public int getCount() {
        return dataList.size();
    }

    @Override
    public String getItem(int position) {
        Log.d("CustomListAdapter",
                dataList.get(position));
        return dataList.get(position);
    }

    @Override
    public View getView(int position, View view, @NonNull ViewGroup parent) {

        if (view == null) {
            view = LayoutInflater.from(parent.getContext())
                    .inflate(itemLayout, parent, false);
        }

        TextView strName = (TextView) view.findViewById(R.id.textView);
        strName.setText(getItem(position));
        return view;
    }

    @NonNull
    @Override
    public Filter getFilter() {
        return listFilter;
    }

    public class ListFilter extends Filter {
        private Object lock = new Object();

        @Override
        protected FilterResults performFiltering(CharSequence prefix) {
            FilterResults results = new FilterResults();
            if (dataListAllItems == null) {
                synchronized (lock) {
                    dataListAllItems = new ArrayList<String>(dataList);
                }
            }

            if (prefix == null || prefix.length() == 0) {
                synchronized (lock) {
                    results.values = dataListAllItems;
                    results.count = dataListAllItems.size();
                }
            } else {
                final String searchStrLowerCase = prefix.toString().toLowerCase();

                ArrayList<String> matchValues = new ArrayList<String>();

                for (String dataItem : dataListAllItems) {
                    if (dataItem.toLowerCase().startsWith(searchStrLowerCase)) {
                        matchValues.add(dataItem);
                    }
                }

                results.values = matchValues;
                results.count = matchValues.size();
            }

            return results;
        }

        @Override
        protected void publishResults(CharSequence constraint, FilterResults results) {
            if (results.values != null) {
                dataList = (ArrayList<String>)results.values;
            } else {
                dataList = null;
            }
            if (results.count > 0) {
                notifyDataSetChanged();
            } else {
                notifyDataSetInvalidated();
            }
        }

    }
} 

Activity

 import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.AdapterView;
import android.widget.AutoCompleteTextView;
import android.widget.Toast;

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

public class CustomAdapterLayoutActivity extends AppCompatActivity {
    private AutoCompleteTextView nameTV;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.autocomplete);

        CustomListAdapter adapter = new CustomListAdapter(this,
                R.layout.autocompleteitem, getData());
        nameTV = (AutoCompleteTextView)
                findViewById(R.id.category);

        nameTV.setAdapter(adapter);
        nameTV.setOnItemClickListener(onItemClickListener);
    }
    private AdapterView.OnItemClickListener onItemClickListener =
            new AdapterView.OnItemClickListener(){
                @Override
                public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {

                    Toast.makeText(CustomAdapterLayoutActivity.this,
                            "Clicked item from auto completion list "
                                    + adapterView.getItemAtPosition(i)
                            , Toast.LENGTH_SHORT).show();
                }
            };


    private List<String> getData(){
        List<String> dataList = new ArrayList<String>();
        dataList.add("Fashion Men");
        dataList.add("Fashion Women");
        dataList.add("Baby");
        dataList.add("Kids");
        dataList.add("Electronics");
        dataList.add("Appliance");
        dataList.add("Travel");
        dataList.add("Bags");
        dataList.add("FootWear");
        dataList.add("Jewellery");
        dataList.add("Sports");
        dataList.add("Electrical");
        dataList.add("Sports Kids");
        return dataList;
    }
} 

Activity layout

 <?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android.support.design="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.design.widget.TextInputLayout
    android:id="@+id/cat_til"
    android:layout_width="200dp"
    android:layout_height="wrap_content"
    android:layout_marginBottom="8dp"
    android:layout_marginEnd="8dp"
    android:layout_marginStart="8dp"
    android:layout_marginTop="16dp"
    android.support.design:counterEnabled="true"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent">
    <AutoCompleteTextView
        android:id="@+id/category"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="Enter Category">
    </AutoCompleteTextView>
</android.support.design.widget.TextInputLayout>
</android.support.constraint.ConstraintLayout>