Inconsistent SpannableString setSpan() coloring? - android

I'm trying to color only vowels in my String 1 color (ex red), and non-vowels another (ex: blue). But the SpannableString setSpan() method is being inconsistent when iterating thru each char. The function is detecting the vows and non-vows correctly, as I had checked the logged output,except that the coloring is not correct:
//ColorLogic.java:
public SpannableString colorString(String myStr)
{
SpannableString spnStr=new SpannableString(myStr);
ForegroundColorSpan vowColor=new ForegroundColorSpan(Color.RED);
ForegroundColorSpan conColor=new ForegroundColorSpan(Color.BLUE);
int strLen=myStr.length();
for(int i=0; i< strLen; i++)
{
if (vowSet.contains(Character.toLowerCase(myStr.charAt(i))))
//if (i%2==0)
{
Log.v(DTAG, "vow"+myStr.charAt(i));
spnStr.setSpan(vowColor, i, i, 0);
}
else
{
Log.v(DTAG, "cons"+myStr.charAt(i));
spnStr.setSpan(conColor, i, i, 0);
}
}
return spnStr;
}
//In my OnCreate of my activity class:
//PASS
//Log.v(DTAG, message);
// Create the text view
TextView textView = new TextView(this);
textView.setTextSize(50);
//Call Color Logic to color each letter individually
ColorLogic myColorTxt=new ColorLogic();
SpannableString spnMsg=myColorTxt.colorString(message);
//Log.v(DTAG, "spnMsg: "+spnMsg.toString());
textView.setText(spnMsg, BufferType.SPANNABLE);
//textView.setTextColor(Color.GREEN);
setContentView(textView);
}
![Vows Only its correct (non-vowels only is correct as well)][1]
![With cons and vows, 2 letters then its incorrect!][2]

You cannot reuse span objects. As pskink indicates, please use distinct ForegroundColorSpan objects for each setSpan() call.
Also, you may wish to use fewer spans overall. While your sample ("abibobu") requires the maximum possible number of spans, most words have consonants and vowels strung together. For example, the word "consonant" has two two-consonant spans ("ns" and "nt"). Those could be colored using a single ForegroundColorSpan, rather than two, improving rendering speed. Spans are easy but not the fastest, and so the fewer spans you use, the better your app will perform, particularly in animated situations (e.g., scrolling in a ListView).
Furthermore, you may only need to color either consonants or vowels, unless you are planning on a third color for hyphens and apostrophes. Remember: your text can start with a color (e.g., android:textColor).

Related

TextView with mixed languages

I have a TextView with 2 lines. first line rtl language (let's say hebrew), second line is ltr language (let's say english)
The View result is something like:
אחת שתיים שלוש
one two three
what i want: align rtl in that case
אחת שתיים שלוש
one two three
I've tried using setTextDirection() with TEXT_DIRECTION_FIRST_STRONG
but alas the results were the same. Also tried TEXT_ANY_RTL without success
myTextView.setTextDirection(View.TEXT_DIRECTION_FIRST_STRONG);
if i'm using TEXT_DIRECTION_RTL it's working as expected but this is not really a solution because most of the time the TextView will contain only one language.
Is this solvable?
--- UPDATE ---
How i'm populating the TextView
SpannableStringBuilder ssb = new SpannableStringBuilder(titleText);
int end = titlText.length();
ssb.append("\n").append(otheText);
ssb.setSpan(new AbsoluteSizeSpan(size), end, ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
textView.setText(ssb);
Why not just use two TextViews?
I've managed to solve this problem using Character.getDirectionality.
The first char that is a directional char will signify the TextView direction
#TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
public static int getTextDirection(String text) {
final int length = text.length();
for (int i = 0; i < length; i++) {
final char c = text.charAt(i);
final byte directionality = Character.getDirectionality(c);
if(directionality == Character.DIRECTIONALITY_LEFT_TO_RIGHT){
return View.TEXT_DIRECTION_LTR;
}
else if(directionality == Character.DIRECTIONALITY_RIGHT_TO_LEFT){
return View.TEXT_DIRECTION_RTL;
}
}
return View.TEXT_DIRECTION_ANY_RTL;
}
and then:
textView.setTextDirection(textDirection);
I Strongly believe that TEXT_DIRECTION_FIRST_STRONG is supposed to do the exact same thing according to the docs. sadly it's not the case.
I'm not accepting my answer in hope that someone will suggest better solution
What About TEXT_DIRECTION_ANY_RTL
This text direction is using "any-RTL" algorithm. The paragraph direction is RTL if it contains any strong RTL character, otherwise it is LTR if it contains any strong LTR characters. If there are neither, the paragraph direction is the view's resolved layout direction.

Getting the range of an applied span in SpannableString

I am trying to get the set of applied spans on a SpannableString and then find the ranges in which they have been applied. Is that possible? As far as I have seen, there doesn't seem to be a built in method to do this. Am I right?
Object[] spans = spannableString.getSpans(0, spannableString.length(), Object.class);
List<Object> spanArray = asList(spans);
for (Object span: spanArray) {
if (span.getClass().equals(MyCustomSpan.class)) {
MyCustomSpan s = (MyCustomSpan) span;
// Get the range in spannableString where this span has been applied.
// How do I do this?
}
}
Call getSpanStart() and getSpanEnd() to retrieve the start and end positions within the Spanned where the span is applied.

Get Spannable String from EditText

I have set a SpannableString to an EditText, now I want to get this text from the EditText and get its markup information. I tried like this:
SpannableStringBuilder spanStr = (SpannableStringBuilder) et.getText();
int boldIndex = spanStr.getSpanStart(new StyleSpan(Typeface.BOLD));
int italicIndex = spanStr.getSpanStart(new StyleSpan(Typeface.ITALIC));
But it gives index -1 for both bold and italic, although it is showing text with italic and bold.
Please help.
From the code you've posted, you're passing new spans to spanStr and asking it to find them. You'll need to have a reference to the instances of those spans that are actually applied. If that's not feasible or you don't want to track spans directly, you can simply call
getSpans to get all the spans applied. You can then filter that array for what you want.
If you don't care about the spans in particular, you can also just call Html.toHtml(spanStr) to get an HTML tagged version.
edit: to add code example
This will grab all applied StyleSpans which is what you want.
/* From the Android docs on StyleSpan: "Describes a style in a span.
* Note that styles are cumulative -- both bold and italic are set in
* separate spans, or if the base is bold and a span calls for italic,
* you get bold italic. You can't turn off a style from the base style."*/
StyleSpan[] mSpans = et.getText().getSpans(0, et.length(), StyleSpan.class);
Here's a link to the StyleSpan docs.
To pick out the spans you want if you have various spans mixed in to a collection/array, you can use instanceof to figure out what type of spans you've got. This snippet will check if a particular span mSpan is an instance of StyleSpan and then print its start/end indices and flags. The flags are constants that describe how the span ends behave such as: Do they include and apply styling to the text at the start/end indices or only to text input at an index inside the start/end range).
if (mSpan instanceof StyleSpan) {
int start = et.getSpanStart(mSpan);
int end = et.getSpanEnd(mSpan);
int flag = et.getSpanFlags(mSpan);
Log.i("SpannableString Spans", "Found StyleSpan at:\n" +
"Start: " + start +
"\n End: " + end +
"\n Flag(s): " + flag);
}

How to make part of the text Bold in android at runtime?

A ListView in my application has many string elements like name, experience, date of joining, etc. I just want to make name bold. All the string elements will be in a single TextView.
my XML:
<ImageView
android:id="#+id/logo"
android:layout_width="55dp"
android:layout_height="55dp"
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"
android:layout_marginTop="15dp" >
</ImageView>
<TextView
android:id="#+id/label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toRightOf="#id/logo"
android:padding="5dp"
android:textSize="12dp" >
</TextView>
My code to set the TextView of the ListView item:
holder.text.setText(name + "\n" + expirience + " " + dateOfJoininf);
Let's say you have a TextView called etx. You would then use the following code:
final SpannableStringBuilder sb = new SpannableStringBuilder("HELLOO");
final StyleSpan bss = new StyleSpan(android.graphics.Typeface.BOLD); // Span to make text bold
final StyleSpan iss = new StyleSpan(android.graphics.Typeface.ITALIC); //Span to make text italic
sb.setSpan(bss, 0, 4, Spannable.SPAN_INCLUSIVE_INCLUSIVE); // make first 4 characters Bold
sb.setSpan(iss, 4, 6, Spannable.SPAN_INCLUSIVE_INCLUSIVE); // make last 2 characters Italic
etx.setText(sb);
Based on Imran Rana's answer, here is a generic, reusable method if you need to apply StyleSpans to several TextViews, with support for multiple languages (where indices are variable):
void setTextWithSpan(TextView textView, String text, String spanText, StyleSpan style) {
SpannableStringBuilder sb = new SpannableStringBuilder(text);
int start = text.indexOf(spanText);
int end = start + spanText.length();
sb.setSpan(style, start, end, Spannable.SPAN_INCLUSIVE_INCLUSIVE);
textView.setText(sb);
}
Use it in an Activity like so:
#Override
protected void onCreate(Bundle savedInstanceState) {
// ...
StyleSpan boldStyle = new StyleSpan(Typeface.BOLD);
setTextWithSpan((TextView) findViewById(R.id.welcome_text),
getString(R.string.welcome_text),
getString(R.string.welcome_text_bold),
boldStyle);
// ...
}
strings.xml
<string name="welcome_text">Welcome to CompanyName</string>
<string name="welcome_text_bold">CompanyName</string>
Result:
Welcome to CompanyName
You can do it using Kotlin and buildSpannedString extension function from core-ktx
holder.textView.text = buildSpannedString {
bold { append("$name\n") }
append("$experience $dateOfJoining")
}
The answers provided here are correct, but can't be called in a loop because the StyleSpan object is a single contiguous span (not a style that can be applied to multiple spans). Calling setSpan multiple times with the same bold StyleSpan would create one bold span and just move it around in the parent span.
In my case (displaying search results), I needed to make all instances of all the search keywords appear bold. This is what I did:
private static SpannableStringBuilder emboldenKeywords(final String text,
final String[] searchKeywords) {
// searching in the lower case text to make sure we catch all cases
final String loweredMasterText = text.toLowerCase(Locale.ENGLISH);
final SpannableStringBuilder span = new SpannableStringBuilder(text);
// for each keyword
for (final String keyword : searchKeywords) {
// lower the keyword to catch both lower and upper case chars
final String loweredKeyword = keyword.toLowerCase(Locale.ENGLISH);
// start at the beginning of the master text
int offset = 0;
int start;
final int len = keyword.length(); // let's calculate this outside the 'while'
while ((start = loweredMasterText.indexOf(loweredKeyword, offset)) >= 0) {
// make it bold
span.setSpan(new StyleSpan(Typeface.BOLD), start, start+len, SPAN_INCLUSIVE_INCLUSIVE);
// move your offset pointer
offset = start + len;
}
}
// put it in your TextView and smoke it!
return span;
}
Keep in mind that the code above isn't smart enough to skip double-bolding if one keyword is a substring of the other. For example, if you search for "Fish fi" inside "Fishes in the fisty Sea" it will make the "fish" bold once and then the "fi" portion. The good thing is that while inefficient and a bit undesirable, it won't have a visual drawback as your displayed result will still look like
Fishes in the fisty Sea
if you don't know exactly the length of the text before the text portion that you want to make Bold, or even you don't know the length of the text to be Bold, you can easily use HTML tags like the following:
yourTextView.setText(Html.fromHtml("text before " + "<font><b>" + "text to be Bold" + "</b></font>" + " text after"));
<string name="My_Name">Given name is <b>Not Right</b>Please try again </string>
use "b" tag in string.xml file.
also for Italic "i" and Underline "u".
Extending frieder's answer to support case and diacritics insensitivity.
public static String stripDiacritics(String s) {
s = Normalizer.normalize(s, Normalizer.Form.NFD);
s = s.replaceAll("[\\p{InCombiningDiacriticalMarks}]", "");
return s;
}
public static void setTextWithSpan(TextView textView, String text, String spanText, StyleSpan style, boolean caseDiacriticsInsensitive) {
SpannableStringBuilder sb = new SpannableStringBuilder(text);
int start;
if (caseDiacriticsInsensitive) {
start = stripDiacritics(text).toLowerCase(Locale.US).indexOf(stripDiacritics(spanText).toLowerCase(Locale.US));
} else {
start = text.indexOf(spanText);
}
int end = start + spanText.length();
if (start > -1)
sb.setSpan(style, start, end, Spannable.SPAN_INCLUSIVE_INCLUSIVE);
textView.setText(sb);
}
If you are using the # srings / your_string annotation, access the strings.xml file and use the <b></b> tag in the part of the text you want.
Example:
<string><b>Bold Text</b><i>italic</i>Normal Text</string>
I recommend to use strings.xml file with CDATA
<string name="mystring"><![CDATA[ <b>Hello</b> <i>World</i> ]]></string>
Then in the java file :
TextView myTextView = (TextView) this.findViewById(R.id.myTextView);
myTextView.setText(Html.fromHtml( getResources().getString(R.string.mystring) ));
To better support translations and remove any dependency on length of the string or particular index, you should use android.text.Annotation in you string defined strings.xml.
In your particular case, you can create a string like below
<string name="bold_name_experience_text"><annotation type="bold">name</annotation> \nexpirience dateOfJoininf</string>
or if you want to substitute these in runtime, you can create a string as follow
<string name="bold_name_experience_text"><annotation type="bold">name</annotation> \n%d %s</string>
You must apply this bold_name_experience_text in your text view label. These annotation class spans get added to your string and then you can iterate on them to apply the bold span.
You can refer to my SO answer which shows the Kotlin code to iterate through these spans and apply the bold span
Remember all the above answers has one of the following flows:
They are using some hard-coded index logic which may crash or give wrong results in some other language
They are using hardcode string in Java code which will result in lots of complicated logic to maintain internalisation
Some used Html.fromHtml which can be acceptable answer depending on the use-case. As Html.fromHtml doesn't always work for all types of HTML attributes for example there is not support of click span. Also depending on OEM you might get different rendered TextView

Can I change TextView link text after using Linkify?

Is is possible to change TextView text after using Linkify to create links? I have something where I want the url to have two fields, a name and id, but then I just want the text to display the name.
So I start off with a textview with text that includes both name and id, and linkify to create the appropriate links with both fields. But for the display, I don't want to show the id.
Is this possible?
It's kind of a pain but yes. So Linkify basically does a few things. First it scans the contents of the textview for strings that match that of a url. Next it creates UrlSpan's and ForegroundColorSpan's for those sections that match it. Then it sets the MovementMethod of the TextView.
The important part here are the UrlSpan's. If you take your TextView and call getText(), notice it returns a CharSequence. It's most likely some sort of Spanned. From the Spanned you can ask, getSpans() and specifcally the UrlSpans. Once you know all those spans you can than loop through the list and find and replace the old span objects with your new span objects.
mTextView.setText(someString, TextView.BufferType.SPANNABLE);
if(Linkify.addLinks(mTextView, Linkify.ALL)) {
//You can use a SpannableStringBuilder here if you are going to
// manipulate the displayable text too. However if not this should be fine.
Spannable spannable = (Spannable) mTextView.getText();
// Now we go through all the urls that were setup and recreate them with
// with the custom data on the url.
URLSpan[] spans = spannable.getSpans(0, spannable.length, URLSpan.class);
for (URLSpan span : spans) {
// If you do manipulate the displayable text, like by removing the id
// from it or what not, be sure to keep track of the start and ends
// because they will obviously change.
// In which case you may have to update the ForegroundColorSpan's as well
// depending on the flags used
int start = spannable.getSpanStart(span);
int end = spannable.getSpanEnd(span);
int flags = spannable.getSpanFlags(span);
spannable.removeSpan(span);
// Create your new real url with the parameter you want on it.
URLSpan myUrlSpan = new URLSpan(Uri.parse(span.getUrl).addQueryParam("foo", "bar");
spannable.setSpan(myUrlSpan, start, end, flags);
}
mTextView.setText(spannable);
}
Hopefully that makes sense. Linkify is just a nice tool to setup the correct Spans. Spans just get interpreted when rendering text.
Greg's answer doesn't really answer the original question. But it does contain some insight as to where to start. Here's a function that you can use. It assumes that you have Linkified your textview prior to this call. It's in Kotlin, but you can get the gist of it if you are using Java.
In short, it builds a new Spannable with your new text. During the build, it copies over the url/flags of the URLSpans that the Linkify call created previously.
fun TextView.replaceLinkedText(pattern: String) { // whatever pattern you used to Linkify textview
if(this.text !is Spannable) return // no need to process since there are no URLSpans
val pattern = Pattern.compile(pattern, Pattern.CASE_INSENSITIVE)
val matcher = pattern.matcher(this.text)
val linkifiedText = SpannableStringBuilder()
var cursorPos = 0
val spannable = this.text as Spannable
while (matcher.find()) {
linkifiedText.append(this.text.subSequence(cursorPos, matcher.start()))
cursorPos = matcher.end()
val span = spannable.getSpans(matcher.start(), matcher.end(), URLSpan::class.java).first()
val spanFlags = spannable.getSpanFlags(span)
val tag = matcher.group(2) // whatever you want to display
linkifiedText.append(tag)
linkifiedText.setSpan(URLSpan(span.url), linkifiedText.length - tag.length, linkifiedText.length, spanFlags)
}
this.text = linkifiedText
}

Categories

Resources