2

I have a Spannable set up on a SearchView for RecyclerView CardViews. If user-entered text is found on the CardView, the text is highlighted in red. When the SearchView line is cleared I would like the CardView text to return to the default Black color. Currently, the text is cleared and the color erroneously remains red. I tried to use "removeSpan" on the TextView but no luck. The TextView is "cardBlankText2". What am I missing here?

RecyclerView Adapter file

private List<ListItem> mListItems;
...    

@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {

    View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_contact_item, parent,false);

    final ItemHolder itemHolder = new ItemHolder(view);    

    return itemHolder;
}

private static class ItemHolder extends RecyclerView.ViewHolder {

    private ForegroundColorSpan foregroundColorSpan = new ForegroundColorSpan(Color.RED);

    private TextView cardBlankText2; 

    private ItemHolder(View itemView) {
        super(itemView);

        cardBlankText2 = (TextView) itemView.findViewById(R.id.cardBlankText2);
}

    public ForegroundColorSpan getForegroundColorSpan(){
        return foregroundColorSpan;
    }
} 


public void setFilter(List<ListItem> listItems, String searchString) {
    // Note: the String is to get s.toString() from the Main Activity SearchView.
    // Note the plural for listItems.
    mListItems = new ArrayList<>();
    mListItems.addAll(listItems);
    this.searchString = searchString;
    notifyDataSetChanged();
}

public void onBindViewHolder(final RecyclerView.ViewHolder holder, int position) {

    final ListItem listItem = mListItems.get(position);


    String todoHighlight = listItem.getTodo().toLowerCase(Locale.getDefault());
    String note1Highlight = listItem.getNote1().toLowerCase(Locale.getDefault());

    // Set up the logic for the highlighted text
    int todoStartPos = todoHighlight.indexOf(searchString);
    int todoEndPos = todoStartPos + searchString.length();
    int note1StartPos = note1Highlight.indexOf(searchString);
    int note1EndPos = note1StartPos + searchString.length();
    Spannable spanString2 = Spannable.Factory.getInstance().newSpannable(
            itemHolder.cardBlankText2.getText());        

    if (todoStartPos != -1 && searchString.length() > 0 && todoHighlight.contains(searchString)) {
        spanString2.setSpan(new ForegroundColorSpan(Color.RED), todoStartPos, todoEndPos,
            Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        itemHolder.cardBlankText2.setText(spanString2);
    }
    **else if (searchString.length() == 0) {
        spanString2.removeSpan(itemHolder.cardBlankText2.getText());
        itemHolder.cardBlankText2.setText(spanString2);
    }**
}


MainActivity

public class MainActivity extends AppCompatActivity {

...
@Override
public boolean onCreateOptionsMenu(Menu menu) {

    getMenuInflater().inflate(R.menu.cardview_menu, menu);

    final MenuItem searchItem = menu.findItem(R.id.action_search);

    searchItem.setVisible(true);

    SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE);

    final SearchView mSearchView = (SearchView) MenuItemCompat.getActionView(searchItem);mSearchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName()));
    final EditText mSearchEditText = (EditText) mSearchView.findViewById(android.support.v7.appcompat.R.id.search_src_text);

        mSearchEditText.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) {                    
            }

            @Override
            public void afterTextChanged(Editable s) {


                final ArrayList<ListItem> filteredModelList2 = filter(allList, s.toString());

                if (!mSearchView.isIconified() && filteredModelList2.size() == 0) {

                        // re-load the list so the Adapter refreshes the RecyclerView list View.
                        adapter.clear();
                        adapter.addAll(allList);
                } else if (!mSearchView.isIconified() && filteredModelList2.size() > 0) {                            
                        adapter.setFilter(filteredModelList2, s.toString());
                        mRecyclerView.scrollToPosition(0);
                }
        });
        return super.onCreateOptionsMenu(menu);
    }  

// Do Search filtering from MainActivity's OnQueryTextChange(String newText)
// The Spannable code is in the onBindVH section for the itemHolders.
public ArrayList<ListItem> filter(List<ListItem> listItems, String query) {

    query = query.toLowerCase();

    final ArrayList<ListItem> filteredModelList = new ArrayList<>();
    for (ListItem listItem : listItems) {
        final String text = listItem.getTodo().toLowerCase();
        final String text2 = listItem.getNote1().toLowerCase();
        final String text3 = listItem.getNote2().toLowerCase();
        if (text.contains(query) || text2.contains(query) ||
            text3.contains(query)) {
            filteredModelList.add(listItem);
        }
    }
    return filteredModelList;
}
AJW
  • 1,578
  • 3
  • 36
  • 77

1 Answers1

2

The method removeSpan() is meant to be the reverse operation for setSpan(), so you should pass both of them the same Object. To achieve this, make the ForegroundColorSpan a field of the ViewHolder (see below)

public void onBindViewHolder(final MyViewHolder itemHolder, int position) {
    String todoHighlight = mListItems.get(position).getTodo();
    itemHolder.cardBlankText2.setText(todoHighlight);

    // Set up the logic for the highlighted text
    int todoStartPos = todoHighlight.indexOf(searchString);
    int todoEndPos = todoStartPos + searchString.length();

    // ... skipped some lines ...

    Spannable spanString2 = Spannable.Factory.getInstance().newSpannable(
        itemHolder.cardBlankText2.getText());        

    if (todoStartPos != -1 && searchString.length() > 0 && todoHighlight.contains(searchString)) {
        spanString2.setSpan(itemHolder.getForegroundColorSpan(), todoStartPos, todoEndPos,
            Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        itemHolder.cardBlankText2.setText(spanString2);
    }
    else if (searchString.length() == 0) {
            spanString2.removeSpan(itemHolder.getForegroundColorSpan());
            itemHolder.cardBlankText2.setText(spanString2);
    }
}

The ViewHolder:

class MyViewHolder extends RecyclerView.ViewHolder{
    private ForegroundColorSpan foregroundColorSpan = new ForegroundColorSpan(Color.RED);

    public MyViewHolder(View itemView){
        super(itemView);
    }

    public ForegroundColorSpan getForegroundColorSpan(){
        return foregroundColorSpan;
    }
}

EDIT Remote debugging is difficult, so let's do this the other way round, MCVE-style: please take the following code, run it and if it works (like "highlighting changes according to search text"), then you're one step further along the way.

public class Activity8_RecyclerViewGrid extends AppCompatActivity
{
    private int counter = 0;
    private static String[] searchTexts = new String[]{"rem", "ia", "", "ol" };

    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity8__recycler_view);

        RecyclerView rcv = (RecyclerView)findViewById(R.id.recyclerView);
        rcv.setLayoutManager(new GridLayoutManager(this, 2));
        final MyAdapter adapter = new MyAdapter(allMyWords());
        rcv.setAdapter(adapter);

        final TextView tvSearchText = (TextView)findViewById(R.id.tvSearchText);

        Button btnFilter = (Button)findViewById(R.id.btnFilter);
        btnFilter.setOnClickListener(new View.OnClickListener()
        {
            @Override
            public void onClick(View view)
            {
                String searchString = searchTexts[counter%4];
                adapter.setFilter(Activity7_GridViewStuff.allMyBooks(), searchString);
                tvSearchText.setText(searchString);
                counter++;
            }
        });
    }


    public static ArrayList<String> allMyWords()
    {
        String[] theLoremArray = null;
        String LOREM_STRING = "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.";
        String[] sTemp = LOREM_STRING.split(" ");
        StringBuilder sb = new StringBuilder();

        theLoremArray = new String[sTemp.length/3];
        for (int i = 0; i < (sTemp.length - sTemp.length%3); i++)
        {
        sb.append(sTemp[i]).append(" ");
        if (i%3 == 2)
        {
            theLoremArray[i/3] = sb.toString();
            //  Log.d(TAG, "mLoremArray [" + i / 3 + "] = " + sb.toString());
            sb.delete(0, sb.length());
        }
    }

    ArrayList<String> words = new ArrayList<>();

    for (int i = 0; i < theLoremArray.length; i++)
        {
            words.add( theLoremArray[i]);
        }
        return words;
    }

    class MyAdapter extends RecyclerView.Adapter<MyHolder>{

        private List<String> data;
        private String searchString = "";

        MyAdapter(List<String> data){
            this.data = data;
        }

        public void setFilter(List<String> listItems, String searchString){
          //  data = new ArrayList<>();
          //  data.addAll(listItems);
            this.searchString = searchString;
            notifyDataSetChanged();
        }

        @Override
        public MyHolder onCreateViewHolder(ViewGroup parent, int viewType)
        {
            View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.activity8_grid_cell, null);
            return new MyHolder(v);
        }

        @Override
        public void onBindViewHolder(final MyHolder holder, int position)
        {
            holder.text.setText(data.get(position));
            String todoHighlight = data.get(position);

            // Set up the logic for the highlighted text
            int todoStartPos = todoHighlight.indexOf(searchString);
            int todoEndPos = todoStartPos + searchString.length();

            Spannable spanString2 = Spannable.Factory.getInstance().newSpannable(holder.text.getText());

            if(todoStartPos != -1 && searchString.length() > 0 && todoHighlight.contains(searchString)){
                spanString2.setSpan(holder.getForegroundColorSpan(),    todoStartPos, todoEndPos, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
            }
            else if (searchString.length() == 0){
                spanString2.removeSpan(holder.getForegroundColorSpan());
            }

            holder.text.setText(spanString2);
        }

        @Override
        public int getItemCount()
        {
        return data.size();
        }
    }

    class MyHolder extends RecyclerView.ViewHolder{

        private ForegroundColorSpan foregroundColorSpan = new   ForegroundColorSpan(Color.CYAN);
        TextView text;

        public MyHolder(View itemView){
            super(itemView);
            text = (TextView) itemView.findViewById(R.id.lorem_text);
        }

        public ForegroundColorSpan getForegroundColorSpan(){
            return foregroundColorSpan;
        }
    }
}

activity8_recycler_view.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.example.samples.Activity8_RecyclerViewGrid">

    <TextView
        android:id="@+id/tvSearchText"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="16dp"
        android:textAppearance="@style/Base.TextAppearance.AppCompat.Title"
        android:textColor="#ff0000" />

    <Button android:id="@+id/btnFilter"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        android:text="SET NEW FILTER"
        android:layout_gravity="center_horizontal" />

    <android.support.v7.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_marginTop="80dp">

    </android.support.v7.widget.RecyclerView>
</FrameLayout>

activity8_grid_cell.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
          android:layout_width="match_parent"
          android:layout_height="match_parent">
    <TextView
        android:id="@+id/lorem_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginEnd="16dp"
        android:textAppearance="?android:attr/textAppearanceLarge" />
</FrameLayout>
Bö macht Blau
  • 12,820
  • 5
  • 40
  • 61
  • I tried your above answer with no luck. I also replaced "searchString.length() == 0" with "itemHolder.cardBlankText2.length() == 0" and that did not work either. Any ideas? – AJW Aug 28 '17 at 05:05
  • @AJW - sorry about that. I tested with a standalone TextView (works) and with a RecyclerView. This works as well, but my simple setup does not contain SearchViews, and I don't know what triggers `onBindViewHolder()`in your app. If you post more code I'll try to find out why it does not work for you. – Bö macht Blau Aug 28 '17 at 19:01
  • I added Viewholder and Itemholder sections of the Adapter and also added the SearchView sections of the MainActivity. Let me know what you think. – AJW Aug 29 '17 at 01:24
  • @AJW - it's still working after using setFilter() like you do. One thing I'm doing differently though is I write something like `itemHolder.cardBlankText2.setText(listItem.getTodo())` before calculating *todoStartPos* and *todoEndPos*, simply because I couldn't imagine how highlighting an empty String could work (and my TextView does not contain any text initially, so for my sample app *spanString2* would be empty) – Bö macht Blau Aug 29 '17 at 18:01
  • I will give it a try. – AJW Aug 30 '17 at 00:30
  • So I setText on the listItem.getTodo() before calculating todos... No luck...not sure why. Back to the drawing board... – AJW Aug 30 '17 at 00:49