6

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...

Community
  • 1
  • 1
CaptainProg
  • 5,610
  • 23
  • 71
  • 116
  • Why do I get that horrible "what am I missing" feeling? Could you not extend the adapter, then override the getFilter() method, create your own filter, override performFiltering() and use a regex to return whatever collection, e.g. ArrayList, you need? – Simon Nov 14 '12 at 00:26
  • I'll leave my comment here in case someone makes the same mistake as me and misses the point that you don't want to extend the adapter. Doh! Sorry! – Simon Nov 14 '12 at 00:35
  • 1
    Hmm, this cannot be done without extending ArrayAdapter because there is no interface to change the filter. Why don't you want to write a custom adapter? – Sam Nov 14 '12 at 00:51
  • 1
    No no, I have no problem with extending the adapter! I didn't stipulate this in my question at all... Any possible solutions welcome :) – CaptainProg Nov 14 '12 at 21:45

5 Answers5

9

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.)

Sam
  • 86,580
  • 20
  • 181
  • 179
  • Works perfect for me and was very easy, thanks! I actually think the original source code must be a bug, because the part that does the word searching will never find a match if prefixString has a space -- who would ever want that weird filtering rule. It's like saying "multiple words only work if they are at the start", no user would ever understand that and would think it's a bug in our apps if we use that filtering as-is. – eselk Jan 11 '13 at 18:19
  • @Sam, thanks very much for this solution. I'm trying to adapt this code so that I can even enter search terms in the wrong order. For example, if I wanted to search for "a thing", I might enter "thing a". In the current state, this will not work. However, the code above contains a short section which tries to return the words 'in order'. I wonder if this 'order' could be broken, perhaps with each word stored in an array and some sort of array comparison... – CaptainProg Nov 03 '13 at 14:58
  • This will accomplish the goal, however, I do not recommend making changes to core library functions as that makes the code much less maintainable. Going forward, upgrading to new versions of android mean you will need to copy the new version and modify it again while maintaining compatability. I would compare this to making your own copy of jQuery and modifying it just because you need the array implementation to work a different way. Use at your own risk. – akousmata May 13 '15 at 15:12
5

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.

eselk
  • 6,764
  • 7
  • 60
  • 93
1

Sam has already given an excellent answer But there is one very short problem For example, if I wanted to search for "a thing", I might enter "thing a". This will not work. However, the code above contains a short section which tries to return the words 'in order'. And you need to make some changes in it to get the best solution.

 // 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"
        for (int j = 1; j < prefixCount && loc > -1; j++) 
            loc = valueText.indexOf(prefixes[j]);

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

I have removed loc + 2 from Sam's answer this line

for (int j = 1; j < prefixCount && loc > -1; j++)
        loc = valueText.indexOf(' ' + prefixes[j], loc + 2);
Vishal Singh
  • 1,341
  • 14
  • 13
0

Currently the only solution to achive what you want is overriding the ArrayAdapter and implement your own filtering requirements.

This requires a lot of code, as Filter in ArrayAdapter is using a lot of private objects, that you will need to replicate.

Fortunatly, someone else (@uʍop ǝpısdn) already went to the process, wrote the code and made it available for the community.

You can find the link to is blog here including how to use examples, and the code in here.

Regards.

Luis
  • 11,978
  • 3
  • 27
  • 35
0

[Complementing the sam's response] Your code will have problems when you digites only spaces. I added an "if".

// 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;
  // HERE CHANGE
  if (prefixCount>0) {
    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);
  }
}
chinglun
  • 637
  • 5
  • 18
TanMeza
  • 1
  • 1