Thursday, April 28, 2016

How to Filter objects in custom Adapter using SearchView and ListView

Hi guys,

Recently I was asked to make a ListView with custom adapter. To show the list view item I have used CardView. Of course this was the easy part. Then again I was asked to include the search functionality for this ListView. In this blog post I'm going to show you the code which I used to achieve this target. I have faced several issues while I'm doing this and hope you can avoid them by using the code I used. Please make a comment if you know a better practice. I'm always looking for knowledge.

I'm going to add comment lines so you can understand the code well. Some explanations have done before the code and here included are only the necessary java and xml files. Ok, Here we go.

Main components used in this projects are,
1. android.support.v7.widget.SearchView;
2. android.support.v4.app.ListFragment;
3. android.support.v7.widget.CardView
4. Custom ArrayAdapter.

I have used a generated master/detail project from android studio. As i said I'm NOT going to include all java files. Here is the fragment that generate list.

1. ProductListFragment.java

1:  package com.???.????.View;  
2:  import android.app.Activity;  
3:  import android.app.SearchManager;  
4:  import android.content.Context;  
5:  import android.os.Bundle;  
6:  import android.support.v4.app.ListFragment;  
7:  import android.support.v7.widget.SearchView;  
8:  import android.util.Log;  
9:  import android.view.LayoutInflater;  
10:  import android.view.Menu;  
11:  import android.view.MenuInflater;  
12:  import android.view.MenuItem;  
13:  import android.view.View;  
14:  import android.view.ViewGroup;  
15:  import android.widget.ArrayAdapter;  
16:  import android.widget.ListView;  
17:  import com.???.????.Adapter.ProductItemAdapter; // can't expose my company :-)  
18:  import com..???.????.Model.ProductItem;  
19:  import com..???.????.R;  
20:  import java.util.ArrayList;  
21:  import java.util.HashMap;  
22:  import java.util.List;  
23:  import java.util.Map;  
24:  public class ProductListFragment extends ListFragment {  
25:    private static final String STATE_ACTIVATED_POSITION = "activated_position";  
26:    private Callbacks mCallbacks = sDummyCallbacks;  
27:    private int mActivatedPosition = ListView.INVALID_POSITION;  
28:    public static Map<String, ProductItem> ITEM_MAP = new HashMap<String, ProductItem>();  
29:    public static List<ProductItem> list = new ArrayList<ProductItem>();  
30:    private SearchView searchView;  
31:    private SearchView.OnQueryTextListener queryTextListener;  
32:    private ProductItemAdapter productItemAdapter;  
33:    public interface Callbacks {  
34:      public void onItemSelected(String id);  
35:    }  
36:    private static Callbacks sDummyCallbacks = new Callbacks() {  
37:      @Override  
38:      public void onItemSelected(String id) {  
39:      }  
40:    };  
41:    public ProductListFragment() {  
42:    }  
43:    @Override  
44:    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {  
45:      inflater.inflate(R.menu.menu_searchable, menu);  
46:      MenuItem searchItem = menu.findItem(R.id.searchButtonInMenu);  
47:      SearchManager searchManager = (SearchManager) getActivity().getSystemService(Context.SEARCH_SERVICE);  
48:      if (searchItem != null) {  
49:        searchView = (SearchView) searchItem.getActionView();  
50:      }  
51:      if(searchItem != null) {  
52:        searchView.setSearchableInfo(searchManager.getSearchableInfo(getActivity().getComponentName()));  
53:        queryTextListener = new SearchView.OnQueryTextListener() {  
54:          @Override  
55:          public boolean onQueryTextChange(String newText) {  
56:            productItemAdapter.getFilter().filter(newText); // I have overide getFilter() in my custom adapter  
57:            return true;  
58:          }  
59:          @Override  
60:          public boolean onQueryTextSubmit(String query) {  
61:            return true;  
62:          }  
63:        };  
64:        searchView.setOnQueryTextListener(queryTextListener);  
65:      }  
66:      super.onCreateOptionsMenu(menu, inflater);  
67:    }  
68:    @Override  
69:    public void onCreate(Bundle savedInstanceState) {  
70:      super.onCreate(savedInstanceState);  
71:      productItemAdapter = new ProductItemAdapter(getActivity(), R.layout.product_item, getModel());  
72:      setListAdapter(productItemAdapter);  
73:    }  
74:    private List<ProductItem> getModel() {  
75:      list.clear();  
76:      ProductItem newItem = new ProductItem();  
77:      newItem.setId("1");  
78:      newItem.setProductImage(R.drawable.drone);  
79:      newItem.setProductName("TestTittle");  
80:      newItem.setProductCode("12434");  
81:      newItem.setProductPrice("$ 450.00");  
82:      newItem.setProductDescription("This is a line that describe the above product. Add all the details you want here. From now ther will be dummy" +  
83:          "content. as toyu sdf asdf asd fdsdfk sdfsd asfsdf dfsdfdf sdp;sdmsm");  
84:      ProductItem newItem1 = new ProductItem();  
85:      newItem1.setId("2");  
86:      newItem1.setProductImage(R.drawable.shoe);  
87:      newItem1.setProductName("TestTittle1");  
88:      newItem1.setProductCode("12434111");  
89:      newItem1.setProductDescription("THIS IS FOR SECOND ITEM AND THESE ARE STATIC CONTENTS. :-) This is a line that describe the above product. Add all the details you want here. From now ther will be dummy" +  
90:          "content. as toyu sdf asdf asd fdsdfk sdfsd asfsdf dfsdfdf sdp;sdmsm");  
91:      newItem1.setProductPrice("$ 250.00");  
92:      ProductItem newItem3 = new ProductItem();  
93:      newItem3.setId("3");  
94:      newItem3.setProductImage(R.drawable.shoe);  
95:      newItem3.setProductName("Shoes of me");  
96:      newItem3.setProductCode("12434111");  
97:      newItem3.setProductDescription("THIS IS FOR SECOND ITEM AND THESE ARE STATIC CONTENTS. :-) This is a line that describe the above product. Add all the details you want here. From now ther will be dummy" +  
98:          "content. as toyu sdf asdf asd fdsdfk sdfsd asfsdf dfsdfdf sdp;sdmsm");  
99:      newItem3.setProductPrice("$ 250.00");  
100:      ProductItem newItem4 = new ProductItem();  
101:      newItem4.setId("4");  
102:      newItem4.setProductImage(R.drawable.shoe);  
103:      newItem4.setProductName("drone is here");  
104:      newItem4.setProductCode("12434111");  
105:      newItem4.setProductDescription("THIS IS FOR SECOND ITEM AND THESE ARE STATIC CONTENTS. :-) This is a line that describe the above product. Add all the details you want here. From now ther will be dummy" +  
106:          "content. as toyu sdf asdf asd fdsdfk sdfsd asfsdf dfsdfdf sdp;sdmsm");  
107:      newItem4.setProductPrice("$ 250.00");  
108:      ProductItem newItem5 = new ProductItem();  
109:      newItem5.setId("5");  
110:      newItem5.setProductImage(R.drawable.shoe);  
111:      newItem5.setProductName("drone is here");  
112:      newItem5.setProductCode("12434111");  
113:      newItem5.setProductDescription("THIS IS FOR SECOND ITEM AND THESE ARE STATIC CONTENTS. :-) This is a line that describe the above product. Add all the details you want here. From now ther will be dummy" +  
114:          "content. as toyu sdf asdf asd fdsdfk sdfsd asfsdf dfsdfdf sdp;sdmsm");  
115:      newItem5.setProductPrice("$ 250.00");  
116:      list.add(newItem);  
117:      ITEM_MAP.put(String.valueOf(list.size()), newItem);  
118:      list.add(newItem1);  
119:      ITEM_MAP.put(String.valueOf(list.size()), newItem1);  
120:      list.add(newItem3);  
121:      ITEM_MAP.put(String.valueOf(list.size()), newItem3);  
122:      list.add(newItem4);  
123:      ITEM_MAP.put(String.valueOf(list.size()), newItem4);  
124:      list.add(newItem5);  
125:      ITEM_MAP.put(String.valueOf(list.size()), newItem5);  
126:      return list;  
127:    }  
128:    @Override  
129:    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {  
130:      View v = inflater.inflate(R.layout.product_list_container,null);  
131:      setHasOptionsMenu(true);  
132:      return v;  
133:    }  
134:    @Override  
135:    public void onViewCreated(View view, Bundle savedInstanceState) {  
136:      super.onViewCreated(view, savedInstanceState);  
137:      // Restore the previously serialized activated item position.  
138:      if (savedInstanceState != null && savedInstanceState.containsKey(STATE_ACTIVATED_POSITION)) {  
139:        setActivatedPosition(savedInstanceState.getInt(STATE_ACTIVATED_POSITION));  
140:      }  
141:    }  
142:    @Override  
143:    public void onAttach(Activity activity) {  
144:      super.onAttach(activity);  
145:      // Activities containing this fragment must implement its callbacks.  
146:      if (!(activity instanceof Callbacks)) {  
147:        throw new IllegalStateException("Activity must implement fragment's callbacks.");  
148:      }  
149:      mCallbacks = (Callbacks) activity;  
150:    }  
151:    @Override  
152:    public void onDetach() {  
153:      super.onDetach();  
154:      // Reset the active callbacks interface to the dummy implementation.  
155:      mCallbacks = sDummyCallbacks;  
156:    }  
157:    @Override  
158:    public void onListItemClick(ListView listView, View view, int position, long id) {  
159:      super.onListItemClick(listView, view, position, id);  
160:      // Notify the active callbacks interface (the activity, if the  
161:      // fragment is attached to one) that an item has been selected.  
162:      mCallbacks.onItemSelected(list.get(position).getId());  
163:    }  
164:    @Override  
165:    public void onSaveInstanceState(Bundle outState) {  
166:      super.onSaveInstanceState(outState);  
167:      if (mActivatedPosition != ListView.INVALID_POSITION) {  
168:        // Serialize and persist the activated item position.  
169:        outState.putInt(STATE_ACTIVATED_POSITION, mActivatedPosition);  
170:      }  
171:    }  
172:    /**  
173:     * Turns on activate-on-click mode. When this mode is on, list items will be  
174:     * given the 'activated' state when touched.  
175:     */  
176:    public void setActivateOnItemClick(boolean activateOnItemClick) {  
177:      // When setting CHOICE_MODE_SINGLE, ListView will automatically  
178:      // give items the 'activated' state when touched.  
179:      getListView().setChoiceMode(activateOnItemClick ? ListView.CHOICE_MODE_SINGLE : ListView.CHOICE_MODE_NONE);  
180:    }  
181:    private void setActivatedPosition(int position) {  
182:      if (position == ListView.INVALID_POSITION) {  
183:        getListView().setItemChecked(mActivatedPosition, false);  
184:      } else {  
185:        getListView().setItemChecked(position, true);  
186:      }  
187:      mActivatedPosition = position;  
188:    }  
189:  }  

2. menu_searchable.xml (where the search button creates)

1:  <?xml version="1.0" encoding="utf-8"?>  
2:  <menu xmlns:android="http://schemas.android.com/apk/res/android"  
3:    xmlns:app="http://schemas.android.com/apk/res-auto">  
4:    <item  
5:      android:id="@+id/searchButtonInMenu"  
6:      android:title="Search Products"  
7:      android:icon="@android:drawable/ic_menu_search"  
8:      app:showAsAction ="collapseActionView|ifRoom"  
9:      app:actionViewClass="android.support.v7.widget.SearchView" />  
10:  </menu>  

3. product_list_container.xml  (the fragments layout and where the ListView is. NOTE you have to use android:id="@android:id/list" because ProductListFragment.java is extending from ListFragment).
1:  <?xml version="1.0" encoding="utf-8"?>  
2:  <LinearLayout  
3:    xmlns:android="http://schemas.android.com/apk/res/android"  
4:    android:layout_width="match_parent"  
5:    android:orientation="vertical"  
6:    android:layout_height="match_parent"  
7:    android:background="#ddd"  
8:    >  
9:    <RelativeLayout  
10:      android:layout_width="match_parent"  
11:      android:layout_height="wrap_content"  
12:      android:background="#fff"  
13:      android:paddingTop="2dp"  
14:      android:paddingBottom="2dp">  
15:      <CheckBox  
16:        android:id="@+id/checkboxActive"  
17:        android:layout_width="wrap_content"  
18:        android:layout_height="wrap_content"  
19:        android:text="Active"/>  
20:      <CheckBox  
21:        android:id="@+id/checkboxInactive"  
22:        android:layout_width="wrap_content"  
23:        android:layout_height="wrap_content"  
24:        android:text="Inactive"  
25:        android:layout_below="@id/checkboxActive"/>  
26:      <Spinner  
27:        android:layout_width="wrap_content"  
28:        android:layout_height="wrap_content"  
29:        android:layout_alignParentRight="true"  
30:        android:drawSelectorOnTop="true"  
31:        android:entries="@array/array_name">  
32:      </Spinner>  
33:    </RelativeLayout>  
34:    <ListView  
35:      android:id="@android:id/list"  
36:      android:layout_width="match_parent"  
37:      android:layout_height="match_parent"  
38:      android:divider="@android:color/transparent"  
39:      android:dividerHeight="4dp"  
40:      android:background="#ddd"  
41:      android:layout_marginLeft="6dp"  
42:      android:layout_marginRight="6dp"  
43:      />  
44:  </LinearLayout>  

4. product_item.xml (layout for each row in ListView. I have used CardView here).

1:  <?xml version="1.0" encoding="utf-8"?>  
2:  <android.support.v7.widget.CardView  
3:    xmlns:android="http://schemas.android.com/apk/res/android"  
4:    xmlns:app="http://schemas.android.com/apk/res-auto"  
5:    android:layout_width="match_parent"  
6:    android:layout_height="wrap_content"  
7:    app:cardBackgroundColor = "#fff"  
8:    app:cardCornerRadius="2dp"  
9:    >  
10:    <LinearLayout  
11:      android:padding="2dp"  
12:      android:layout_margin="8dp"  
13:      android:layout_width="match_parent"  
14:      android:layout_height="match_parent"  
15:      android:orientation="horizontal"  
16:      android:background="@android:color/white">  
17:      <ImageView  
18:        android:id="@+id/productImage"  
19:        android:layout_width="0dp"  
20:        android:layout_height="match_parent"  
21:        android:layout_weight="1"  
22:        android:gravity="center"  
23:        android:src="@drawable/drone" />  
24:      <LinearLayout  
25:        android:layout_width="0dp"  
26:        android:layout_height="wrap_content"  
27:        android:layout_gravity="center"  
28:        android:layout_weight="2"  
29:        android:orientation="vertical">  
30:        <TextView  
31:          android:id="@+id/productName"  
32:          android:layout_width="match_parent"  
33:          android:layout_height="match_parent"  
34:          android:gravity="center"  
35:          android:text="Pro Title"  
36:          android:textStyle="bold" />  
37:        <TextView  
38:          android:id="@+id/productCode"  
39:          android:layout_width="match_parent"  
40:          android:layout_height="match_parent"  
41:          android:gravity="center"  
42:          android:text="Sub Title" />  
43:      </LinearLayout>  
44:      <TextView  
45:        android:id="@+id/productPrice"  
46:        android:layout_width="0dp"  
47:        android:layout_height="match_parent"  
48:        android:layout_weight="1"  
49:        android:gravity="center"  
50:        android:text="Sub Title" />  
51:    </LinearLayout>  
52:  </android.support.v7.widget.CardView>  

5. ProductItem.java (POJO class)

1:  package com.duo.cloudchargev1.Model;  
2:  /**  
3:   * Created by Sijith on 4/7/2016.  
4:   */  
5:  public class ProductItem {  
6:    private String id;  
7:    private int productImage;  
8:    private String productName;  
9:    private String productCode;  
10:    private String productPrice;  
11:    private String productDescription;  
12:    public String getId() {  
13:      return id;  
14:    }  
15:    public void setId(String id) {  
16:      this.id = id;  
17:    }  
18:    public int getProductImage() {  
19:      return productImage;  
20:    }  
21:    public void setProductImage(int productImage) {  
22:      this.productImage = productImage;  
23:    }  
24:    public String getProductName() {  
25:      return productName;  
26:    }  
27:    public void setProductName(String productName) {  
28:      this.productName = productName;  
29:    }  
30:    public String getProductCode() {  
31:      return productCode;  
32:    }  
33:    public void setProductCode(String productCode) {  
34:      this.productCode = productCode;  
35:    }  
36:    public String getProductPrice() {  
37:      return productPrice;  
38:    }  
39:    public void setProductPrice(String productPrice) {  
40:      this.productPrice = productPrice;  
41:    }  
42:    public String getProductDescription() {  
43:      return productDescription;  
44:    }  
45:    public void setProductDescription(String productDescription) {  
46:      this.productDescription = productDescription;  
47:    }  
48:    @Override  
49:    public String toString() {  
50:      return productName;  
51:    }  
52:  }  


6. ProductItemAdapter.java (A custom adapter and most important java class. I am going to include important things I have learnt after the code.)

1:  package com.????.??????.Adapter;  
2:  import android.content.Context;  
3:  import android.view.LayoutInflater;  
4:  import android.view.View;  
5:  import android.view.ViewGroup;  
6:  import android.widget.ArrayAdapter;  
7:  import android.widget.Filter;  
8:  import android.widget.ImageView;  
9:  import android.widget.TextView;  
10:  import com.????.??????.Model.ProductItem;  
11:  import com.????.??????.R;  
12:  import java.util.ArrayList;  
13:  import java.util.List;  
14:  public class ProductItemAdapter extends ArrayAdapter<ProductItem> {  
15:    private List<ProductItem> productItemListOriginal;  
16:    private List<ProductItem> productItemListFiltered; // if you are not keeping a duplicate no product item views when deleting values in search view  
17:    private final Context context;  
18:    public ProductItemAdapter(Context context, int resource, List<ProductItem> productItems) {  
19:      super(context, resource, productItems);  
20:      this.context = context;  
21:      this.productItemListOriginal = productItems;  
22:      this.productItemListFiltered = productItems;  
23:    }  
24:    static class ViewHolder {  
25:      protected ImageView productImage;  
26:      protected TextView productName;  
27:      protected TextView productCode;  
28:      protected TextView productPrice;  
29:    }  
30:    @Override  
31:    public View getView(int position, View convertView, ViewGroup parent) {  
32:      View view = null;  
33:      if(convertView == null){  
34:        view = LayoutInflater.from(parent.getContext()).inflate(R.layout.product_item,parent,false);  
35:        ViewHolder viewHolder = new ViewHolder();  
36:        viewHolder.productImage = (ImageView) view.findViewById(R.id.productImage);  
37:        viewHolder.productName = (TextView)view.findViewById(R.id.productName);  
38:        viewHolder.productCode = (TextView)view.findViewById(R.id.productCode);  
39:        viewHolder.productPrice = (TextView)view.findViewById(R.id.productPrice);  
40:        view.setTag(viewHolder);  
41:      }  
42:      else{  
43:        view = convertView;  
44:      }  
45:      ViewHolder holder = (ViewHolder) view.getTag();  
46:      holder.productImage.setImageResource(productItemListFiltered.get(position).getProductImage());  
47:      holder.productName.setText(productItemListFiltered.get(position).getProductName());  
48:      holder.productCode.setText(productItemListFiltered.get(position).getProductCode());  
49:      holder.productPrice.setText(productItemListFiltered.get(position).getProductPrice());  
50:      return view;  
51:    }  
52:    @Override  
53:    public Filter getFilter() {  
54:      return new Filter() {  
55:        @Override  
56:        protected FilterResults performFiltering(CharSequence constraint) {  
57:          constraint = constraint.toString().toLowerCase();  
58:          FilterResults result = new FilterResults();  
59:          if (constraint != null && constraint.toString().length() > 0) {  
60:            List<ProductItem> founded = new ArrayList<ProductItem>();  
61:            for(ProductItem item: productItemListOriginal){  
62:              if(item.toString().toLowerCase().contains(constraint)){  
63:                founded.add(item);  
64:              }  
65:            }  
66:            result.values = founded;  
67:            result.count = founded.size();  
68:          }else {  
69:            result.values = productItemListOriginal;  
70:            result.count = productItemListOriginal.size();  
71:          }  
72:          return result;  
73:        }  
74:        @Override  
75:        protected void publishResults(CharSequence constraint, FilterResults results) {  
76:          if (results != null) {  
77:            productItemListFiltered = (List<ProductItem>) results.values; // set the adapter list to data  
78:            notifyDataSetChanged(); // notify data set change  
79:          } else {  
80:            notifyDataSetChanged(); // notify data set change  
81:          }  
82:        }  
83:      };  
84:    }  
85:    @Override  
86:    public int getCount() { // indexoutofboundsexception if not implemented  
87:      return productItemListFiltered.size();   
88:    }  
89:  }  


Important things to Note...
  1. Override the getCount(). If you are not override this there will be indexOutOfBoundsException
  2. Create a original and duplicate Lists. If you make the changes to the original list, deleting        characters in SearchView will show you empty list. Always make changes to the duplicate list.

Here how it looks like.




Hope you can get something from this post. Happy Coding :-).

No comments:

Post a Comment