Search code examples
androidlistviewandroid-arrayadapterandroid-filterable

Selecting and searching simultaneously through custom listView not working


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.


Solution

  • 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!