I'm working on creating my own MarkdownTextView.
In this particular instance, I'm sifting through a body of text parsing out italic tags example:
Here is text, and *here is italic text*, and maybe *more* italic text
I have a regex function that does the sifting for me:
(\*[^*])(.*?)([^*]\*)
Below is the code that I am using to replace all of the italic snippets:
val commentBody = "Here is text, and *here is italic text*, and maybe *more* italic text"
val check = "(\*[^*])(.*?)([^*]\*)".toRegex()
val newSpan = SpannableString(commentBody.replace(check, { result ->
val innerSpan = SpannableString(result.value.substring(1, result.value.length - 1))
innerSpan.setSpan(StyleSpan(Typeface.ITALIC), 0, innerSpan.length, spanFlag)
return#replace innerSpan
}))
My regex is working properly, and
Here is text, and *here is italic text*, and maybe *more* italic text
is correctly converted to show
Here is text, and here is italic text, and maybe more italic text
But nothing is italicized. I debugged this, and it confirms my fear that when setting the italics span inside of that transform, and using that to setup my new spannable string, that I'm losing all of those spans.
Any ideas?
In case anyone is interested this is my answer:
val commentBody = "Here is text, and *here is italic text*, and maybe *more* italic text"
val check = "(\\*[^*])(.*?)([^*]\\*)".toRegex()
val spannableStringBuilder = SpannableStringBuilder(commentBody.replace("*", ""))
check.findAll(commentBody).map { it.value.substring(1, it.value.length - 1) }.forEach {
val start = spannableStringBuilder.indexOf(it)
val end = start + it.length
spannableStringBuilder.setSpan(StyleSpan(Typeface.ITALIC), 0, innerSpan.length, spanFlag)
}
return spannableStringBuilder
If you have a look at String.replace, you'll find, that the SpannableString you return is actually used as a CharSequence for transform: (MatchResult) -> CharSequence.
Also you're creating the outer SpannableString from a pure String only.
Thus you never had the information available. You could have a look into SpannableStringBuilder and use it with a different approach like using a tokenizer or a custom Markdown span which removes the markup syntax on the fly.
Related
This is the code that I have.
val s = SpannableString("hello")
s.setSpan(StyleSpan(Typeface.BOLD), 0, s.length, Spannable.SPAN_INCLUSIVE_EXCLUSIVE)
textView.text = TextUtils.concat(s, s)
The text shown in textView is: hellohello
Why aren't both "hello" in bold?
Edit: I am actually trying to concatenate some SpannableStrings with some normal Strings, something like TextUtils.concat("A", s, "B", s, "C") (the result of which I expect to be AhelloBhelloC). Replacing SPAN_INCLUSIVE_EXCLUSIVE with SPAN_INCLUSIVE_INCLUSIVE solves the "hellohello" problem but not this one.
https://developer.android.com/reference/android/text/TextUtils#concat(java.lang.CharSequence...):
If there are paragraph spans in the source CharSequences that satisfy paragraph boundary requirements in the sources but would no longer satisfy them in the concatenated CharSequence, they may get extended in the resulting CharSequence or not retained.
While this isn't super clear (at least to me) I infer that paragraph boundaries are extended if they should be extended. They should be extended if they are INCLUSIVE boundaries meaning if you insert at the beginning you should use Spannable.SPAN_INCLUSIVE_EXCLUSIVE or Spannable.SPAN_INCLUSIVE_INCLUSIVE, if you append at the end, you should use Spannable.SPAN_EXCLUSIVE_INCLUSIVE or Spannable.SPAN_INCLUSIVE_INCLUSIVE.
So use Spannable.SPAN_INCLUSIVE_INCLUSIVE instead of Spannable.SPAN_INCLUSIVE_EXCLUSIVE (I tested it and it works).
With SPAN_EXCLUSIVE_INCLUSIVE:
With SPAN_INCLUSIVE_INCLUSIVE:
After some digging I found TextUtils is using SpannableStringBuilder.append which is using SpannableStringBuilder.replace (https://developer.android.com/reference/android/text/SpannableStringBuilder#replace(int,%20int,%20java.lang.CharSequence,%20int,%20int)) under the hood and the replace is considered an insert at the end. With SPAN_EXCLUSIVE_INCLUSIVE it would not expand the formatting while it would with SPAN_INCLUSIVE_INCLUSIVE:
If the source contains a span with Spanned#SPAN_PARAGRAPH flag, and it does not satisfy the paragraph boundary constraint, it is not retained.
I also checked the resulting span and printed the start/end of the StyleSpan. The first line with SPAN_EXCLUSIVE_INCLUSIVE the last line with SPAN_INCLUSIVE_INCLUSIVE:
01-14 12:44:11.218 5017 5017 E test : span start/end: 0/5
01-14 12:44:22.575 5079 5079 E test : span start/end: 0/10
If you append text to a Spannable it doesn't matter whether that text is just plain text or another Spannable. Spans in the original texts will either expand or not depending on the span's flags (Spannable.SPAN_xyz_INCLUSIVE vs Spannable.SPAN_xyz_EXCLUSIVE). That's why the bold formatting will either expand to the whole string with TextUtils.concat("A", s, "B", s, "C") (using Spannable.SPAN_xyz_INCLUSIVE) or not expand at all (using Spannable.SPAN_xyz_EXCLUSIVE).
Your attempt to append an already formatted Spannable won't work because a span can be used just once.
That's why this:
val s = SpannableStringBuilder("hellohello")
val span = StyleSpan(Typeface.BOLD)
s.setSpan(span, 0, 5, Spannable.SPAN_INCLUSIVE_EXCLUSIVE)
s.setSpan(span, 5, 10, Spannable.SPAN_INCLUSIVE_EXCLUSIVE)
will result in hellohello, while this:
val s = SpannableStringBuilder("hellohello")
val span1 = StyleSpan(Typeface.BOLD)
val span2 = StyleSpan(Typeface.BOLD)
s.setSpan(span1, 0, 5, Spannable.SPAN_INCLUSIVE_EXCLUSIVE)
s.setSpan(span2, 5, 10, Spannable.SPAN_INCLUSIVE_EXCLUSIVE)
will result in hellohello
This answers your question:
Why aren't both "hello" in bold?
Unfortunately your question isn't very specific on what you're actually trying to achieve. If it's simply to have a Spannable with AhelloBhelloC, then you now know how. If your goal is to have a generic solution that allows to append arbitrary Spannable with arbitrary spans and keep those spans (CharacterStyle and ParagraphStyle) then you'll have to find a way to clone arbitrary Spanned texts with arbitrary spans (note if s is a SpannableStringBuilder then s.subSequence(0, s.length) copies spans but doesn't clone them). So here's how I'd do it:
private fun Spannable.clone(): Spannable {
val clone = SpannableStringBuilder(toString())
for (span in getSpans(0, length, Any::class.java)) {
if (span is CharacterStyle || span is ParagraphStyle) {
val st = getSpanStart(span).coerceAtLeast(0)
val en = getSpanEnd(span).coerceAtMost(length)
val fl = getSpanFlags(span)
val clonedSpan = cloneSpan(span)
clone.setSpan(clonedSpan, st, en, fl)
}
}
return clone
}
private fun cloneSpan(span: Any): Any {
return when (span) {
is StyleSpan -> StyleSpan(span.style)
// more clone code to be written...
else -> span
}
}
Then you can do:
val s = SpannableString("hello")
s.setSpan(StyleSpan(Typeface.BOLD), 0, s.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
textView.text = TextUtils.concat(s, " not bold ", s.clone())
I ended up creating a helper property to make strings bold.
val String.bold: CharSequence
get() = buildSpannedString {
append(this#bold, StyleSpan(Typeface.BOLD), Spannable.SPAN_INCLUSIVE_EXCLUSIVE)
}
I still can't re-use Spannables but probably it's because they can't be re-used (as #Emanuel suggested). But with this new property, the calling code looks pretty clean.
TextUtils.concat("A", "hello".bold, "B", "hello".bold, "C")
prints AhelloBhelloC
I have this code
TextView text1 = (TextView) view.findViewById(R.layout.myLayout);
Spanned myBold = (Html.fromHtml("<b>Test<b>", Html.FROM_HTML_MODE_LEGACY));
If I do
text1.setText(myBold);
Then myBold is in bold,which is ok. But when I want to add a string more, like
text1.setText(myBold+"bla");
Then the whole TextView is not bold anymore. Why does the new String "bla" affect this?
Thanks.
Why does the new String "bla" affect this?
Because what you are really doing is:
text1.setText(myBold.toString() + "bla");
A String has no style information. A Spanned object does.
Use TextUtils.concat() instead:
text1.setText(TextUtils.concat(myBold, "bla"));
A better choice would be to use a Bold StyleSpan. In the next sample only the "hello" world will be set to bold by using such technique:
Java:
final SpannableString caption = new SpannableString("hello world");
// Set to bold from index 0 to the length of 'hello'
caption.setSpan(new StyleSpan(Typeface.BOLD), 0, "hello".length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
yourTextView.setText(caption);
Kotlin:
yourTextView.text = SpannableString("hello world").apply {
// Set to bold from index 0 to the length of 'hello'
setSpan(StyleSpan(Typeface.BOLD), 0, "hello".length, Spannable.SPAN_INCLUSIVE_EXCLUSIVE))
}
This would be a more optimal solution rather than using the Html.fromHtml technicque, as it doesn't have to go through the overhead of parsing/interpreting the HTML tags.
In addition, it allows you to combine more styles, sizes, etc, in the same SpannableString.
Formatted text appears in a edittext.But the string contains multiple styling .
Is it possible to get the styling used on a particular character in a string , using getTypeFace command.
The first question is how to you set that styling?
If your doing something like Html.fromHtml("hello") calling getTypeFace() will return 0 (TypeFace.NORMAL). For this I would say that you might need to make the parse yourself and create substrings according to the HTML tags you've found.
If you your using the TextView attributes for this - android:textStyle="bold"
You can call directly:
Log.d(TAG, "has typeface=${tv_typeface.typeface.style}")
Alternatively if you're using SpannableStrings to edit how your text should look
You can do something like:
//Set an italic style to the word "hello"
val spannableString = SpannableString("hello world")
spannableString.setSpan(StyleSpan(Typeface.ITALIC), 0, 5, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
tv_typeface.text = spannableString
//Get the style italic used
val spannedString = tv_typeface.text as SpannedString
val spans = spannedString.getSpans(0, tv_typeface.length(), StyleSpan::class.java)
for (span in spans) {
Log.d(TAG, "StyleSpan between: ${spannedString.getSpanStart(span)} and ${spannedString.getSpanEnd(span)} with style ${span.style}")
}
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);
}
I am developing an application in which there will be a search screen
where user can search for specific keywords and that keyword should be
highlighted. I have found Html.fromHtml method.
But I will like to know whether its the proper way of doing it or
not.
Please let me know your views on this.
Or far simpler than dealing with Spannables manually, since you didn't say that you want the background highlighted, just the text:
String styledText = "This is <font color='red'>simple</font>.";
textView.setText(Html.fromHtml(styledText), TextView.BufferType.SPANNABLE);
Using color value from xml resource:
int labelColor = getResources().getColor(R.color.label_color);
String сolorString = String.format("%X", labelColor).substring(2); // !!strip alpha value!!
Html.fromHtml(String.format("<font color=\"#%s\">text</font>", сolorString), TextView.BufferType.SPANNABLE);
This can be achieved using a Spannable String. You will need to import the following
import android.text.SpannableString;
import android.text.style.BackgroundColorSpan;
import android.text.style.StyleSpan;
And then you can change the background of the text using something like the following:
TextView text = (TextView) findViewById(R.id.text_login);
text.setText("");
text.append("Your text here");
Spannable sText = (Spannable) text.getText();
sText.setSpan(new BackgroundColorSpan(Color.RED), 1, 4, 0);
Where this will highlight the charecters at pos 1 - 4 with a red color. Hope this helps!
Alternative solution: Using a WebView instead. Html is easy to work with.
WebView webview = new WebView(this);
String summary = "<html><body>Sorry, <span style=\"background: red;\">Madonna</span> gave no results</body></html>";
webview.loadData(summary, "text/html", "utf-8");
String name = modelOrderList.get(position).getName(); //get name from List
String text = "<font color='#000000'>" + name + "</font>"; //set Black color of name
/* check API version, according to version call method of Html class */
if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.N) {
Log.d(TAG, "onBindViewHolder: if");
holder.textViewName.setText(context.getString(R.string._5687982) + " ");
holder.textViewName.append(Html.fromHtml(text));
} else {
Log.d(TAG, "onBindViewHolder: else");
holder.textViewName.setText("123456" + " "); //set text
holder.textViewName.append(Html.fromHtml(text, Html.FROM_HTML_MODE_LEGACY)); //append text into textView
}
font is deprecated use span instead Html.fromHtml("<span style=color:red>"+content+"</span>")
To make part of your text underlined and colored
in your strings.xml
<string name="text_with_colored_underline">put the text here and <u><font color="#your_hexa_color">the underlined colored part here<font><u></string>
then in the activity
yourTextView.setText(Html.fromHtml(getString(R.string.text_with_colored_underline)));
and for clickable links:
<string name="text_with_link"><![CDATA[<p>text before linktitle of link.<p>]]></string>
and in your activity:
yourTextView.setText(Html.fromHtml(getString(R.string.text_with_link)));
yourTextView.setMovementMethod(LinkMovementMethod.getInstance());
First Convert your string into HTML then convert it into spannable. do as suggest the following codes.
Spannable spannable = new SpannableString(Html.fromHtml(labelText));
spannable.setSpan(new ForegroundColorSpan(Color.parseColor(color)), spannable.toString().indexOf("•"), spannable.toString().lastIndexOf("•") + 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
textview.setText(Html.fromHtml("<font color='rgb'>"+text contain+"</font>"));
It will give the color exactly what you have made in html editor , just set the textview and concat it with the textview value. Android does not support span color, change it to font color in editor and you are all set to go.
Adding also Kotlin version with:
getting text from resources (strings.xml)
getting color from resources (colors.xml)
"fetching HEX" moved as extension
fun getMulticolorSpanned(): Spanned {
// Get text from resources
val text: String = getString(R.string.your_text_from_resources)
// Get color from resources and parse it to HEX (RGB) value
val warningHexColor = getHexFromColors(R.color.your_error_color)
// Use above string & color in HTML
val html = "<string>$text<span style=\"color:#$warningHexColor;\">*</span></string>"
// Parse HTML (base on API version)
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
Html.fromHtml(html, Html.FROM_HTML_MODE_LEGACY)
} else {
Html.fromHtml(html)
}
}
And Kotlin extension (with removing alpha):
fun Context.getHexFromColors(
colorRes: Int
): String {
val labelColor: Int = ContextCompat.getColor(this, colorRes)
return String.format("%X", labelColor).substring(2)
}
Demo