17

I am trying to do a search such that all the "visible" search letters should be highlighted. I tried using spannable but that didn't do the trick, maybe I wasnt doing it right? based on this: Highlight searched text in ListView items How do i get to highlight the visible text? here's my filter :

private LayoutInflater mInflater;

        private ValueFilter valueFilter;

        public MySimpleArrayAdapter(Activity context) {

            this.context = context;
            mInflater = LayoutInflater.from(context);

        }
        private class ValueFilter extends Filter {


            //Invoked in a worker thread to filter the data according to the constraint.
            @Override
            protected synchronized FilterResults performFiltering(CharSequence constraint) {

                FilterResults results = new FilterResults();

                if (constraint != null && constraint.length() > 0) {

                    ArrayList<Integer> filterList = new ArrayList<>();

                    int iCnt = listItemsHolder.Names.size();
                    for (int i = 0; i < iCnt; i++) {
                        if(listItemsHolder.Types.get(i).toString().indexOf("HEADER_")>-1){
                            continue;
                        }
                        if (listItemsHolder.Names.get(i).matches(getRegEx(constraint))||(listItemsHolder.Names.get(i).toLowerCase().contains(constraint.toString().toLowerCase()))) {
                            if(filterList.contains(i))
                                continue;

                            filterList.add(i);

                        }
                        }

                    results.count = filterList.size();

                    results.values = filterList;
                }else {
                String prefixString = getRegEx(constraint);
                mSearchText = prefixString;
                    results.count = listItemsHolder.Names.size();

                    ArrayList<Integer> tList = new ArrayList<>();
                    for(int i=0;i<results.count;i++){
                        tList.add(i);
                    }

                    results.values = tList;

                }

                return results;


}


                //Invoked in the UI thread to publish the filtering results in the user interface.
                @SuppressWarnings("unchecked")
                @Override
                protected void publishResults(CharSequence constraint, FilterResults results) {
                    ArrayList<Integer> resultsList = (ArrayList<Integer>)results.values;
                    if(resultsList != null) {
                        m_filterList = resultsList;
                    }
                    notifyDataSetChanged();
                }

            }

            public String getRegEx(CharSequence elements){
                String result = "(?i).*";
                for(String element : elements.toString().split("\\s")){
                    result += element + ".*";
                }
                result += ".*";
                return result;
            }

Thanks in advance! 

Here's my getview

@Override
        public View getView(int position, View convertView, ViewGroup parent) {
            View rowView = convertView;
            ViewHolder holder;
            if(filtering && m_filterList != null && m_filterList.size() > position)
                position = m_filterList.get(position);

            if (rowView == null) {
                holder = new ViewHolder();

                mInflater = context.getLayoutInflater();
                rowView = mInflater.inflate(R.layout.rowlayout, null);
                // configure view holder
                holder.text = (TextView) rowView.findViewById(R.id.label);
                holder.text.setTextColor(Color.WHITE);
                holder.text.setSingleLine();
                holder.text.setTextSize(15);
                holder.text.setEllipsize(TextUtils.TruncateAt.END);
                holder.text.setPadding(2, 2, 6, 2);
                Typeface label = Typeface.createFromAsset(holder.text.getContext().getAssets(),
                        "fonts/arial-bold.ttf");
                holder.text.setTypeface(label);
                holder.image = (ImageView) rowView.findViewById(R.id.icon);
                holder.image.setPadding(6, 4, 0, 4);
                holder.image.getLayoutParams().height = (int) getResources().getDimension(R.dimen.icon_width_height);
                holder.image.getLayoutParams().width = (int) getResources().getDimension(R.dimen.icon_width_height);
                rowView.setBackgroundResource(R.drawable.row_border);
                rowView.setPadding(2, 2, 6, 2);
                rowView.setTag(holder);
            }else {

                // fill data
                holder = (ViewHolder) rowView.getTag();
            }

            String id  = listItemsHolder.getid(position);
            String name = listItemsHolder.getName(position);
            holder.image.setVisibility(View.VISIBLE);


            if (name != null) {
                holder.text.setText(listItemsHolder.getName(position));
                ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) holder.text.getLayoutParams();
                params.leftMargin = 20;
            }else{
                holder.text.setText(id);
            }
            String fullText = listItemsHolder.getName(position);
            // highlight search text
            if (mSearchText != null && !mSearchText.isEmpty()) {
                int startPos = fullText.toLowerCase(Locale.US).indexOf(mSearchText.toLowerCase(Locale.US));
                int endPos = startPos + mSearchText.length();
                if (startPos != -1) {
                    Spannable spannable = new SpannableString(fullText);
                    ColorStateList blueColor = new ColorStateList(new int[][]{new int[]{}}, new int[]{Color.BLUE});
                    TextAppearanceSpan highlightSpan = new TextAppearanceSpan(null, Typeface.BOLD, -1, blueColor, null);
                    spannable.setSpan(highlightSpan, startPos, endPos, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
                    holder.text.setText(spannable);
                } else {
                    holder.text.setText(fullText);
                }
            } else {
                holder.text.setText(fullText);
            }
            return rowView;
        }
Community
  • 1
  • 1

6 Answers6

24

Let's assume you have create a custom adapter, then you can refer to the following code:

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        View view;
        TextView text;

        if (convertView == null) {
            view = mInflater.inflate(mResource, parent, false);
        } else {
            view = convertView;
        }

        try {
            if (mFieldId == 0) {
                //  If no custom field is assigned, assume the whole resource is a TextView
                text = (TextView) view;
            } else {
                //  Otherwise, find the TextView field within the layout
                text = (TextView) view.findViewById(mFieldId);
            }
        } catch (ClassCastException e) {
            Log.e("ArrayAdapter", "You must supply a resource ID for a TextView");
            throw new IllegalStateException(
                    "ArrayAdapter requires the resource ID to be a TextView", e);
        }
        String item = getItem(position);
        text.setText(item);

        String fullText = getItem(position);
        // highlight search text
        if (mSearchText != null && !mSearchText.isEmpty()) {
            int startPos = fullText.toLowerCase(Locale.US).indexOf(mSearchText.toLowerCase(Locale.US));
            int endPos = startPos + mSearchText.length();

            if (startPos != -1) {
                Spannable spannable = new SpannableString(fullText);
                ColorStateList blueColor = new ColorStateList(new int[][]{new int[]{}}, new int[]{Color.BLUE});
                TextAppearanceSpan highlightSpan = new TextAppearanceSpan(null, Typeface.BOLD, -1, blueColor, null);
                spannable.setSpan(highlightSpan, startPos, endPos, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
                text.setText(spannable);
            } else {
                text.setText(fullText);
            }
        } else {
            text.setText(fullText);
        }

        return view;
    }

The mSearchText will be updated at the following inside performFiltering of ArrayFilter class.

String prefixString = prefix.toString().toLowerCase();
mSearchText = prefixString;

You can find more details in my sample code here or my GitHub (with lastest update).

Here is the screenshot

enter image description here

BNK
  • 23,994
  • 8
  • 77
  • 87
  • thanks, this is one of the sample codes similar to my code, can you explain with respect to this, when i used your code it turned all of my text blue instead of just the filtered characters . http://www.androidhive.info/2012/09/android-adding-search-functionality-to-listview/ –  Nov 07 '15 at 16:22
  • 1
    thanks for the detailed answer.I will try out the highlight logic and let you know. For now, thanks for taking the extra step, just for that, the bounty is yours.if it works for me, i will tick off this answer as the correct one. –  Nov 08 '15 at 13:45
  • You're welcome, if it does not work for your app, pls send me your code so that I can check :) – BNK Nov 08 '15 at 14:09
  • I am not able to get it to work with my code. I added the getview and made changes as per your code, but it still doesn't work, can you check my getview I updated in my question? what am i missing? –  Nov 09 '15 at 02:38
  • Where did you update `mSearchText `'s value? Try debugging to see variables' values, for example, `startPos`, `endPos`, `spannable` – BNK Nov 09 '15 at 02:50
  • Please add `mSearchText = constraint.toString().toLowerCase()` inside `if (constraint != null && constraint.length() > 0) {` I think your app go into that IF :) – BNK Nov 09 '15 at 02:52
  • that works! thanks, but currently ,even in your test app, I see that when I clear the character sequence I am searching with, the matching characters remain highlighted. say I am searching with letters "ab" and some strings in the list match that ,then when i clear the search, the letters remain colored. is there a way to clear off the colors when your search box is empty? –  Nov 09 '15 at 03:17
  • 1
    Yes, I forgot to reset `mSearchText` variable, in my code, `mSearchText = "";` should be inside `if (prefix == null || prefix.length() == 0) {`. I have just posted my sample project to https://github.com/ngocchung/filteredlistview – BNK Nov 09 '15 at 03:19
  • 1
    Glad it could help, happy coding :) – BNK Nov 09 '15 at 03:29
  • do you happen to know how I can also apply my getRegEx(CharSequence elements) function to the highlight view and not just tolowercase()? is there any way i can get it to highlight letters searched even after a space in between or so based on the regex function –  Nov 11 '15 at 00:53
  • if you mean `sp` in searchbox, listview display `Dell Inspiron` for example, then inside `performFiltering` of `ArrayFilter` class: use `if (valueText.startsWith(prefixString) || valueText.contains(prefixString))` and `if (word.startsWith(prefixString) || word.contains(prefixString))` – BNK Nov 11 '15 at 01:11
  • no i meant String prefixString = constraint.toString().toLowerCase(); , this just highlights lowercase and dies off after space, is there a way i can use my current regex function to highlight the matching characters along with constraint.toString().toLowerCase() –  Nov 11 '15 at 01:17
  • Do you mean like the following screenshot https://drive.google.com/file/d/0B2HGUM4c0YwpRjJhX1hfMkNnelk/view?usp=sharing? – BNK Nov 11 '15 at 01:20
  • .toLowerCase() just for correct comparation, Uppercase letters highlighted too – BNK Nov 11 '15 at 01:21
  • sort of , more like search for "k 8" and it will highlight k and 8 only from that screenshot for "nokia 8800" ,right now it highlight only if the words are searched in continuity, like for nokia 8800 it will highlight kia 8 as they are searched for in continuity. even if i search for any letter from the first word and any letter from second etc, it should highlight them ,for which i m using my regex function(for search), just wanna know how i can apply that for highlights aswell –  Nov 11 '15 at 01:23
  • Ah, I have not tried it yet, ok, I will try today or tomorrow when have more freetime :) – BNK Nov 11 '15 at 01:26
  • no problem, whenever u can, if you can use my regex function and similar logic, it will be awesome, thanks! –  Nov 11 '15 at 01:28
  • 1
    Sorry I don't find a solution with your getRegex function, my lastest try the new adapter, you can see https://github.com/ngocchung/FilteredListView/blob/master/app/src/main/java/com/example/filteredlistview/HighlightArrayAdapter2.java, then if you type `k 8 4` the listview will show and highlight all items having at least one letter. Hope this helps! – BNK Nov 11 '15 at 01:53
  • 1
    thanks, i am able to see the issues, will try to modify further as i go along ,thanks for the help though, appreciate it! –  Nov 11 '15 at 02:08
  • 1
    @BNK great Example. – Kuldeep Singh Feb 15 '19 at 11:39
6

In your filter method, store the string used to perform the filter:

// Filter Class
public void filter(String searchString) {
    this.searchString = searchString;
    ...
    // Filtering stuff as normal.
}

You must declare a member string to store it:

public class ListViewAdapter extends BaseAdapter {
    ...    
    String searchString = "";
    ...

And, in getView you highlight the search term:

public View getView(final int position, View view, ViewGroup parent) {
    ...
    // Set the results into TextViews
    WorldPopulation item = worldpopulationlist.get(position);
    holder.rank.setText(item.getRank());
    holder.country.setText(item.getCountry());
    holder.population.setText(item.getPopulation());

    // Find charText in wp
    String country = item.getCountry().toLowerCase(Locale.getDefault());
    if (country.contains(searchString)) {
        Log.e("test", country + " contains: " + searchString);
        int startPos = country.indexOf(searchString);
        int endPos = startPos + searchString.length();

        Spannable spanText = Spannable.Factory.getInstance().newSpannable(holder.country.getText()); // <- EDITED: Use the original string, as `country` has been converted to lowercase.
        spanText.setSpan(new ForegroundColorSpan(Color.RED), startPos, endPos, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);

        holder.country.setText(spanText, TextView.BufferType.SPANNABLE);
    }
    ...
}

Hope it helps.

Vishavjeet Singh
  • 1,385
  • 11
  • 13
4

Hi on your adapter class ,make a spanneble text and set it to your textview, the below code you can use for reference.

 if ("text contains filter value".toLowerCase().contains("filter".toLowerCase())) {
        Spannable spanText = Spannable.Factory.getInstance().newSpannable("text contains filter value".toLowerCase());

        Matcher matcher = Pattern.compile("filter".toLowerCase())
                .matcher("text contains filter value".toLowerCase());
        while (matcher.find()) {
            spanText.setSpan(new ForegroundColorSpan(Color.RED), matcher.start(),
                    matcher.start() + "filter".length(),
                    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        }
       yourTextView.setText(spanText);
    }
Arun Antoney
  • 4,292
  • 2
  • 20
  • 26
1

This is only demo for highlight text, you can implement your self by calling highlight(searchText, originalText) in filter,

public class MainActivity extends AppCompatActivity {
EditText editText;
TextView text;

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    editText = (EditText) findViewById(R.id.editText);
    text = (TextView) findViewById(R.id.textView1);

    editText.addTextChangedListener(new TextWatcher() {
        @Override
        public void beforeTextChanged(CharSequence s, int start, int count, int after) {

        }

        @Override
        public void onTextChanged(CharSequence s, int start, int before, int count) {
                text.setText(highlight(editText.getText().toString(), text.getText().toString()));
        }

        @Override
        public void afterTextChanged(Editable s) {

        }
    });

}

public static CharSequence highlight(String search, String originalText) {
    String normalizedText = Normalizer.normalize(originalText, Normalizer.Form.NFD).replaceAll("\\p{InCombiningDiacriticalMarks}+", "").toLowerCase();
    int start = normalizedText.indexOf(search);
    if (start <= 0) {
        return originalText;
    } else {
        Spannable highlighted = new SpannableString(originalText);
        while (start > 0) {
            int spanStart = Math.min(start, originalText.length());
            int spanEnd = Math.min(start + search.length(), originalText.length());
            highlighted.setSpan(new BackgroundColorSpan(Color.YELLOW), spanStart, spanEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
            start = normalizedText.indexOf(search, spanEnd);
        }
        return highlighted;
    }
 }
}
Rajesh
  • 2,618
  • 19
  • 25
0

Put this code before setting text in getview

Spannable wordtoSpan = new SpannableString("Your_text_in_getviews");

        wordtoSpan.setSpan(new ForegroundColorSpan(Color.RED), 0, edtFilter
                .getText().toString().length(),
                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
txt_contact.setText(wordtoSpan);
0

It can be done in a bit simpler way:

  1. Define custom adapter:
class HighlightAutoCompleteAdapter(context: Context, resource: Int, private val textResId: Int, items: List<String>) :
    ArrayAdapter<String>(context, resource, textResId, items) {

    private val inflater = LayoutInflater.from(context)
    var queryText = ""

    override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
        val view = convertView ?: inflater.inflate(textResId, parent, false)
        val textView: TextView = view.findViewById(android.R.id.text1) as TextView
        val fullText = getItem(position) as String
        // highlight search text
        val highlight: Spannable = SpannableString(fullText)
        if (queryText.isNotEmpty()) {
            val startPos: Int = fullText.toLowerCase(Locale.US).indexOf(queryText.toLowerCase(Locale.US))
            val endPos: Int = startPos + queryText.length
            if (startPos != -1) {
                highlight.setSpan(StyleSpan(BOLD),
                                    startPos,
                                    endPos,
                                    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
            }
        }
        textView.text = highlight
        return view
    }
}
  1. Create the adapter and listen to text changes to keep the adapter updated:
        val searchEditText: AutoCompleteTextView = view.findViewById(R.id.search_edit_text)
        val arrayAdapter = HighlightAutoCompleteAdapter(requireContext(), 0, R.layout.search_complete_item, autoCompletionList)
        searchEditText.setAdapter(arrayAdapter)
        searchEditText.addTextChangedListener(object : TextWatcher {
            override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
            override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
                arrayAdapter.queryText = s?.toString() ?: ""
            }
            override fun afterTextChanged(s: Editable?) {}
        })
yshahak
  • 4,996
  • 1
  • 31
  • 37