I am new to Android and I am working on a grocery list app and I am running into some trouble.
Right now, I have made a custom listView that contains an image and a name field. I have also made a custom object GroceryItem that populates the listViews.
I want the user to be able to select GroceryItems from the listView, and also search through the list. Here is my custom adapter for my listView.
class CustomAdapter extends ArrayAdapter<GroceryItem> implements Filterable
{
private ArrayList<GroceryItem> mObjects;
private ArrayFilter mFilter;
private ArrayList<GroceryItem> mOriginalValues;
CustomAdapter(@NonNull Context context, int resource, @NonNull ArrayList<GroceryItem> inputValues) {
super(context, resource, inputValues);
mObjects = new ArrayList<>(inputValues);
}
public void setBackup(){
mOriginalValues = new ArrayList<GroceryItem>();
mOriginalValues.addAll(mObjects);
}
@SuppressLint("ClickableViewAccessibility")
@Override
public View getView(int position, View convertView, ViewGroup parent) {
// Get the data item for this position
final GroceryItem currentGroceryItem = getItem(position);
// Check if an existing view is being reused, otherwise inflate the view
if (convertView == null) {
convertView = getLayoutInflater().inflate(R.layout.custom_layout, parent, false);
}
// Lookup view for data population
ImageView groceryImage = (ImageView) convertView.findViewById(R.id.groceryImage);
TextView groceryNameText = (TextView) convertView.findViewById(R.id.groceryName);
LinearLayout overallItem = (LinearLayout) convertView.findViewById(R.id.linearLayout);
// Populate the data into the template view using data
groceryImage.setImageResource(currentGroceryItem.getImageID());
groceryNameText.setText(currentGroceryItem.getName());
// Set onClickListener for overallItem
overallItem.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
searchBar.clearFocus();
// Changes the selection status of the GroceryItem
currentGroceryItem.toggle();
// Changes the colour of the background accordingly (to show selection)
if(currentGroceryItem.isSelected()){
v.setBackgroundColor(0xFF83B5C7);
} else{
v.setBackgroundColor(0xFFFFFFFF);
}
}
});
// Return the completed view to render on screen
return convertView;
}
////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////FOR SEARCH FUNCTIONALITY////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////
public View getViewByPosition(int pos, ListView myListView) {
final int firstListItemPosition = myListView.getFirstVisiblePosition();
final int lastListItemPosition = firstListItemPosition + myListView.getChildCount() - 1;
if (pos < firstListItemPosition || pos > lastListItemPosition ) {
return getView(pos, null, myListView);
} else {
final int childIndex = pos - firstListItemPosition;
return myListView.getChildAt(childIndex);
}
}
public void fixToggling(){
runOnUiThread(new Runnable() {
@Override
public void run() {
for(int i = 0; i < getCount(); ++i){
// Finds view at index i in listView, which is a global variable
View view = getViewByPosition(i, listView);
if(getItem(i).isSelected()){
view.setBackgroundColor(0XFF83B5C7);
} else{
view.setBackgroundColor(0xFFFFFFFF);
}
}
}
});
}
// The following function reverses the filter (sets everything to default)
public void reverseFilter(){
//Replaces mObjects with mOriginal Values
mObjects.clear();
mObjects.addAll(mOriginalValues);
//Loads mObjects (now filled with the original items) into the adapter
this.clear();
this.addAll(mObjects);
fixToggling();
}
// The following function applies a filter given a query
public void applyFilter(String query) {
if(query == null || query.length() == 0){
reverseFilter();
} else{
// Creates a new array filter
mFilter = new ArrayFilter();
// performs the filters, and publishes the result (i.e. writes the result into
// mObjects)
mFilter.publishResults(query, mFilter.performFiltering(query));
// Clears current content of the adapter
this.clear();
// Fills the adapter with the content of the filtered result
this.addAll(mObjects);
fixToggling();
}
}
private class ArrayFilter extends Filter {
@Override
protected FilterResults performFiltering(CharSequence prefix) {
final FilterResults results = new FilterResults();
if(mOriginalValues == null){
mOriginalValues = new ArrayList<>(mObjects);
}
// If there is no input query
if (prefix == null || prefix.length() == 0) {
// Make a copy of mOriginalValues into the list
final ArrayList<GroceryItem> list;
list = new ArrayList<>(mOriginalValues);
// Set the FilterResults value to the copy of mOriginalValues
results.values = list;
results.count = list.size();
// If there is an input query (at least one character in length)
} else {
// Converts the query to a lowercase String
final String prefixString = prefix.toString().toLowerCase();
// Makes a copy of mOriginalValues into the ArrayList "values"
final ArrayList<GroceryItem> values;
values = new ArrayList<>(mOriginalValues);
final int count = values.size();
// Makes a new empty ArrayList
final ArrayList<GroceryItem> newValues = new ArrayList<>();
// Iterates through the number of elements in mOriginalValues
for (int i = 0; i < count; i++) {
// Gets the GroceryItem element at position i from mOriginalValues' copy
final GroceryItem value = values.get(i);
// Extracts the name of the GroceryItem element into valueText
final String valueText = value.getName().toLowerCase();
// First match against the whole, non-splitted value
if (valueText.startsWith(prefixString)) {
newValues.add(value);
}
else {
// Splits the one String into all its constituent words
final String[] words = valueText.split(" ");
// If any of the constituent words starts with the prefix, adds them
for (String word : words) {
if (word.startsWith(prefixString)) {
newValues.add(value);
break;
}
}
}
}
// Sets the FilterResult value to the newValues ArrayList. mOriginalValues is
// preserved.
results.values = newValues;
results.count = newValues.size();
// Changes mObjects from (potentially) the original items or the previously filtered
// results to the new filtered results. Needs to be loaded into the adapter still.
mObjects.clear();
mObjects.addAll(newValues);
}
return results;
}
@Override
protected void publishResults(CharSequence constraint, FilterResults results) {
notifyDataSetChanged();
}
}
////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////
}
Here’s the problem I am running into. I have four grocery items right now: Apple, Banana, Grapes, and Mango. If I select Grapes, everything works fine. It shows Grape with a blue background and all the other items with a white background. When I start typing “gr” into the search bar, things work fine as well. I only see Grapes, and the item is shown selected (blue background). When I type “grm”, everything disappears and nothing is selected. But, when I backspace one character and go back to “gr”, it shows me Grapes, but it is no longer selected.
Another similar problem. Once again starting off with Apple, Banana, Grapes, and Mango, if I select Grapes and search “b”, I get Banana unselected. Great. Now, when I select Banana, it shows it as selected. But, once I backspace, I go back to the full list of items and only Grapes are selected.
I’ve written the fixToggling() function to iterate through every view and fix the background colour as necessary. I’ve also done some debugging to learn that the isSelected Boolean from each groceryItem is properly recorded, so it’s not the problem that the app is not remembering which ones are supposed to be selected or not selected. For some reason, the toggling is just off.
Can anyone help? I just want to allow users to use the search functionality and item selection simultaneously.
Try this adapter code:
class CustomAdapter2 extends ArrayAdapter<GroceryItem> implements Filterable
{
private ArrayList<GroceryItem> mObjects;
private ArrayList<GroceryItem> mOriginalValues;
private ArrayFilter mFilter;
private LayoutInflater mLayoutInflater;
CustomAdapter2(@NonNull Context context, int resource, @NonNull ArrayList<GroceryItem> inputValues) {
super(context, resource, inputValues);
mLayoutInflater = LayoutInflater.from(context);
mObjects = inputValues;
}
public void setBackup(){
mOriginalValues = new ArrayList<>();
mOriginalValues.addAll(mObjects);
}
@SuppressLint("ClickableViewAccessibility")
@Override
public View getView(int position, View convertView, ViewGroup parent) {
// Get the data item for this position
GroceryItem currentGroceryItem = getItem(position);
// Check if an existing view is being reused, otherwise inflate the view
if (convertView == null) {
convertView = mLayoutInflater.inflate(R.layout.custom_layout, parent, false);
}
// Lookup view for data population
ImageView groceryImage = (ImageView) convertView.findViewById(R.id.groceryImage);
TextView groceryNameText = (TextView) convertView.findViewById(R.id.groceryName);
LinearLayout overallItem = (LinearLayout) convertView.findViewById(R.id.linearLayout);
// Populate the data into the template view using data
groceryImage.setImageResource(currentGroceryItem.getImageID());
groceryNameText.setText(currentGroceryItem.getName());
if(currentGroceryItem.isSelected()){
overallItem.setBackgroundColor(0xFF83B5C7);
} else{
overallItem.setBackgroundColor(0xFFFFFFFF);
}
// Set onClickListener for overallItem
overallItem.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
int pos = (int)v.getTag();
GroceryItem currentGroceryItem = getItem(pos);
searchBar.clearFocus();
// Changes the selection status of the GroceryItem
currentGroceryItem.toggle();
// Changes the colour of the background accordingly (to show selection)
if(currentGroceryItem.isSelected()){
v.setBackgroundColor(0xFF83B5C7);
} else{
v.setBackgroundColor(0xFFFFFFFF);
}
}
});
// Return the completed view to render on screen
convertView.setTag(position);
return convertView;
}
/////////////////////////////////FOR SEARCH FUNCTIONALITY///////////////////////////////////
@NonNull
@Override
public Filter getFilter() {
if(mFilter == null){
// Make a backup copy of list
setBackup();
mFilter = new ArrayFilter();
}
return mFilter;
}
private class ArrayFilter extends Filter {
@Override
protected FilterResults performFiltering(CharSequence prefix) {
FilterResults results = new FilterResults();
ArrayList<GroceryItem> list = new ArrayList<>();
// If there is no input query
if (prefix == null || prefix.length() == 0) {
// Set the FilterResults value to the copy of mOriginalValues
list = new ArrayList<>(mOriginalValues);
// If there is an input query (at least one character in length)
} else {
// Converts the query to a lowercase String
String prefixString = prefix.toString().toLowerCase();
// Iterates through the number of elements in mOriginalValues
for (int i = 0; i < mOriginalValues.size(); i++) {
// Gets the GroceryItem element at position i from mOriginalValues' copy
GroceryItem value = mOriginalValues.get(i);
// Extracts the name of the GroceryItem element into valueText
String valueText = value.getName().toLowerCase();
// First match against the whole, non-splitted value
if (valueText.startsWith(prefixString)) {
list.add(value);
}
else {
// Splits the one String into all its constituent words
String[] words = valueText.split(" ");
// If any of the constituent words starts with the prefix, adds them
for (String word : words) {
if (word.startsWith(prefixString)) {
list.add(value);
break;
}
}
}
}
}
// Sets the FilterResult value to list ArrayList.
results.values = list;
results.count = list.size();
return results;
}
@Override
protected void publishResults(CharSequence constraint, FilterResults results) {
clear();
addAll((ArrayList<GroceryItem>)results.values);
notifyDataSetChanged();
}
}
////////////////////////////////////////////////////////////////////////////////////////////
}
Hope that helps!