Custom Listview Adapter With Filter Android

Custom Listview Adapter with filter Android

You can use the Filterable interface on your Adapter, have a look at the example below:

public class SearchableAdapter extends BaseAdapter implements Filterable {

private List<String>originalData = null;
private List<String>filteredData = null;
private LayoutInflater mInflater;
private ItemFilter mFilter = new ItemFilter();

public SearchableAdapter(Context context, List<String> data) {
this.filteredData = data ;
this.originalData = data ;
mInflater = LayoutInflater.from(context);
}

public int getCount() {
return filteredData.size();
}

public Object getItem(int position) {
return filteredData.get(position);
}

public long getItemId(int position) {
return position;
}

public View getView(int position, View convertView, ViewGroup parent) {
// A ViewHolder keeps references to children views to avoid unnecessary calls
// to findViewById() on each row.
ViewHolder holder;

// When convertView is not null, we can reuse it directly, there is no need
// to reinflate it. We only inflate a new View when the convertView supplied
// by ListView is null.
if (convertView == null) {
convertView = mInflater.inflate(R.layout.list_item, null);

// Creates a ViewHolder and store references to the two children views
// we want to bind data to.
holder = new ViewHolder();
holder.text = (TextView) convertView.findViewById(R.id.list_view);

// Bind the data efficiently with the holder.

convertView.setTag(holder);
} else {
// Get the ViewHolder back to get fast access to the TextView
// and the ImageView.
holder = (ViewHolder) convertView.getTag();
}

// If weren't re-ordering this you could rely on what you set last time
holder.text.setText(filteredData.get(position));

return convertView;
}

static class ViewHolder {
TextView text;
}

public Filter getFilter() {
return mFilter;
}

private class ItemFilter extends Filter {
@Override
protected FilterResults performFiltering(CharSequence constraint) {

String filterString = constraint.toString().toLowerCase();

FilterResults results = new FilterResults();

final List<String> list = originalData;

int count = list.size();
final ArrayList<String> nlist = new ArrayList<String>(count);

String filterableString ;

for (int i = 0; i < count; i++) {
filterableString = list.get(i);
if (filterableString.toLowerCase().contains(filterString)) {
nlist.add(filterableString);
}
}

results.values = nlist;
results.count = nlist.size();

return results;
}

@SuppressWarnings("unchecked")
@Override
protected void publishResults(CharSequence constraint, FilterResults results) {
filteredData = (ArrayList<String>) results.values;
notifyDataSetChanged();
}

}
}

In your Activity or Fragment where of Adapter is instantiated :

editTxt.addTextChangedListener(new TextWatcher() {

@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
System.out.println("Text ["+s+"]");

mSearchableAdapter.getFilter().filter(s.toString());
}

@Override
public void beforeTextChanged(CharSequence s, int start, int count,
int after) {

}

@Override
public void afterTextChanged(Editable s) {
}
});

Here are the links for the original source and another example

Android filter listview custom adapter

As far as I can see you are only checking if the two Strings match exactly.
You might want to use boolean startsWith(String prefix)

Which would make something like

if (currentString.trim().toLowerCase().startsWith(searchString.trim().toLowerCase()))
{
arrayTemplist.add(arraylist.get(i));
}

Filter ListView with custom adapter

youll need to create a class that extends filter.try this i had used it for a project of mine..tweeked it to match your code. add the appfilter class under the View GetView() in your adapter file.

  private class AppFilter extends Filter {

@Override
protected FilterResults performFiltering(CharSequence constraint) {
// TODO Auto-generated method stub
constraint = constraint.toString().toLowerCase();
FilterResults result = new FilterResults();
if (constraint != null && constraint.toString().length() > 0) {
List<ApplicationInfo> filteredItems = new ArrayList<ApplicationInfo>();
for (int i = 0, l = appsList.size(); i < l; i++) {
ApplicationInfo data = appsList.get(i);
String name = data.loadLabel(packageManager).toString();
if (name.toLowerCase().contains(constraint)) {
filteredItems.add(data);
}
}
result.count = filteredItems.size();
result.values = filteredItems;
} else {
synchronized (this) {
result.values = appsList;
result.count = appsList.size();
}
}
return result;
}

@SuppressWarnings("unchecked")
@Override
protected void publishResults(CharSequence constraint,
FilterResults results) {
// TODO Auto-generated method stub
appsList = (ArrayList<ApplicationInfo>) results.values;
notifyDataSetChanged();
clear();
for (int i = 0; i < appsList.size(); i++)
add(appsList.get(i));
notifyDataSetInvalidated();
}

}

you will also need to initialize and override the constructor of the filter class

private AppFilter filter;
@Override
public Filter getFilter() {
if (filter == null) {
filter = new AppFilter();
}
return filter;
}

and in your main list file

        searchtext.addTextChangedListener(new TextWatcher() {

@Override
public void onTextChanged(CharSequence s, int start, int before,
int count) {
// TODO Auto-generated method stub
adapter.getFilter().filter(s.toString());
}

@Override
public void beforeTextChanged(CharSequence s, int start, int count,
int after) {
// TODO Auto-generated method stub

}

@Override
public void afterTextChanged(Editable s) {
// TODO Auto-generated method stub

}
});

Custom Filtering of ListView with a Custom List Adapter including section headers

I did not find a solution to my original problem, but I came up with a better approach to the whole situation. I didn't know there was an ExpandableListView available in Android. This is basically a ListView, but the items are divided into Groups and their Childs which are expandable and collapsable, so exactly what I wanted.

Here is how I implemented it with working filters and groups:

So, to start off, here is my main layout file. Please note that I am using Fragments, which is why the code is a bit different in terms of getting the context for example. The functionality of the component stays the same though.

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >

<EditText
android:id="@+id/fragment_data_search"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text"
android:hint="@string/data_search_hint"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp" />

<ExpandableListView
android:id="@+id/fragment_data_expandable_list_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:groupIndicator="@null" />

</LinearLayout>

You will also need two layout files for your header/group items and for your child items. My header item has a TextView which displays the category name and an ImageView which displays a + or - to show if the category is collapsed or expanded.

Here is my header layout file:

<?xml version="1.0" encoding="utf-8"?>

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/colorAccent"
android:descendantFocusability="blocksDescendants" >

<TextView
android:id="@+id/fragment_data_list_view_category"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:gravity="start"
android:textStyle="bold"
android:textSize="18sp"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingBottom="8dp"
android:paddingTop="8dp"
android:textColor="@android:color/primary_text_light"
android:text="@string/placeholder_header_listview"
android:maxLines="1"
android:ellipsize="end" />

<ImageView
android:id="@+id/fragment_data_list_view_category_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_gravity="end"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingBottom="8dp"
android:paddingTop="8dp"
android:contentDescription="@string/content_description_list_view_header"
android:src="@drawable/ic_remove_black_24dp"
android:tag="maximized"/>

</RelativeLayout>

The property android:descendantFocusability="blocksDescendants" fixed a bug when I tried setting an onItemClickListener. If you have that problem, try using RelativeLayout's for your child layout if you're not already. It fixed it for me, the onClickItemListener did not execute with a LinearLayout.

And here is my layout file for the child items:

<?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="horizontal"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingTop="8dp"
android:paddingBottom="8dp"
android:descendantFocusability="blocksDescendants" >

<TextView
android:id="@+id/fragment_data_list_view_carrier_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/placeholder_item_listview"
android:textSize="18sp"
android:textStyle="normal"
android:textColor="@android:color/primary_text_light"
android:maxLines="1"
android:ellipsize="end" />

</RelativeLayout>

The following code is from my fragment class, which handles all the logic for the ExpandableListView:

public class Fragment_Data extends Fragment {

private Context mContext;

private ExpandableListView expandableListView;
private List<String> categories_list;
private HashMap<String, List<Carrier>> carriers_list;
private DataExpandableListAdapter adapter;

private DatabaseHelper dbHelper;

@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
getActivity().setTitle(R.string.nav_item_data);
}

This first part shows the declaration of needed variables and the necessary method onViewCreated. The Carrier class is a custom object with properties like name, category and so on. The DatabaseHelper is also a custom class which handley my database and gets all the data for me, which is casted into Carrier objects. You can of course use anything you like as data types.

@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {

View view = inflater.inflate(R.layout.fragment_data_layout, container, false);
mContext = getContext();
expandableListView = (ExpandableListView) view.findViewById(R.id.fragment_data_expandable_list_view);
dbHelper = new DatabaseHelper(mContext, null, null, 1);

adapter = new DataExpandableListAdapter(mContext, categories_list, carriers_list);

displayList();

expandAllGroups();

EditText searchEditText = (EditText) view.findViewById(R.id.fragment_data_search);
searchEditText.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {

}

@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
adapter.filterData(s.toString());
expandAllGroups();
}

@Override
public void afterTextChanged(Editable s) {

}
});

expandableListView.setOnItemLongClickListener(deleteSelectedItem);
expandableListView.setOnChildClickListener(editSelectedItem);

return view;
}

The onCreate method deals with all the important stuff like setting the adapter, inflating the layout and setting onClick events for the items and a onTextChange event for the search field.

private void expandAllGroups() {
for(int i = 0; i < adapter.getGroupCount(); i++) {
expandableListView.expandGroup(i);
}
}

private void displayList() {
prepareListData();

adapter = new DataExpandableListAdapter(mContext, categories_list, carriers_list);
expandableListView.setAdapter(adapter);

expandAllGroups();
}

private void prepareListData() {
categories_list = new ArrayList<>();
carriers_list = new HashMap<>();

categories_list = dbHelper.getCategoryList();

for(int i = 0; i < categories_list.size(); i++) {
List<Carrier> carrierList = dbHelper.getCarriersWithCategory(categories_list.get(i));
carriers_list.put(categories_list.get(i), carrierList);
}
}

With expandAllGroups() you can simply expand all groups, because they are collapsed by default. The displayList() simply sets the Adapter for the ExpandableListView and calls prepareListData(), which fills both the category (group) list and the carrier (child) list. Note that the child List is a hashmap with the key being the category and the value a Carrier List by itself, so the Adapter knows which child items belong to which parent.

Here is the code for the Adapter:

class DataExpandableListAdapter extends BaseExpandableListAdapter {

private Context mContext;
private List<String> list_categories = new ArrayList<>();
private List<String> list_categories_original = new ArrayList<>();
private HashMap<String, List<Carrier>> list_carriers = new HashMap<>();
private HashMap<String, List<Carrier>> list_carriers_original = new HashMap<>();

DataExpandableListAdapter(Context context, List<String> categories, HashMap<String, List<Carrier>> carriers) {
this.mContext = context;
this.list_categories = categories;
this.list_categories_original = categories;
this.list_carriers = carriers;
this.list_carriers_original = carriers;
}

You need to have a copy of both of your original lists, if you want to use filtering. This is used for restoring all data when the search query is empty or again or simply different. The filter deletes all items that do not match from the original list.

@Override
public int getGroupCount() {
return this.list_categories.size();
}

@Override
public int getChildrenCount(int groupPosition) {
return this.list_carriers.get(this.list_categories.get(groupPosition)).size();
}

@Override
public Object getGroup(int groupPosition) {
return this.list_categories.get(groupPosition);
}

@Override
public Object getChild(int groupPosition, int childPosition) {
return this.list_carriers.get(this.list_categories.get(groupPosition)).get(childPosition);
}

@Override
public long getGroupId(int groupPosition) {
return groupPosition;
}

@Override
public long getChildId(int groupPosition, int childPosition) {
return childPosition;
}

@Override
public boolean hasStableIds() {
return true;
}

@Override
public boolean isChildSelectable(int groupPosition, int childPosition) {
return true;
}

Those methods need to be overwritten when you expand the BaseExpandableListAdapter. You can replace all the return null; statements with something similar like this, depending on your data.

@SuppressLint("InflateParams")
@Override
public View getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) {

String headerTitle = (String) getGroup(groupPosition);

if (convertView == null) {
LayoutInflater inflater = (LayoutInflater) this.mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
convertView = inflater.inflate(R.layout.listview_header_data_layout, null);
}

TextView lblListHeader = (TextView) convertView.findViewById(R.id.fragment_data_list_view_category);
lblListHeader.setText(headerTitle);

ImageView expandIcon = (ImageView) convertView.findViewById(R.id.fragment_data_list_view_category_icon);
if(isExpanded) {
expandIcon.setImageResource(R.drawable.ic_remove_black_24dp);
} else {
expandIcon.setImageResource(R.drawable.ic_add_black_24dp);
}

return convertView;
}

This overriden method simply inflates the layout for each header/group/category item and sets it text and image depending on the state of the group, if it's collapsed or expanded.

@SuppressLint("InflateParams")
@Override
public View getChildView(int groupPosition, int childPosition, boolean isLastChild, View convertView, ViewGroup parent) {

final String carrierName = ((Carrier)getChild(groupPosition, childPosition)).get_name();

if (convertView == null) {
LayoutInflater inflater = (LayoutInflater) this.mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
convertView = inflater.inflate(R.layout.listview_item_data_layout, null);
}

TextView txtListChild = (TextView) convertView.findViewById(R.id.fragment_data_list_view_carrier_name);

txtListChild.setText(carrierName);
return convertView;
}

Same thing with the child items.

Now finally to the filtering:
I use this custom method to filter out all items that I need matching the search query. Remember that this method is called each time the text of the EditText changes.

void filterData(String query) {
query = query.toLowerCase();
list_categories = new ArrayList<>();
list_carriers = new HashMap<>();

DatabaseHelper dbHelper = new DatabaseHelper(mContext, null, null, 1);

if(query.trim().isEmpty()) {
list_categories = new ArrayList<>(list_categories_original);
list_carriers = new HashMap<>(list_carriers_original);
notifyDataSetInvalidated();
}
else {
//Filter all data with the given search query. Yes, it's complicated
List<String> new_categories_list = new ArrayList<>();
HashMap<String, List<Carrier>> new_carriers_list = new HashMap<>();
List<String> all_categories_list = dbHelper.getCategoryList();
for(int i = 0; i < all_categories_list.size(); i++) {
List<Carrier> carriersWithCategoryList = dbHelper.getCarriersWithCategory(all_categories_list.get(i));
List<Carrier> matchingCarriersInCategory = new ArrayList<>();
for(Carrier carrierInCategory : carriersWithCategoryList) {
if(carrierInCategory.get_name().toLowerCase().contains(query)) {
matchingCarriersInCategory.add(carrierInCategory);
if(!new_categories_list.contains(all_categories_list.get(i))) {
new_categories_list.add(all_categories_list.get(i));
}
}
}
new_carriers_list.put(all_categories_list.get(i), matchingCarriersInCategory);
}

if(new_categories_list.size() > 0 && new_carriers_list.size() > 0) {
list_categories.clear();
list_categories.addAll(new_categories_list);
list_carriers.clear();
list_carriers.putAll(new_carriers_list);
}

notifyDataSetChanged();
}
}`

This might be very confusing, but it needs to be that complicated in my case because of my data structure. It might be easier in your case.

What this basically does is, that it first checks if the search query is empty. And if it is empty it resets both lists to the "backup" lists which I assigned in the constructor. I then call notifyDataSetInvalidated(); to tell the Adapter that it's content will be refilled. It might work aswell with notifyDataSetChanged();, I didn't test that, but it should since we set the original lists back to their old state.

Now, if the search query is not empty I go through every category and see if that specific category has any items that match the search query. If that is the case, that item is added to a new child list and it's category/parent will also be added to a new parent list, if it's not already in there.

And last but not least, the method checks if both lists are not empty. If they are not empty, the original lists are emptied and the new, filtered data, is put in and the Adapter is notified by calling notifyDataSetChanged();

I hope this will help anyone.

Search items from listview with custom adapter

You should implements Filterable in your Adapter

The clear() in custom listview adapter also updates the list data

public class AutoCompleteAdapter extends ArrayAdapter<String> implements Filterable {

private ArrayList<String> fullList;
private ArrayList<String> mOriginalValues;
private ArrayFilter mFilter;

public AutoCompleteAdapter(Context context, int resource, List<String> objects) {

super(context, resource, objects);
fullList = (ArrayList<String>) objects;
mOriginalValues = new ArrayList<String>(fullList);

}

@Override
public void add(String object) {
super.add(object);
fullList.add(object);
mOriginalValues.add(object);
}

@Override
public void clear() {
super.clear();
fullList.clear();
mOriginalValues.clear();
}

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

@Override
public String getItem(int position) {
return fullList.get(position);
}

@Override
public View getView(int position, View convertView, ViewGroup parent) {
TextView view = (TextView) super.getView(position, convertView, parent);
view.setEllipsize(TextUtils.TruncateAt.START);
return view;
}

@Override
public Filter getFilter() {
if (mFilter == null) {
mFilter = new ArrayFilter();
}
return mFilter;
}


private class ArrayFilter extends Filter {

@Override
protected FilterResults performFiltering(CharSequence prefix) {
FilterResults results = new FilterResults();

if (mOriginalValues == null) {
mOriginalValues = new ArrayList<String>(fullList);
}
ArrayList<String> list = new ArrayList<String>(mOriginalValues);
results.values = list;
results.count = list.size();


return results;
}

@SuppressWarnings("unchecked")
@Override
protected void publishResults(CharSequence constraint, FilterResults results) {

if (results.values != null) {
fullList = (ArrayList<String>) results.values;
} else {
fullList = new ArrayList<String>();
}
if (results.count > 0) {
notifyDataSetChanged();
} else {
notifyDataSetInvalidated();
}
}
}
}

Check it out. this adapter works great. Good luck.

ListView using a custom Adapter to implement a Filter is getting IndexOutOfBoundsException

The problem is the array is out of bounds because getCount() is returning the size of the warrantyList instead of the filteredList.

There is a good example of how to handle the two lists here:
Implementing Filterable

But in general, the WarrantyAdapter should be using the filteredList. The ItemFilter should be using the warrantyList to publish the filteredList.

I have your code running in my environment with the following changes:

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

@Override
public Object getItem(int position) {
return filteredList.get(position);
}

@Override
public long getItemId(int position) {
return position;
}

and I modified WarrantyAdapter to extend BaseAdapter and inside getView I use the filteredList...



Related Topics



Leave a reply



Submit