Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ArrayAdapter - filtering with multiple search terms

I have recently added a bounty to this SO question, but realise the original question asks for a SimpleAdapter and not an ArrayAdapter. So, this question relates to the ArrayAdapter:

I would like to be able to filter an ArrayAdapter in a ListView using multiple search terms. For example, if my list contains the following items:

a thing
another thing
a different thing
nothing
some thing

If I search for 'thing' then all the terms should be returned in the filter results. This is normal behaviour that can be accomplished using the following code:

    private TextWatcher filterTextWatcher = new TextWatcher() {

    public void onTextChanged(CharSequence s, int start, int before, int count) {
        // TODO Auto-generated method stub

    }

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

    }

    public void afterTextChanged(Editable s) {
        // TODO Auto-generated method stub
        myAdapter.getFilter().filter(s.toString());
    }
};

However; if I enter the search phrase 'a thing', I expect the following results to be shown:

a thing
another thing
a different thing

In fact, all I get returned in the result 'a thing'. Since the filter is looking for the entire string, it treats the space as a part of the search term. Essentially, what I'm asking is how do I filter the list not by one specific string, but by multiple search terms, each separated by spaces? So long as each of the search terms appear at least once in the item, then that item should be returned in the result.

Similar questions seem to have been asked before on SO (for example, see the link at the top of this post, which concerns SimpleAdapters), but nowhere have I found an actual way to accomplish this. I suppose the answer would involve separating the search phrase into individual strings, delimited by the space character, and then running the search multiple times on each string, and only returning the results which match for all of the iterations...

like image 737
CaptainProg Avatar asked Nov 14 '12 00:11

CaptainProg


2 Answers

Unfortunately ArrayAdapter and other built-in adapters don't allow you to change their existing filters. There is no setFilter() method in the API... You must create a custom adapter that performs the same functions as ArrayAdapter.

To do this: simply cut & paste ArrayAdapter's source code into a new class in your project. Then find the Filter at the bottom. Inside performFiltering() we'll make a few changes. Find the if-else block marked with the following comment and replace that code block with this:

// First match against the whole, non-splitted value
if (valueText.startsWith(prefixString)) {
    newValues.add(value);
} else {
    // Break the prefix into "words"
    final String[] prefixes = prefixString.split(" ");
    final int prefixCount = prefixes.length;

    int loc;
    // Find the first "word" in prefix
    if(valueText.startsWith(prefixes[0]) || (loc = valueText.indexOf(' ' + prefixes[0])) > -1)
        loc = valueText.indexOf(prefixes[0]);

    // Find the following "words" in order
    for (int j = 1; j < prefixCount && loc > -1; j++) 
        loc = valueText.indexOf(' ' + prefixes[j], loc + 2);

    // If every "word" is in this row, add it to the results
    if(loc > -1) 
        newValues.add(value);
}

That's it. We only had to change the section of code above to meet your requirements, but we have to copy the entire adapter into a class since there is no other way to makes these changes.

(PS Since you've already created a new class, you might as well optimize getView() to suit you needs. The generic method is slower than it needs to be for a custom class.)

like image 134
Sam Avatar answered Oct 15 '22 11:10

Sam


I thought I would share my modified version of Sam's excellent answer. I just made 2 minor changes to support multi-line text in both the filter string and the text being filtered:

final String valueText = value.toString().toLowerCase().replace('\r', ' ').replace('\n', ' ');

// First match against the whole, non-splitted value
if (valueText.startsWith(prefixString)) {
    newValues.add(value);
} else {
    // Break the prefix into "words"
    final String[] prefixes = prefixString.split("\\s+");
    final int prefixCount = prefixes.length;

    int loc;
    // Find the first "word" in prefix
    if(valueText.startsWith(prefixes[0]) || (loc = valueText.indexOf(' ' + prefixes[0])) > -1)
        loc = valueText.indexOf(prefixes[0]);

    // Find the following "words" in order
    for (int j = 1; j < prefixCount && loc > -1; j++) 
        loc = valueText.indexOf(' ' + prefixes[j], loc + 2);

    // If every "word" is in this row, add it to the results
    if(loc > -1) 
        newValues.add(value);
}

I just replace \r and \n with spaces. You could also remove other white-space if needed, such as \t, or change to a regular expression... for me \r and \n was enough. I also changed the split() to \s+ so that it splits on any white-space.

like image 29
eselk Avatar answered Oct 15 '22 12:10

eselk