Filtering Listview with Custom (Object) Adapter

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.

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

}
});

Android Search Filter with custom listview, custom adapter and custom object

You should call getFilter().filter(...) not on your datasource (values) but as

adapter.getFilter().filter(s.toString();

No typecast needed.

When filtering data, do not ever modify original dataset, in publishResults modify its internal copy using adapter's getItem, add and clear methods or use a manual copy of data for getView. Also override getCount to size of this manual copy.

see here as one of the samples using internal data copy.

for the first alternative (most sane to me), in getView use getItem to acquire Record not from original dataset but from internal one. Do not override getCount. in publishResults use clear and then add (addAll) to copy filtered data.

for the second alternative, copy data in constructor to, say, filteredData and use filteredData in getView() getCount() and publishResults instead of data. Leave performFiltering as is

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 custom array adapter and bring back old items again

My solution:

 public class ResultsArrayAdapter extends ArrayAdapter<SuchErgebnis> {

Context myContext;
int layoutResourceId;
ArrayList<SuchErgebnis> ergebnisListeOriginal = null;
ArrayList<SuchErgebnis> ergebnisListeGefiltert = null;
private Filter filter;

public ResultsArrayAdapter(Context context, int textViewResourceId,
ArrayList<SuchErgebnis> objects) {
super(context, textViewResourceId, objects);

this.myContext = context;
this.layoutResourceId = textViewResourceId;

this.ergebnisListeOriginal = new ArrayList<SuchErgebnis>(objects);
this.ergebnisListeGefiltert = new ArrayList<SuchErgebnis>(objects);
}

@Override
public View getView(int position, View convertView, ViewGroup parent) {
View row = convertView;
ErgebnisHolder eHolder = null;
SuchErgebnis ergebnis = ergebnisListeGefiltert.get(position);

if (row == null) // Wird zum ersten Mal gelanden...
{
LayoutInflater inflater = (LayoutInflater) myContext
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);

row = inflater.inflate(R.layout.ergebnis_list_item, parent, false);

eHolder = new ErgebnisHolder();
eHolder.eTitel = (TextView) row
.findViewById(R.id.ergebnis_list_item_textview_titel);
eHolder.eInfo = (TextView) row
.findViewById(R.id.ergebnis_list_item_textview_info);
eHolder.eTreffer = (TextView) row
.findViewById(R.id.ergebnis_list_item_textview_treffer);

row.setTag(eHolder);
} else { // Wurde schoneinmal geladen, Views sind noch gespeichert...
eHolder = (ErgebnisHolder) row.getTag();
}

eHolder.eTitel.setText(ergebnis.titel);
eHolder.eInfo.setText(ergebnis.info != null ? ergebnis.info : "");
eHolder.eTreffer.setText(ergebnis.treffer != null ? ergebnis.treffer
: "");
row.setPadding(
ergebnis.isChild ? Main.mFHelper
.getPixels(10 * ergebnis.childNumber) : 0, 0, 0, 0);

return row;
}

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

static class ErgebnisHolder {
TextView eTitel;
TextView eInfo;
TextView eTreffer;
}

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

@Override
public SuchErgebnis getItem(int position) {
return ergebnisListeGefiltert.get(position);
}

private class ResultFilter extends Filter {

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

String filterText = constraint.toString().toLowerCase();
if (filterText == null || filterText.length() == 0) {
synchronized (this) {
results.values = ergebnisListeOriginal;
results.count = ergebnisListeOriginal.size();
}
} else {
ArrayList<SuchErgebnis> gefilterteListe = new ArrayList<SuchErgebnis>();
ArrayList<SuchErgebnis> ungefilterteListe = new ArrayList<SuchErgebnis>();
synchronized (this) {
ungefilterteListe.addAll(ergebnisListeOriginal);
}
for (int i = 0, l = ungefilterteListe.size(); i < l; i++) {
SuchErgebnis m = ungefilterteListe.get(i);
if (m.titel.toLowerCase().contains(filterText)) {
gefilterteListe.add(m);
}
}
results.values = gefilterteListe;
results.count = gefilterteListe.size();

}

return results;
}

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

ergebnisListeGefiltert = (ArrayList<SuchErgebnis>) results.values;
if(results.count > 0)
{
notifyDataSetChanged();
}else{
notifyDataSetInvalidated();
}


}

}
}


Related Topics



Leave a reply



Submit