Removing span in SpannableString - android

If i select a particular text in a text box (Edit Text), i want to make bold, italic, underline, change font, change font color of that selected text alone.
Eg:
Text want to be formatted segmented
if I select "matt" in the word formatted and try to make unbold, the result will be like this
Text want to be for matted segmented
Code:
public class RichTextEditor extends EditText
{
Context context;
boolean isBold = false;
boolean isItalic = false;
boolean isUnderline = false;
int fontSize = 6;
int fontColor = Color.BLACK;
TextView firstT, middleT, lastT;
SpannableStringBuilder res = new SpannableStringBuilder();
StyleSpan boldSpan = new StyleSpan(Typeface.BOLD);
StyleSpan italicSpan = new StyleSpan(Typeface.ITALIC);
StyleSpan BoldItalicSpan
= new StyleSpan(Typeface.BOLD_ITALIC);
public void setAllT (TextView firstT, TextView middleT, TextView lastT)
{
this.firstT = firstT;
this.middleT = middleT;
this.lastT = lastT;
}
public void makeBold ()
{
SpannableString text = new SpannableString(getText());
int start = getSelectionStart();
int end = getSelectionEnd();
if (start == end)
{
isBold = true;
}
else
{
SpannableString before = (SpannableString) text.subSequence(0,
start);
System.out.println("First : " + before);
SpannableString middle = (SpannableString) text.subSequence(start,
end);
System.out.println("Middle : " + middle);
SpannableString after = (SpannableString) text.subSequence(end,
text.length());
System.out.println("Last : " + after);
StyleSpan ref = isBetween(text, Typeface.BOLD);
if (ref != null)
{
middle.removeSpan(ref);
System.out.println("Removing Bold...");
setText(before);
append(middle);
append(new SpannableString(" "));
append(after);
}
else
{
middle.setSpan(getBoldStyleSpan(), 0, middle.length(), 0);
setText(before);
append(middle);
append(after);
}
CharSequence now = TextUtils.concat(before, > middle, after);
SpannableString ne = new SpannableString(now);
}
}
public void makeItalic ()
{
SpannableString text = new SpannableString(getText());
res = new SpannableStringBuilder(getText());
int start = getSelectionStart();
int end = getSelectionEnd();
if (start == end)
{
isBold = true;
}
else
{
SpannableString before = (SpannableString) text.subSequence(0,
start);
SpannableString middle = (SpannableString) text.subSequence(start, end);
SpannableString after = (SpannableString) text.subSequence(end,
text.length());
StyleSpan ref = isBetween(text, Typeface.ITALIC);
if (ref != null)
{
middle.removeSpan(getBoldStyleSpan());
System.out.println("Removing...");
}
else
{
middle.setSpan(getItalicStyleSpan(), 0, middle.length(), > 0);
}
setText(before);
append(middle);
append(after);
}
}
public void changeFontSize () {} // TODO Auto-generated method stub
public void changeFont () {} // TODO Auto-generated method stub
public void changeFontColor () {} // TODO Auto-generated method stub
private StyleSpan isBetween (SpannableString text, StyleSpan toCheck){
//return span if it is between the param tocheck span,else null
}
private StyleSpan getBoldStyleSpan () {
return new StyleSpan(Typeface.BOLD);
}
private StyleSpan getItalicStyleSpan () {
return new StyleSpan(Typeface.ITALIC);
}
}
public RichTextEditor (Context context, AttributeSet attr)
{
super(context, attr);
this.context = context;>mStyles = new ArrayList<RichTextEditor.PropertyHolder>();
}

Related

How to click a clickablespan using espresso?

I have a textview with multiple clickable spans in it. I want to be able to test clicking these spans.
I tried setting up a custom ViewAction that would find the clickablespans in the TextView and then match their text with the desired text and then click on the xy coordinates of that text. However, it seems the spans added to the TextView aren't of type ClickableSpan and are instead the the fragment that added the span.
Therefore, I am not able to distinguish the link spans. Is there a better way to do this?
Adding the spans:
Util.addClickableSpan(spannableString, string, linkedString, new ClickableSpan() {
#Override
public void onClick(View textView) {}
});
tvAcceptTc.setText(spannableString);
tvAcceptTc.setMovementMethod(LinkMovementMethod.getInstance());
Utility method:
public static void addClickableSpan(SpannableString spannableString,
String text,
String subText,
ClickableSpan clickableSpan) {
int start = text.indexOf(subText);
int end = text.indexOf(subText) + subText.length();
int flags = Spanned.SPAN_EXCLUSIVE_EXCLUSIVE;
spannableString.setSpan(clickableSpan, start, end, flags);
}
Defining the ViewAction:
#Override
public void perform(UiController uiController, View view) {
uiController.loopMainThreadUntilIdle();
if (view instanceof TextView) {
TextView textView = (TextView) view;
Layout textViewLayout = textView.getLayout();
SpannableString fullSpannable = new SpannableString(textView.getText());
Object[] spans = fullSpannable.getSpans(0, fullSpannable.length(), Object.class);
ClickableSpan span = null;
for (Object object : spans) {
if (object instanceof BaseFragment) {
ClickableSpan foundSpan = (ClickableSpan)object;
int spanStart = fullSpannable.getSpanStart(foundSpan);
int spanEnd = fullSpannable.getSpanEnd(foundSpan);
if (fullSpannable.subSequence(spanStart, spanEnd).equals(aSubstring)) {
//Found the correct span!
span = foundSpan;
}
}
} ... go on to click the xy-coordinates
This is my solution. It's simpler because we don't need to find the coordinates. Once we have found the ClickableSpan, we just click on it:
public static ViewAction clickClickableSpan(final CharSequence textToClick) {
return new ViewAction() {
#Override
public Matcher<View> getConstraints() {
return Matchers.instanceOf(TextView.class);
}
#Override
public String getDescription() {
return "clicking on a ClickableSpan";
}
#Override
public void perform(UiController uiController, View view) {
TextView textView = (TextView) view;
SpannableString spannableString = (SpannableString) textView.getText();
if (spannableString.length() == 0) {
// TextView is empty, nothing to do
throw new NoMatchingViewException.Builder()
.includeViewHierarchy(true)
.withRootView(textView)
.build();
}
// Get the links inside the TextView and check if we find textToClick
ClickableSpan[] spans = spannableString.getSpans(0, spannableString.length(), ClickableSpan.class);
if (spans.length > 0) {
ClickableSpan spanCandidate;
for (ClickableSpan span : spans) {
spanCandidate = span;
int start = spannableString.getSpanStart(spanCandidate);
int end = spannableString.getSpanEnd(spanCandidate);
CharSequence sequence = spannableString.subSequence(start, end);
if (textToClick.toString().equals(sequence.toString())) {
span.onClick(textView);
return;
}
}
}
// textToClick not found in TextView
throw new NoMatchingViewException.Builder()
.includeViewHierarchy(true)
.withRootView(textView)
.build();
}
};
}
Now you can use our custom ViewAction just like that:
onView(withId(R.id.myTextView)).perform(clickClickableSpan("myLink"));
Here is the Kotlin version of accepted answer
fun clickClickableSpan(textToClick: CharSequence): ViewAction {
return object : ViewAction {
override fun getConstraints(): Matcher<View> {
return Matchers.instanceOf(TextView::class.java)
}
override fun getDescription(): String {
return "clicking on a ClickableSpan";
}
override fun perform(uiController: UiController, view: View) {
val textView = view as TextView
val spannableString = textView.text as SpannableString
if (spannableString.isEmpty()) {
// TextView is empty, nothing to do
throw NoMatchingViewException.Builder()
.includeViewHierarchy(true)
.withRootView(textView)
.build();
}
// Get the links inside the TextView and check if we find textToClick
val spans = spannableString.getSpans(0, spannableString.length, ClickableSpan::class.java)
if (spans.isNotEmpty()) {
var spanCandidate: ClickableSpan
for (span: ClickableSpan in spans) {
spanCandidate = span
val start = spannableString.getSpanStart(spanCandidate)
val end = spannableString.getSpanEnd(spanCandidate)
val sequence = spannableString.subSequence(start, end)
if (textToClick.toString().equals(sequence.toString())) {
span.onClick(textView)
return;
}
}
}
// textToClick not found in TextView
throw NoMatchingViewException.Builder()
.includeViewHierarchy(true)
.withRootView(textView)
.build()
}
}
}
The best option would be to subclass a ViewAction. Here is the way of doing it in Kotlin:
class SpannableTextClickAction(val text: String) : ViewAction {
override fun getDescription(): String = "SpannableText click action"
override fun getConstraints(): Matcher<View> =
isAssignableFrom(TextView::class.java)
override fun perform(uiController: UiController?, view: View?) {
val textView = view as TextView
val spannableString = textView.text as SpannableString
val spans = spannableString.getSpans(0, spannableString.count(), ClickableSpan::class.java)
val spanToLocate = spans.firstOrNull { span: ClickableSpan ->
val start = spannableString.getSpanStart(span)
val end = spannableString.getSpanEnd(span)
val spanText = spannableString.subSequence(start, end).toString()
spanText == text
}
if (spanToLocate != null) {
spanToLocate.onClick(textView)
return
}
// textToClick not found in TextView
throw NoMatchingViewException.Builder()
.includeViewHierarchy(true)
.withRootView(textView)
.build()
}
}
and use it as:
onView(withId(<view_id>)).perform(scrollTo(), SpannableTextClickAction(text))
It worked with a minor change.
just recheck the "textToClick" and the variable "sequence" in:
CharSequence sequence = spannableString.subSequence(start, end);
are exactly same.
I have to use trim() like this:
textToClick.toString() == sequence.trim().toString()
because my textToClick value is "click here" and sequence value that I got " click here"
Note: The space before the "click".
I hope this is useful for someone.
This works for me:
/**
* Clicks the first ClickableSpan in the TextView
*/
public static ViewAction clickFirstClickableSpan() {
return new GeneralClickAction(
Tap.SINGLE,
new CoordinatesProvider() {
#Override
public float[] calculateCoordinates(View view) {
//https://leons.im/posts/how-to-get-coordinate-of-a-clickablespan-inside-a-textview/
TextView textView = (TextView) view;
Rect parentTextViewRect = new Rect();
SpannableString spannableString = (SpannableString) textView.getText();
Layout textViewLayout = textView.getLayout();
ClickableSpan spanToLocate = null;
if (spannableString.length() == 0) {
return new float[2];
}
ClickableSpan[] spans = spannableString.getSpans(0, spannableString.length(), ClickableSpan.class);
if (spans.length > 0) {
spanToLocate = spans[0];
}
if (spanToLocate == null) {
// no specific view found
throw new NoMatchingViewException.Builder()
.includeViewHierarchy(true)
.withRootView(textView)
.build();
}
double startOffsetOfClickedText = spannableString.getSpanStart(spanToLocate);
double endOffsetOfClickedText = spannableString.getSpanEnd(spanToLocate);
double startXCoordinatesOfClickedText = textViewLayout.getPrimaryHorizontal((int) startOffsetOfClickedText);
double endXCoordinatesOfClickedText = textViewLayout.getPrimaryHorizontal((int) endOffsetOfClickedText);
// Get the rectangle of the clicked text
int currentLineStartOffset = textViewLayout.getLineForOffset((int) startOffsetOfClickedText);
int currentLineEndOffset = textViewLayout.getLineForOffset((int) endOffsetOfClickedText);
boolean keywordIsInMultiLine = currentLineStartOffset != currentLineEndOffset;
textViewLayout.getLineBounds(currentLineStartOffset, parentTextViewRect);
// Update the rectangle position to his real position on screen
int[] parentTextViewLocation = {0, 0};
textView.getLocationOnScreen(parentTextViewLocation);
double parentTextViewTopAndBottomOffset = (
parentTextViewLocation[1] -
textView.getScrollY() +
textView.getCompoundPaddingTop()
);
parentTextViewRect.top += parentTextViewTopAndBottomOffset;
parentTextViewRect.bottom += parentTextViewTopAndBottomOffset;
parentTextViewRect.left += (
parentTextViewLocation[0] +
startXCoordinatesOfClickedText +
textView.getCompoundPaddingLeft() -
textView.getScrollX()
);
parentTextViewRect.right = (int) (
parentTextViewRect.left +
endXCoordinatesOfClickedText -
startXCoordinatesOfClickedText
);
int screenX = (parentTextViewRect.left + parentTextViewRect.right) / 2;
int screenY = (parentTextViewRect.top + parentTextViewRect.bottom) / 2;
if (keywordIsInMultiLine) {
screenX = parentTextViewRect.left;
screenY = parentTextViewRect.top;
}
return new float[]{screenX, screenY};
}
},
Press.FINGER);
}
you may use Spannable instead of SpannableString compatible with SpannableStringBuilder.
sorry, i am a new man , have only 1 Reputation , can not add a comment.Even my English is very poor.....
i suggest to use:
Spannable spannableString = (Spannable) textView.getText();
instead of :
SpannableString spannableString = (SpannableString) textView.getText();
post all the code below:
public class CustomViewActions {
/**
* click specific spannableString
*/
public static ViewAction clickClickableSpan(final CharSequence textToClick) {
return clickClickableSpan(-1, textToClick);
}
/**
* click the first spannableString
*/
public static ViewAction clickClickableSpan() {
return clickClickableSpan(0, null);
}
/**
* click the nth spannableString
*/
public static ViewAction clickClickableSpan(final int index) {
return clickClickableSpan(index, null);
}
public static ViewAction clickClickableSpan(final int index,final CharSequence textToClick) {
return new ViewAction() {
#Override
public Matcher<View> getConstraints() {
return instanceOf(TextView.class);
}
#Override
public String getDescription() {
return "clicking on a ClickableSpan";
}
#Override
public void perform(UiController uiController, View view) {
TextView textView = (TextView) view;
Spannable spannableString = (Spannable) textView.getText();
ClickableSpan spanToLocate = null;
if (spannableString.length() == 0) {
// TextView is empty, nothing to do
throw new NoMatchingViewException.Builder()
.includeViewHierarchy(true)
.withRootView(textView)
.build();
}
// Get the links inside the TextView and check if we find textToClick
ClickableSpan[] spans = spannableString.getSpans(0, spannableString.length(), ClickableSpan.class);
if (spans.length > 0) {
if(index >=spans.length){
throw new NoMatchingViewException.Builder()
.includeViewHierarchy(true)
.withRootView(textView)
.build();
}else if (index >= 0) {
spanToLocate = spans[index];
spanToLocate.onClick(textView);
return;
}
for (int i = 0; i < spans.length; i++) {
int start = spannableString.getSpanStart(spans[i]);
int end = spannableString.getSpanEnd(spans[i]);
CharSequence sequence = spannableString.subSequence(start, end);
if (textToClick.toString().equals(sequence.toString())) {
spanToLocate = spans[i];
spanToLocate.onClick(textView);
return;
}
}
}
// textToClick not found in TextView
throw new NoMatchingViewException.Builder()
.includeViewHierarchy(true)
.withRootView(textView)
.build();
}
};
}
}
Espresso has a one-liner for this:
onView(withId(R.id.textView)).perform(openLinkWithText("..."))

How to highlight the filtered text while using SearchView widget in android

I have implemented SearchView Widget in my app. Its working fine. Now i need to do is, whenever i type a word in my SearchView Bar , the filtered result should show the searched word highlighted. like:
I am using this SearchView widget as :
#Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.myMenu , menu);
MenuItem searchItem = menu.findItem(R.id.action_search);
SearchView sv = new SearchView(getActivity());
// Changing the color of Searchview widget text field to white.
int searchSrcTextId = getResources().getIdentifier("android:id/search_src_text", null, null);
EditText searchEditText = (EditText) sv.findViewById(searchSrcTextId);
searchEditText.setTextColor(Color.WHITE);
sv.setOnQueryTextListener(this);
searchItem.setActionView(sv);
super.onCreateOptionsMenu(menu, inflater);
}
you can use Spannable TextView for this.
hope so this Method will help you
Method:
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;
}
and return originalText will highlight text.
You Should do this in onBindViewHolder() method (using RecyclerView)
class YourAdapter
String searchString="";
#Override
public void onBindViewHolder(AllMessageAdapter.DataObjectHolder holder, final int position) {
holder.message.setText(mDataset.get(position).Message);
AllMessageList.Message message=mDataset.get(position);
String name = message.Message.toLowerCase(Locale.getDefault());
if (name.contains(searchString)) {
int startPos = name.indexOf(searchString);
int endPos = startPos + searchString.length();
Spannable spanString = Spannable.Factory.getInstance().newSpannable(holder.message.getText());
spanString.setSpan(new ForegroundColorSpan(Color.RED), startPos, endPos, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
holder.message.setText(spanString);
}
}
Your Filter(in Adapter)
public void setFilter(ArrayList<AllMessageList.Message> countryModels,String searchString) {
this.searchString=searchString;
mDataset = new ArrayList<>();
mDataset.addAll(countryModels);
notifyDataSetChanged();
}
You can use this to highlight all the keywords.
button.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v) {
String ett = edittext.getText().toString();
String tvt = textview.getText().toString();
int ofe = tvt.indexOf(ett, 0);
Spannable spannable = new SpannableString(tvt);
for (int ofs = 0; ofs < tvt.length() && ofe != -1; ofs = ofe + 1) {
ofe = tvt.indexOf(ett, ofs);
if (ofe == -1)
break;
else {
ColorStateList blueColor = new ColorStateList(new nt[][] { new int[] {} }, new int[] { Color.BLUE });
TextAppearanceSpan highlightSpan = new TextAppearanceSpan(null, Typeface.BOLD, -1, blueColor, null);
spannable.setSpan(highlightSpan, ofe, ofe+edittext.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
textview.setText(spannable);
}
}
}
});

Highlight search results in ListView

I have a ListView with Strings. With the below code I can highlight search results, but the user must type the words to search case sensitive. How can I implement a none - case sensitive highlighting of search results for example like the native Android Contact search?
Here is my code for Highlighting. I extend the ArrayAdapter and implement customized filter to get the string to search. In the getView method I check if my String in ListView contains the prefixString and highlight it.
public class HighlightListAdapter extends ArrayAdapter {
ArrayList<String> objects;
final Object mLock =new Object();
private ArrayList<String> mOriginalValues;
private ArrayFilter filter;
private String prefixString;
public AuthorsListAdapter(Context context, int textViewResourceId, ArrayList<String> objects) {
super(context, textViewResourceId, objects);
this.objects = objects;
}
class ViewHolder{
TextView author;
}
public View getView(final int position, View convertView, ViewGroup parent){
// assign the view we are converting to a local variable
View v = convertView;
ViewHolder holder = null;
// first check to see if the view is null. if so, we have to inflate it.
// to inflate it basically means to render, or show, the view.
LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
if (v == null) {
holder = new ViewHolder();
v = inflater.inflate(R.layout.author_list_item, null);
holder.author =(TextView) v.findViewById(R.id.author_list_item_text);
v.setTag(holder);
}else{
holder = (ViewHolder) v.getTag();
}
final String author = objects.get(position);
if (author != null) {
holder.author.setText(author);
if(prefixString !=null && prefixString.length()>1){
String s = author;
**if(s.contains(prefixString)){
String rep = s.replace(prefixString, "<b><font color=#2825A6>"+ prefixString+ "</font></b>");
holder.author.setText(Html.fromHtml(rep));
}** // higlight
}
}
return v;
}
#Override
public int getCount() {
// TODO Auto-generated method stub
return objects.size();
}
#Override
public Filter getFilter() {
// TODO Auto-generated method stub
if(filter == null){
filter =new ArrayFilter();
}
return filter;
}
#Override
public Object getItem(int position) {
// TODO Auto-generated method stub
return this.objects.get(position);
}
private class ArrayFilter extends Filter {
#Override
protected FilterResults performFiltering(CharSequence prefix) {
FilterResults results = new FilterResults();
if (mOriginalValues == null) {
synchronized (mLock) {
mOriginalValues = new ArrayList<String>(objects);
}
}
if (prefix == null || prefix.length() == 0) {
ArrayList<String> list;
synchronized (mLock) {
list = new ArrayList<String>(mOriginalValues);
}
results.values = list;
results.count = list.size();
} else {
**prefixString = prefix.toString();** // get string to search
ArrayList<String> values;
synchronized (mLock) {
values = new ArrayList<String>(mOriginalValues);
}
final int count = values.size();
final ArrayList<String> newValues = new ArrayList<String>();
for (int i = 0; i < count; i++) {
final String value = values.get(i);
final String valueText = value.toString().toLowerCase();
// First match against the whole, non-splitted value
if (valueText.startsWith(prefixString)) {
newValues.add(value);
} else {
final String[] words = valueText.split(" ");
final int wordCount = words.length;
// Start at index 0, in case valueText starts with space(s)
for (int k = 0; k < wordCount; k++) {
if (words[k].startsWith(prefixString)) {
newValues.add(value);
break;
}
}
}
}
results.values = newValues;
results.count = newValues.size();
}
return results;
}
#SuppressWarnings("unchecked")
#Override
protected void publishResults(CharSequence constraint, FilterResults results) {
objects = (ArrayList<String>) results.values;
if (results.count > 0) {
notifyDataSetChanged();
} else {
notifyDataSetInvalidated();
}
}
};
}
This what I use :
Every occurence is replaced (not only prefix)
Case and accent are ignored while searching but retained in the result.
It uses directly SpannableString, which you can use in setText(). I believe it's more efficient than using an intermediate html step.
.
public static CharSequence highlight(String search, String originalText) {
// ignore case and accents
// the same thing should have been done for the search text
String normalizedText = Normalizer.normalize(originalText, Normalizer.Form.NFD).replaceAll("\\p{InCombiningDiacriticalMarks}+", "").toLowerCase();
int start = normalizedText.indexOf(search);
if (start < 0) {
// not found, nothing to to
return originalText;
} else {
// highlight each appearance in the original text
// while searching in normalized text
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(<background_color>), spanStart, spanEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
start = normalizedText.indexOf(search, spanEnd);
}
return highlighted;
}
}
The accepted answer is nice. But you can do it by a single line of code. What I've done in my case to avoid the case sensitive issue is:
Spannable sb = new SpannableString(originalText);
sb.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), originalText.toLowerCase().indexOf(query.toLowerCase()),
originalText.toLowerCase().indexOf(query.toLowerCase()) + query.length(),
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
result.setText(sb);
Hope it might help!
Note: Here 'query' is the part of the string that you want to highlight.
Simple & Advanced Search Highlighting Example [Case Insensitive Order]
1. Simple Search (Html):
public static void setSearchTextHighlightSimpleHtml(TextView textView, String fullText, String searchText) {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
fullText = fullText.replaceAll("(?i)(" + searchText + ")", "<span style=\"background-color:#FCFF48;\"><b><big><font color='#a10901'>$1</font></big></b></span>");
textView.setText(Html.fromHtml(fullText, Html.FROM_HTML_MODE_LEGACY), TextView.BufferType.SPANNABLE);
} else {
fullText = fullText.replaceAll("(?i)(" + searchText + ")", "<b><big><font color='red'>$1</font></big></b>");
textView.setText(Html.fromHtml(fullText), TextView.BufferType.SPANNABLE);
}
} catch (Exception e) {
textView.setText(fullText);
}
}
2. Simple Search (Spannable):
public static void setSearchTextHighlightSimpleSpannable(TextView textView, String fullText, String searchText) {
// highlight search text
if (null != searchText && !searchText.isEmpty()) {
SpannableStringBuilder wordSpan = new SpannableStringBuilder(fullText);
Pattern p = Pattern.compile(searchText, Pattern.CASE_INSENSITIVE);
Matcher m = p.matcher(fullText);
while (m.find()) {
int wordStart = m.start();
int wordEnd = m.end();
// Now highlight based on the word boundaries
ColorStateList redColor = new ColorStateList(new int[][]{new int[]{}}, new int[]{0xffa10901});
TextAppearanceSpan highlightSpan = new TextAppearanceSpan(null, Typeface.BOLD, -1, redColor, null);
wordSpan.setSpan(highlightSpan, wordStart, wordEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
wordSpan.setSpan(new BackgroundColorSpan(0xFFFCFF48), wordStart, wordEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
wordSpan.setSpan(new RelativeSizeSpan(1.25f), wordStart, wordEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
textView.setText(wordSpan, TextView.BufferType.SPANNABLE);
} else {
textView.setText(fullText);
}
}
3. Advanced Search (Spannable):
public static void setAdvancedSearch(TextView textView, String fullText, String searchText) {
if (searchText.length() == 0) {
textView.setText(fullText);
return;
}
final String searchBoundary = " \n()ред.,;?-+!";
char[] boundaries = searchBoundary.toCharArray();
// highlight search text
if (isNotEquals(searchText, boundaries)) {
SpannableStringBuilder wordSpan = new SpannableStringBuilder(fullText);
Pattern p = Pattern.compile(searchText, Pattern.CASE_INSENSITIVE);
Matcher m = p.matcher(fullText);
while (m.find()) {
int wordStart = m.start();
while (wordStart >= 0 && isNotEquals(fullText.charAt(wordStart), boundaries)) {
--wordStart;
}
wordStart = wordStart + 1;
int wordEnd = m.end();
while (wordEnd < fullText.length() && isNotEquals(fullText.charAt(wordEnd), boundaries)) {
++wordEnd;
}
setWordSpan(wordSpan, wordStart, wordEnd);
}
textView.setText(wordSpan, TextView.BufferType.SPANNABLE);
} else {
textView.setText(fullText);
}
}
private static boolean isNotEquals(char charAt, char[] boundaries) {
return isNotEquals(String.valueOf(charAt), boundaries);
}
private static boolean isNotEquals(String searchText, char[] boundaries) {
for (char boundary : boundaries) {
boolean equals = searchText.equals(String.valueOf(boundary));
if (equals) return false;
}
return true;
}
private static void setWordSpan(SpannableStringBuilder wordSpan, int wordStart, int wordEnd) {
// Now highlight based on the word boundaries
ColorStateList redColor = new ColorStateList(new int[][]{new int[]{}}, new int[]{0xffa10901});
TextAppearanceSpan highlightSpan = new TextAppearanceSpan(null, Typeface.BOLD, -1, redColor, null);
wordSpan.setSpan(highlightSpan, wordStart, wordEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
wordSpan.setSpan(new BackgroundColorSpan(0xFFFCFF48), wordStart, wordEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
wordSpan.setSpan(new RelativeSizeSpan(1.25f), wordStart, wordEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
First, your code
if(s.contains(prefixString)){
String rep = s.replace(prefixString, "<b><font color=#2825A6>"+ prefixString+ "</font></b>");
holder.author.setText(Html.fromHtml(rep));
}
is not good. You should use String.startsWith to check if the start of s equals to prefixString. Your actual code works, but it checks presence of prefixString in s, but doesn't care about its position.
For having case insensitive search, you can use String.toLowerCase or String.toUpperCase on both strings when checking presence of prefixString. Case will be ignored.
if(s.toLowerCase().startsWith(prefixString.toLowerCase())){
String rep = "<b><font color=#2825A6>" + prefixString + "</font></b>" + s.substring(prefixString.length());
holder.author.setText(Html.fromHtml(rep));
}

How to set multiple spans on a TextView's text on the same partial text?

Suppose I have the next text :
Hello stackOverflow
And I wish to set the second word to be both RelativeSizeSpan (to set a relative font size) and TextAppearanceSpan (to set the color of the text) , how do I merge them both ?
All I know is that I can choose one of them , using the next code for example :
final SpannableString textToShow = new SpannableString("Hello stackOverflow");
textToShow.setSpan(new RelativeSizeSpan(1.5f), textToShow.length() - "stackOverflow".length(),textToShow.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
textView.setText(textToShow);
But I need to also set the color , or even add other features from other spanning classes ...
What can I do ?
Simply set additional spans. They are going to overlap/merge when neccessary. This code works for me:
final SpannableString text = new SpannableString("Hello stackOverflow");
text.setSpan(new RelativeSizeSpan(1.5f), text.length() - "stackOverflow".length(), text.length(),
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
text.setSpan(new ForegroundColorSpan(Color.RED), 3, text.length() - 3, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
tv.setText(text);
I know this a new reply to an already answered question but I'd like to share a utility class I made which makes this task easier.
Java Version
public class SimpleSpanBuilder {
private class SpanSection{
private final String text;
private final int startIndex;
private final CharacterStyle[] styles;
private SpanSection(String text, int startIndex,CharacterStyle... styles){
this.styles = styles;
this.text = text;
this.startIndex = startIndex;
}
private void apply(SpannableStringBuilder spanStringBuilder){
if (spanStringBuilder == null) return;
for (CharacterStyle style : styles){
spanStringBuilder.setSpan(style, startIndex, startIndex + text.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
}
}
}
private List<SpanSection> spanSections;
private StringBuilder stringBuilder;
public SimpleSpanBuilder(){
stringBuilder = new StringBuilder();
spanSections = new ArrayList<>();
}
public SimpleSpanBuilder append(String text,CharacterStyle... styles){
if (styles != null && styles.length > 0) {
spanSections.add(new SpanSection(text, stringBuilder.length(),styles));
}
stringBuilder.append(text);
return this;
}
public SimpleSpanBuilder appendWithSpace(String text,CharacterStyle... styles){
return append(text.concat(" "),styles);
}
public SimpleSpanBuilder appendWithLineBreak(String text,CharacterStyle... styles){
return append(text.concat("\n"),styles);
}
public SpannableStringBuilder build(){
SpannableStringBuilder ssb = new SpannableStringBuilder(stringBuilder.toString());
for (SpanSection section : spanSections){
section.apply(ssb);
}
return ssb;
}
#Override
public String toString() {
return stringBuilder.toString();
}
}
Usage:
SimpleSpanBuilder ssb = new SimpleSpanBuilder();
ssb.appendWithSpace("Hello");
ssb.append("StackOverflow",new ForegroundColorSpan(Color.RED),new RelativeSizeSpan(1.5));
textView.setText(ssb.build());
Kotlin Version
class SimpleSpanBuilder() {
class Span {
private var startIndex: Int = 0
internal var text: String
private var styles: Array<out CharacterStyle>
internal constructor(index: Int, text: String, vararg styles: CharacterStyle) {
this.startIndex = index
this.text = text
this.styles = styles
}
constructor(text: String, vararg styles: CharacterStyle) : this(0, text, *styles)
internal fun setIndex(index: Int): Span {
return Span(index, this.text, *this.styles)
}
internal fun apply(spanStringBuilder: SpannableStringBuilder?) {
if (spanStringBuilder == null) return
for (style in styles) {
spanStringBuilder.setSpan(
style,
startIndex,
startIndex + text.length,
Spannable.SPAN_INCLUSIVE_EXCLUSIVE
)
}
}
}
private val spanSections = mutableListOf<Span>()
private val stringBuilder = StringBuilder()
constructor(text: String, vararg styles: CharacterStyle) : this() {
plus(Span(text, *styles))
}
operator fun plus(span: SimpleSpanBuilder.Span): SimpleSpanBuilder {
spanSections.add(span.setIndex(stringBuilder.length))
stringBuilder.append(span.text)
return this
}
fun build(): SpannableStringBuilder {
val ssb = SpannableStringBuilder(stringBuilder.toString())
for (section in spanSections) {
section.apply(ssb)
}
return ssb
}
override fun toString(): String {
return stringBuilder.toString()
}
}
Usage
var ssb = SimpleSpanBuilder("Hello ",ForegroundColorSpan(Color.BLUE))
ssb += SimpleSpanBuilder.Span(
"StackOverflow",
ForegroundColorSpan(Color.RED),
RelativeSizeSpan(1.5f)
)
textView.text = ssb.build()
Most Easy Way?
textView.setText("I love coding");
setHighLightedText(textView,"coding");
Just use below method -
public void setHighLightedText(TextView tv, String textToHighlight) {
String tvt = tv.getText().toString();
int ofe = tvt.indexOf(textToHighlight, 0);
Spannable wordToSpan = new SpannableString(tv.getText());
for (int ofs = 0; ofs < tvt.length() && ofe != -1; ofs = ofe + 1) {
ofe = tvt.indexOf(textToHighlight, ofs);
if (ofe == -1)
break;
else {
// you can change or add more span as per your need
wordToSpan.setSpan(new RelativeSizeSpan(2f), ofe,ofe + textToHighlight.length(), 0); // set size
wordToSpan.setSpan(new ForegroundColorSpan(Color.RED), ofe, ofe + textToHighlight.length(), 0);// set color
tv.setText(wordToSpan, TextView.BufferType.SPANNABLE);
}
}
}
Kotlin can help doing this with an extension on SpannableStringBuilder:
fun SpannableStringBuilder.spansAppend(
text: CharSequence,
flags: Int,
vararg spans: Any
): SpannableStringBuilder {
val start = length
append(text)
spans.forEach { span ->
setSpan(span, start, length, flags)
}
return this
}
Examples of usage:
val builder = SpannableStringBuilder()
builder.append("Start of string ")
builder.spansAppend(
"text spanned",
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE,
RelativeSizeSpan(1.1f),
ForegroundColorSpan(Color.RED)
)

Multiple clickspans in a textview

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;
}

Categories

Resources