This question already has an answer here:
How to hightlight the typed text in searchview in android
How can we have searched characters colored when we use searchview in recyclerview?
(1 answer)
Closed 2 years ago.
How to highlight search text result in RecyclerView. I found some posts regarding Spannable TextView, but not sure where to implement in my case. Appreciate you can look and assist.
Can any one help it is not highlight the search text
public void onSearch(String query){
Log.d("Query From Activity",query);
ArrayList<TaskModel> filteredList = new ArrayList<>();
//resultView.setVisibility(View.GONE);
for (int i = 0; i < listTask.size(); i++) {
final String jTitle = listTask.get(i).getAssigned().toLowerCase();
// final String jCompany = listTask.get(i).getJob_company().toLowerCase();
// final String jLoc = mDataList.get(i).getJob_location().toLowerCase();
if (jTitle.contains(query) ) {
// make them also bold
filteredList.add(listTask.get(i));
highlightText(query,jTitle);
// resultView.setVisibility(View.GONE);
// listTask.get(i).setText(sb);
} else {
// resultView.setText("No results found for : " + query);
// resultView.setVisibility(View.VISIBLE);
}
}
final LinearLayoutManager mLayoutManager = new LinearLayoutManager(getActivity());
recyclerViewTask.setLayoutManager(mLayoutManager);
taskAdapter = new TaskAdapter(getContext(),filteredList,this,this);
recyclerViewTask.setAdapter(taskAdapter);
taskAdapter.notifyDataSetChanged();
}
public static CharSequence highlightText(String search, String originalText) {
if (search != null && !search.equalsIgnoreCase("")) {
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 ForegroundColorSpan(Color.BLUE), spanStart, spanEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
start = normalizedText.indexOf(search, spanEnd);
}
return highlighted;
}
}
return originalText;
}
The Adapter code below for the RecyclerView successfully highlights user entered text (using an EditText line linked to a SearchView) to the color Green.
public void onBindViewHolder(final RecyclerView.ViewHolder holder, int position) {
String cardNumsHighlight = String.valueOf(card.getId());
String todoHighlight = card.getTodo().toLowerCase(Locale.getDefault());
String note1Highlight = card.getNote1().toLowerCase(Locale.getDefault());
// Set up the code for the highlighted text of successful search results
int cardNumsStartPos = cardNumsHighlight.indexOf(searchString);
int cardNumsEndPos = cardNumsStartPos + searchString.length();
int todoStartPos = todoHighlight.indexOf(searchString);
int todoEndPos = todoStartPos + searchString.length();
int note1StartPos = note1Highlight.indexOf(searchString);
int note1EndPos = note1StartPos + searchString.length();
Spannable spanString1 = Spannable.Factory.getInstance().newSpannable(
itemHolder.cardBlankTextNumstotal.getText());
Spannable spanString2 = Spannable.Factory.getInstance().newSpannable(
itemHolder.cardBlankText2.getText());
Spannable spanString4 = Spannable.Factory.getInstance().newSpannable(
itemHolder.cardBlankText4.getText());
if (cardNumsStartPos != -1 && searchString.length() > 0 && cardNumsHighlight.contains(searchString)) {
spanString1.setSpan(new ForegroundColorSpan(Color.GREEN), cardNumsStartPos, cardNumsEndPos,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
itemHolder.cardBlankTextNumstotal.setText(spanString1);
}
if (todoStartPos != -1 && searchString.length() > 0 && todoHighlight.contains(searchString)) {
spanString2.setSpan(new ForegroundColorSpan(Color.GREEN), todoStartPos, todoEndPos,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
itemHolder.cardBlankText2.setText(spanString2);
}
if (note1StartPos != -1 && searchString.length() > 0 && note1Highlight.contains(searchString)) {
spanString4.setSpan(new ForegroundColorSpan(Color.GREEN), note1StartPos, note1EndPos,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
itemHolder.cardBlankText4.setText(spanString4);
}
I tried the following code addition using removeSpan() in the Answer here: Android Spannable: how to clear color? to clear the color when the query does not return any items, but with no luck. What am I missing here?
Activity
...
#Override
public void afterTextChanged(Editable s) {
if (s.toString().length() >0) {
String queryText = "%" + s.toString() + "%";
mQuickcardViewModel.searchQuery(queryText).observe(MainActivity.this, searchQuickcards -> {
searchList = searchQuickcards;
if (!mSearchView.isIconified() && searchList.size() == 0) {
mQuickcardViewModel.loadFullList();
}
...
ViewModel
...
public LiveData<List<Quickcard>> getAllCards() {
if (quickcards == null) {
quickcards = new MutableLiveData<>();
loadFullList();
}
return quickcards;
}
public void loadFullList(){
quickcards.postValue(repository.getAllCards());
}
// Repository gets data from Dao, which then gets data from Room database.
I have an activity where credit/debit card details is added. Currently my code allows to add date and month in MM/YY format. But i need year in 4 digits format MM/YYYY. Adding the '/' divider is set after 2nd digit. So if i increase my total digits count, i get the divider after every 2nd digit. How to fix this?
Carddetails.java:
public class CardDetailsActivity extends AppCompatActivity implements TextWatcher {
private static final int CARD_NUMBER_TOTAL_SYMBOLS = 19; // size of pattern 0000-0000-0000-0000
private static final int CARD_NUMBER_TOTAL_DIGITS = 16; // max numbers of digits in pattern: 0000 x walknew2
private static final int CARD_NUMBER_DIVIDER_MODULO = 5; // means divider position is every 5th symbol beginning with walknew4
private static final int CARD_NUMBER_DIVIDER_POSITION = CARD_NUMBER_DIVIDER_MODULO - 1; // means divider position is every 4th symbol beginning with 0
private static final char CARD_NUMBER_DIVIDER = '-';
#BindView(R.id.cardno)
EditText cardno;
#BindView(R.id.cardDateEditText)
EditText cardDateEditText;
#BindView(R.id.cardCVCEditText)
EditText cardCVCEditText;
SharedPreferences pref;
SharedPreferences.Editor editor;
private static final int CARD_DATE_TOTAL_SYMBOLS = 7; // size of pattern MM/YY
private static final int CARD_DATE_TOTAL_DIGITS = 6; // max numbers of digits in pattern: MM + YY
private static final int CARD_DATE_DIVIDER_MODULO = 3; // means divider position is every 3rd symbol beginning with walknew4
private static final int CARD_DATE_DIVIDER_POSITION = CARD_DATE_DIVIDER_MODULO - 1; // means divider position is every 2nd symbol beginning with 0
private static final char CARD_DATE_DIVIDER = '/';
private static final int CARD_CVC_TOTAL_SYMBOLS = 3;
String card_date, card_year, card_month, card_cvv, card_num, user_id, card_type = "debitcard";
#BindView(R.id.btn_add_card)
Button btn_add_card;
#BindView(R.id.btn_back)
Button btn_back;
Context context;
ProgressDialog progressDialog;
ApiService apiService;
String driverid, id, usertype, c_id, booking_id;
EditText card_numm, card_yearm, card_cvvm;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_card_details);
ButterKnife.bind(this);
context = CardDetailsActivity.this;
Log.e("TAG", "Inside Card Details Activity:");
apiService = RetrofitSingleton.getApiService();
usertype = PrefConnect.readString(CardDetailsActivity.this, PrefConnect.USERTYPE, "");
pref = getApplicationContext().getSharedPreferences("MyPref", 0);
editor = pref.edit();
id = pref.getString("driver_id", "");
Log.e("driver_id", id);
try {
if (getIntent() != null) {
driverid = getIntent().getStringExtra("Driverid");
booking_id = getIntent().getStringExtra("bookingid");
}
} catch (Exception e) {
e.printStackTrace();
Log.e("paymentact", e.getMessage());
}
cardno.getText().toString().trim();
getWindow().setSoftInputMode(
WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN);
cardno.addTextChangedListener(this);
cardDateEditText.addTextChangedListener(this);
card_numm = (EditText) findViewById(R.id.cardno);
card_yearm = (EditText) findViewById(R.id.cardDateEditText);
card_cvvm = (EditText) findViewById(R.id.cardCVCEditText);
}
#OnClick({R.id.btn_add_card, R.id.btn_back})
public void OnClick(View view) {
switch (view.getId()) {
case R.id.btn_back:
finish();
break;
case R.id.btn_add_card:
View view1 = getCurrentFocus();
Log.e("TAG", "Inside Card Details Activity Button Add Card ");
Validation();
break;
}
}
private boolean isInputCorrect(Editable s, int size, int dividerPosition, char divider) {
boolean isCorrect = s.length() <= size;
for (int i = 0; i < s.length(); i++) {
if (i > 0 && (i + 1) % dividerPosition == 0) {
isCorrect &= divider == s.charAt(i);
} else {
isCorrect &= Character.isDigit(s.charAt(i));
}
}
return isCorrect;
}
private String concatString(char[] digits, int dividerPosition, char divider) {
final StringBuilder formatted = new StringBuilder();
for (int i = 0; i < digits.length; i++) {
if (digits[i] != 0) {
formatted.append(digits[i]);
if ((i > 0) && (i < (digits.length - 1)) && (((i + 1) % dividerPosition) == 0)) {
formatted.append(divider);
}
}
}
return formatted.toString();
}
private char[] getDigitArray(final Editable s, final int size) {
char[] digits = new char[size];
int index = 0;
for (int i = 0; i < s.length() && index < size; i++) {
char current = s.charAt(i);
if (Character.isDigit(current)) {
digits[index] = current;
index++;
}
}
return digits;
}
#Override
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
}
#Override
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
}
#Override
public void afterTextChanged(Editable s) {
card_num = String.valueOf(s);
Log.i("TAG", "Card Date:" + s);
Log.i("TAG", "Card Date :" + cardDateEditText.getEditableText());
if (s == cardno.getEditableText()) {
if (!isInputCorrect(s, CARD_NUMBER_TOTAL_SYMBOLS, CARD_NUMBER_DIVIDER_MODULO, CARD_NUMBER_DIVIDER)) {
s.replace(0, s.length(), concatString(getDigitArray(s, CARD_NUMBER_TOTAL_DIGITS), CARD_NUMBER_DIVIDER_POSITION, CARD_NUMBER_DIVIDER));
}
// DO STH
} else if (s == cardDateEditText.getEditableText()) {
if (!isInputCorrect(s, CARD_DATE_TOTAL_SYMBOLS, CARD_DATE_DIVIDER_MODULO, CARD_DATE_DIVIDER)) {
s.replace(0, s.length(), concatString(getDigitArray(s, CARD_DATE_TOTAL_DIGITS), CARD_DATE_DIVIDER_POSITION, CARD_DATE_DIVIDER));
} else if (s == cardCVCEditText.getEditableText()) {
if (s.length() > CARD_CVC_TOTAL_SYMBOLS) {
s.delete(CARD_CVC_TOTAL_SYMBOLS, s.length());
}
}
}
}
}
As by my understanding, your date divider must always be at position 2
private static final int CARD_DATE_DIVIDER_POSITION = CARD_DATE_DIVIDER_MODULO - 1; // means divider position is every 2nd symbol beginning with 0
with the above declaration, before appending the date divider, I should test this way ( you can change your isInputCorrect method this way):
private boolean isInputCorrect(Editable s, int size, int dividerPosition, char divider) {
boolean isCorrect = s.length() <= size;
for (int i = 0; i < s.length(); i++) {
if (i > 0 && (i + 1) % dividerPosition == 0) {
if (divider=='/') {
if (i==2) {
isCorrect &= divider == s.charAt(i);
}
}
} else {
isCorrect &= Character.isDigit(s.charAt(i));
}
}
return isCorrect;
}
And then after the text has changed can be modified a bit to become:
#Override
public void afterTextChanged(Editable s) {
card_num = String.valueOf(s);
String ss=cardDateEditText.getText().toString();
Log.i("TAG", "Card Date:" + s);
Log.i("TAG", "Card Date :" + ss);
if (s == cardno.getEditableText()) {
if (!isInputCorrect(s, CARD_NUMBER_TOTAL_SYMBOLS, CARD_NUMBER_DIVIDER_MODULO, CARD_NUMBER_DIVIDER)) {
s.replace(0, s.length(), concatString(getDigitArray(s, CARD_NUMBER_TOTAL_DIGITS), CARD_NUMBER_DIVIDER_POSITION, CARD_NUMBER_DIVIDER));
}
} else if (s == cardDateEditText.getEditableText()) {
if (s.length()<=CARD_DATE_TOTAL_SYMBOLS)
{
if (!isInputCorrect(s, CARD_DATE_TOTAL_SYMBOLS, CARD_DATE_DIVIDER_MODULO, CARD_DATE_DIVIDER)) {
Log.e("TAG",s+"");
s.replace(0, s.length(), concatString(getDigitArray(s, CARD_DATE_TOTAL_DIGITS), CARD_DATE_DIVIDER_POSITION, CARD_DATE_DIVIDER));
}
}else
{
s.delete(CARD_DATE_TOTAL_SYMBOLS,s.length());
}
} else if (s == cardCVCEditText.getEditableText()) {
if (s.length() > CARD_CVC_TOTAL_SYMBOLS) {
s.delete(CARD_CVC_TOTAL_SYMBOLS, s.length());
}
}
}
So you test also the length of the editable text.
The date divider must be included only once and just after the month represented by two first characters. That means the last digit must be at 2nd position index 1, at that instant i==1. So no other divider will be appended. I should have to test if the divider set is for date, to know I'm dealing with the date and then test if the index is not greater or equals to 2.
That's how I could handle that. I hope this will help.
I need to be able to display text with all words starting with a # in a different color and they should be clickable. How can i do this?
This should do the trick
private void setTags(TextView pTextView, String pTagString) {
SpannableString string = new SpannableString(pTagString);
int start = -1;
for (int i = 0; i < pTagString.length(); i++) {
if (pTagString.charAt(i) == '#') {
start = i;
} else if (pTagString.charAt(i) == ' ' || pTagString.charAt(i) == '\n' || (i == pTagString.length() - 1 && start != -1)) {
if (start != -1) {
if (i == pTagString.length() - 1) {
i++; // case for if hash is last word and there is no
// space after word
}
final String tag = pTagString.substring(start, i);
string.setSpan(new ClickableSpan() {
#Override
public void onClick(View widget) {
Log.d("Hash", String.format("Clicked %s!", tag));
}
#Override
public void updateDrawState(TextPaint ds) {
// link color
ds.setColor(Color.parseColor("#33b5e5"));
ds.setUnderlineText(false);
}
}, start, i, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
start = -1;
}
}
}
pTextView.setMovementMethod(LinkMovementMethod.getInstance());
pTextView.setText(string);
}
I have a function to build clickable tags for a textview. It goes as follows:
private CharSequence tagsBuilder(String text, String token) {
SpannableStringBuilder builtTags = new SpannableStringBuilder();
int start = 0, end = 0;
for(int i = 0; i < 5; i++) {
start = 0;
end = text.indexOf(token, 0);
try {
if(start < end) {
SpannableStringBuilder ssb = new SpannableStringBuilder(text.substring(start, end));
ssb.setSpan(new ClickableSpan() {
#Override
public void onClick(View v)
{
Log.i("DEBUGTAG", "Span clicked - " + ((TextView) v).getText());
}
}, start, end, 0);
builtTags.append(ssb);
builtTags.append(" ");
text = text.substring(end + 1);
}
} catch (IndexOutOfBoundsException e) {
break;
}
}
return builtTags;
}
I can see the textview with 5 individually clickable tags. But the problem is, the Log that prints for any tag that is clicked is whole text of the textview.
Am I doing something wrong here? How do I get the text of individual tags that were clicked.
Your log line is the following:
Log.i("DEBUGTAG", "Span clicked - " + ((TextView) v).getText());
That logs the contents of the TextView. So... you get the text of the TextView. If you want to get token in there, you'll have to copy that in.
Here's something you can try:
private CharSequence tagsBuilder(String text, final String token) {
SpannableStringBuilder builtTags = new SpannableStringBuilder();
int start = 0, end = 0;
for(int i = 0; i < 5; i++) {
start = 0;
end = text.indexOf(token, 0);
try {
if(start < end) {
SpannableStringBuilder ssb = new SpannableStringBuilder(text.substring(start, end));
ssb.setSpan(new ClickableSpan() {
private String mText = token;
#Override
public void onClick(View v)
{
Log.i("DEBUGTAG", "Span clicked - " + mText);
}
}, start, end, 0);
builtTags.append(ssb);
builtTags.append(" ");
text = text.substring(end + 1);
}
} catch (IndexOutOfBoundsException e) {
break;
}
}
return builtTags;
}