I am trying to place html into a TextView. Everything works perfectly, this is my code.
String htmlTxt = "<p>Hellllo</p>"; // the html is form an API
Spanned html = Html.fromHtml(htmlTxt);
myTextView.setText(html);
This sets my TextView with the correct html. But my problem is, having a tag in the html, the result text that goes into the TextView has a "\n" at the end, so it pushes my TextView's height higher than it should be.
Since its a Spanned variable, I can't apply regex replace to remove the "\n", and if I was to convert it into a string, then apply regex, I lose the functionality of having html anchors to work properly.
Does anyone know any solutions to remove the ending linebreak(s) from a "Spanned" variable?
Nice answer #Christine. I wrote a similar function to remove trailing whitespace from a CharSequence this afternoon:
/** Trims trailing whitespace. Removes any of these characters:
* 0009, HORIZONTAL TABULATION
* 000A, LINE FEED
* 000B, VERTICAL TABULATION
* 000C, FORM FEED
* 000D, CARRIAGE RETURN
* 001C, FILE SEPARATOR
* 001D, GROUP SEPARATOR
* 001E, RECORD SEPARATOR
* 001F, UNIT SEPARATOR
* #return "" if source is null, otherwise string with all trailing whitespace removed
*/
public static CharSequence trimTrailingWhitespace(CharSequence source) {
if(source == null)
return "";
int i = source.length();
// loop back to the first non-whitespace character
while(--i >= 0 && Character.isWhitespace(source.charAt(i))) {
}
return source.subSequence(0, i+1);
}
The spannable is a CharSequence, which you can manipulate.
This works:
myTextView.setText(noTrailingwhiteLines(html));
private CharSequence noTrailingwhiteLines(CharSequence text) {
while (text.charAt(text.length() - 1) == '\n') {
text = text.subSequence(0, text.length() - 1);
}
return text;
}
You can try this:
Spanned htmlDescription = Html.fromHtml(textWithHtml);
String descriptionWithOutExtraSpace = new String(htmlDescription.toString()).trim();
textView.setText(htmlDescription.subSequence(0, descriptionWithOutExtraSpace.length()));
you can use this lines ... totally works ;)
i know your problem solved but maybe some one find this useful .
try{
string= replceLast(string,"<p dir=\"ltr\">", "");
string=replceLast(string,"</p>", "");
}catch (Exception e) {}
and here is replaceLast ...
public String replceLast(String yourString, String frist,String second)
{
StringBuilder b = new StringBuilder(yourString);
b.replace(yourString.lastIndexOf(frist), yourString.lastIndexOf(frist)+frist.length(),second );
return b.toString();
}
Related
I woduld like to make all of links in textview clickable.
The example text is:
"All three should link out http://google.com and here link and http://google.com"
If I use MovementMethod with the html text, only second and third link is clickable.
If I use Linkify(or mix both) only first and second link is clickable.
How can I make all of them clickable?
After invesigation I found that Linkify.addLinks() method remove current spans from text and apply new once (based on eg web page url). Because of that my spans from Html.fromHtml() was deleted at the beginning and never applay again.
So I did following:
1. Read thext from htmml Html.fromHtml which gives me Spanned obj with html spans.
2. Save spans from html in array
3. Make linkify.addLinks - this method remove my old spans so I will have to add it back
4. Add old spans
5. Set text to the textview.
Implementation:
private void setLabel(){
label.setText(linkifyHTML(Html.fromHtml("text with links here"));
label.setMovementMethod(LinkMovementMethod.getInstance());
label.setLinkTextColor(getRes().getColor(R.color.link));
}
private Spannable linkifyHTML(CharSequence text) {
Spannable s = new SpannableString(text);
URLSpan[] old = s.getSpans(0, s.length(), URLSpan.class);
LinkSpec oldLinks[] = new LinkSpec[old.length];
for (int i = 0; i < old.length; i++) {
oldLinks[i] = new LinkSpec(old[i], s.getSpanStart(old[i]), s.getSpanEnd(old[i]));
}
Linkify.addLinks(s, Linkify.ALL);
for (LinkSpec span : oldLinks) {
s.setSpan(span.span, span.start, span.end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
return s;
}
class LinkSpec {
final URLSpan span;
final int start, end;
public LinkSpec(URLSpan urlSpan, int spanStart, int spanEnd) {
span = urlSpan;
start = spanStart;
end = spanEnd;
}
}
You have to use the backslash \ to scape " character so the string will not consider it as the final point of the string. I mean, a string is considered when all the text is inside two "". You have to scape " characters in your url because if not the string will consider that it has to end when he find a new " character, in this case in your url.
"All three should link out http://google.com and here link and http://google.com"
Background
Suppose I use SpannableStringBuilder to append multiple stuff into it, and one of them is string that I format from the strings.xml file, which has a span inside:
SpannableStringBuilder stringBuilder = new SpannableStringBuilder ();
stringBuilder.append(...)...
final SpannableString span = new SpannableString(...);
span.setSpan(new BackgroundColorSpan(0xff990000), ...,...,Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
stringBuilder.append(getString(R.string.string_to_format, span));
stringBuilder.append(...)...
textView.setText(stringBuilder);
The problem
Sadly, formatting such a string removes the span itself, so in my case, there won't be any text with a background color.
This happens on the line of the "getString".
What I've tried
If I just append the span alone (without "getString"), it works fine.
I also tried to investigate Html.fromHtml, but it doesn't seem to support a background color for text anyway.
The question
Is it possible to format a string that has a span, yet still have the span within?
More specifically, the input is a string A from the strings.xml file, which only has a placeholder (no special HTML tags), and another string B that is supposed to replace the placeholder at runtime. The string B should have a highlight for a partial text of itself.
In my case, the highlighted text is a something to search for within string B.
OK, I've found an answer to my special end case, but I'd still like to know if there are better ways.
Here's what I did:
String stringToSearchAt=...
String query=...
int queryIdx = stringToSearchAt.toLowerCase().indexOf(query);
stringToSearchAt= stringToSearchAt.substring(0, queryIdx + query.length()) + "<bc/>" + stringToSearchAt.substring(queryIdx + query.length());
final String formattedStr=getString(..., stringToSearchAt);
stringBuilder.append(Html.fromHtml(formattedStr, null, new TagHandler() {
int start;
#Override
public void handleTag(final boolean opening, final String tag, Editable output, final XMLReader xmlReader) {
switch (tag) {
case "bc":
if (!opening)
start = output.length() - query.length();
break;
case "html":
if (!opening)
output.setSpan(new BackgroundColorSpan(0xff00bbaa), start, start + query.length(), 0);
}
}
}));
This is only good for my case, but in the case of general formatting, this won't suffice.
format a spanned string may be impossible, because it still use String.format() to format a String finilly, it's a Java API, and Span is Android API.
But I think you can use html string instead. Look at this document Styling with HTML markup.
for example:
String str = "Hi <strong><font color=\"#00bbaa\">%s</font></strong>, Welcome to <em><font color=\"#FF4081\">%s</font></em>";
String text = String.format(str, "Lucy", "Android");
Spanned spanned = Html.fromHtml(text);
// after Html.fromHtml(), you can still change the Span
SpannableString spannableString = new SpannableString(spanned);
spannableString.setSpan(new BackgroundColorSpan(0xff990000), 0, 2, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
textView.setText(spannableString);
the result
if you want to put the string in the string.xml, you may need to change '<' to '<', '%s' to '%1$s'.
<string name="from_offical">Hello <strong><font color="#00bbaa">%1$s</font></strong>, Welcome to <em><font color="#00bbaa">%2$s</font></em></string>
This is an old question, but I am posting a more general solution than the accepted answer.
Reference is made to the following string resources:
<string name="string_1"><b>Bolded</b> <abc>Not bolded</abc></string>
<string name="string_2"><font bgcolor="red">Background red</font> No background color.</string>
<string name="string_3">The <b>capital</b> of %1 is %2\n%2 is the capital of %1.</string>
Android stores string resource tags separately from strings. Tags will always be consumed when read into an app.
var s1 = getString(R.string.string_1)
var s2 = getText(R.string.string_1)
s1 placed in a TextView will show "Bolded Not bolded" while s2 in a TextView will show "Bolded Not bolded". The tag "<abc>" has no interpretation, so it is lost.
If the background color is known at compile time then we can do the following:
textView.text = getText(R.string.string_2)
will display:
Of some interest is that while the font tag is supported by the Android framework and the HTML translation class (Html .java), the Html.java implementation does not support the bgcolor attribute, so the following
var s4 = "<font bgcolor=\"red\">Background red</font> No background color."
textView.text = HtmlCompat.fromHtml(s4, FROM_HTML_MODE_LEGACY)
will not display the background color.
If the formatting is indeterminate at compile time, then we must do a little more work. Replacing string arguments with spanned text using getString(string_id, varargs) fails as the OP notes. What is an alternative?
One way is to read a string in with placeholders intact.
getString(R.string.string_3) will produce the string "The capital of %1 is %2\n%2 is the capital of %1.". We could then search for "%1", "%2", etc. and make the replacements with spanned text. In this case, the placeholder identifiers could be any unique set of characters.
It may be better, however, to use getText(R.string.string_3) which will interpret any HTML codes supported by the framework.
The following code shows hot to make substitutions of spanned text into string_3. The spanned text that will be substituted simply has the first letter highlighted.
textView.text = SpanFormatter.getText(this, R.string.string_3, { Int -> getArg(Int) })
private fun getArg(argNum: Int) =
when (argNum) {
1 -> { // Get the country with a highlighted first character.
SpannableString("France").apply {
setSpan(
BackgroundColorSpan(0x55FF0000),
0,
1,
SpannedString.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
}
2 -> { // Get the capital city with a highlighted first character.
SpannableString("Paris").apply {
setSpan(
BackgroundColorSpan(0x550000FF),
0,
1,
SpannedString.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
}
else -> throw IllegalArgumentException("$argNum is a bad argument number.")
}
SpanFormatter.kt
object SpanFormatter {
private const val END_OF_STRING = -1
private const val SPAN_FLAGS = SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE
fun getText(
context: Context,
#StringRes stringId: Int,
argFactory: (Int) -> CharSequence,
argStartChar: Char = '%'
) = getText(context.getText(stringId), argFactory, argStartChar)
fun getText(
cs: CharSequence,
argFactory: (Int) -> CharSequence,
argStartChar: Char = '%'
): CharSequence {
// Mark all areas of substitution with an ArgNum span.
val sb = SpannableStringBuilder(cs)
var pos = sb.indexOf(argStartChar, 0)
while (pos != END_OF_STRING) {
var argEnd = pos + 1
while (argEnd < sb.length && sb[argEnd].isDigit()) ++argEnd
if (argEnd - pos > 1) {
val argnum = sb.substring(pos + 1, argEnd).toInt()
check(argnum > 0) {
"Incorrect argument number (%d) which must greater than zero.\nString: %s".format(
argnum
)
}
sb.setSpan(ArgMark(argnum), pos, argEnd, SPAN_FLAGS)
}
pos = sb.indexOf(argStartChar, argEnd)
}
// Replace all ArgMark spans with the appropriate substitution text.
val argMarkSpans = sb.getSpans<ArgMark>(0, sb.length)
argMarkSpans.forEach { argMarkSpan ->
val start = sb.getSpanStart(argMarkSpan)
val end = sb.getSpanEnd(argMarkSpan)
sb.replace(start, end, argFactory(argMarkSpan.argNum))
sb.removeSpan(argMarkSpan)
}
return sb
}
private data class ArgMark(val argNum: Int)
}
The foregoing displays:
And a simpler way without the use of the marking spans which aren't really needed:
SpanFormatter.kt
object SpanFormatter {
private const val END_OF_STRING = -1
fun getText(
context: Context,
#StringRes stringId: Int,
argFactory: (Int) -> CharSequence,
argStartChar: Char = '%'
) = getText(context.getText(stringId), argFactory, argStartChar)
fun getText(
cs: CharSequence,
argFactory: (Int) -> CharSequence,
argStartChar: Char = '%'
): CharSequence {
val sb = SpannableStringBuilder(cs)
var argStart = sb.indexOf(argStartChar, 0)
while (argStart != END_OF_STRING) {
var argEnd = argStart + 1
while (argEnd < sb.length && sb[argEnd].isDigit()) ++argEnd
if (argEnd - argStart > 1) {
val argNum = sb.substring(argStart + 1, argEnd).toInt()
argFactory(argNum).apply {
sb.replace(argStart, argEnd, this)
argEnd = argStart + length
}
}
argStart = sb.indexOf(argStartChar, argEnd)
}
return sb
}
}
In my android app's textview, I want to ellipsis all URLs (which already have been linked using clickable span) to get ellipsis (or truncate) if the length of URL is greater than a certain limit.
This behaviour is inspired from twitter and facebook.
For example, the link http://www.getfluttr.com/flap/3rL7/now-only-if-modi-would-listen-to-opposition-party-/ it should look something like this:
(Screenshot Source: Twitter)
I understand that this has to involve spans. I can't seem to be able to find a span that'll allow me to replace text while keeping link.
the following code will do the trick for you
public class LinkShortener {
public static final int MAX_LINK_LENGTH = 20;
public static CharSequence shortenLinks(String text) {
return shortenLinks(text, Linkify.ALL);
}
public static CharSequence shortenLinks(String text, int linkMask) {
SpannableStringBuilder builder = new SpannableStringBuilder(text);
Linkify.addLinks(builder, linkMask);
URLSpan[] spans = builder.getSpans(0, builder.length(), URLSpan.class);
for (URLSpan span : spans) {
int start = builder.getSpanStart(span);
int end = builder.getSpanEnd(span);
int flags = builder.getSpanFlags(span);
CharSequence linkText = builder.subSequence(start, end);
if (linkText.length() > MAX_LINK_LENGTH) {
linkText = linkText.subSequence(0, 20) + "…";
builder.replace(start, end, linkText);
builder.removeSpan(span);
builder.setSpan(span, start, start+linkText.length(), flags);
}
}
return builder;
}
}
Then you can simply use it like this:
itemView.setText(LinkShortener.shortenLinks("https://example.com/really_long_url"));
You might need to disable autoLink on the text view
The code first linkifies the text using the build in Android tooling. Then it goes through all creates URLSpans, and shortens the text via replace. Lastly we change the span to make sure it has the right bounds. Since we are reusing the existing span, the URL will be preserved
I added some "pretty" updates to #Filip Wieladek answer based on Twitter behaviour.
The provided code first removes the http(s)://www. prefixes providing up to 12 characters for more usefull information. It also perfectly matches the question desired behaviour. Example:
https://www.stackove... (20 chars)
stackoverflow.com/que... (20 chars without prefix)
Code
public static CharSequence shortenLinks(String text, int linkMask) {
SpannableStringBuilder builder = new SpannableStringBuilder(text);
Linkify.addLinks(builder, linkMask);
URLSpan[] spans = builder.getSpans(0, builder.length(), URLSpan.class);
for (URLSpan span : spans) {
int start = builder.getSpanStart(span);
int end = builder.getSpanEnd(span);
int flags = builder.getSpanFlags(span);
CharSequence linkText = builder.subSequence(start, end);
if (linkText.length() > MAX_LINK_LENGTH) {
// 1 - Remove the https:// or http:// prefix
if(linkText.toString().toLowerCase().startsWith("https://"))
linkText = linkText.subSequence("https://".length(), linkText.length());
else if(linkText.toString().toLowerCase().startsWith("http://"))
linkText = linkText.subSequence("http://".length(), linkText.length());
// 2 - Remove the www. prefix
if(linkText.toString().toLowerCase().startsWith("www."))
linkText = linkText.subSequence("www.".length(), linkText.length());
// 3 - Truncate if still longer than MAX_LINK_LENGTH
if (linkText.length() > MAX_LINK_LENGTH) {
linkText = linkText.subSequence(0, MAX_LINK_LENGTH) + "…";
}
// 4 - Replace the text preserving the spans
builder.replace(start, end, linkText);
builder.removeSpan(span);
builder.setSpan(span, start, start+linkText.length(), flags);
}
}
return builder;
}
spanned = Html.fromHtml("<sup>aaa</sup>bbb<sub>ccc</sub><b>ddd</b>");
Will create a Spanned object with with 3 spans aaa, ccc, ddd.
bbb is being ignored since it's not inside an html tag,
spans = spanned.getSpans(0, spanned.length(), Object.class);
will only identify 3 spans.
I need a way to extract all the 4 sections of the code, if possible into some sort of an array that will allow to me to identify the the type of each span.
I need a way to extract all the 4 sections of the code
Use nextSpanTransition() to find the starting point of the next span. The characters between your initial position (first parameter to nextSpanTransition()) and the next span represent an unspanned portion of text.
You can take a look at the source code to the toHtml() method on the Html class to see this in action.
'bbb' is the one which is not inside html tag. Even though i guess it will not be missed. 'ccc' is a subscript, may be it is rendered but not visible to you. Try to increase textview height if you have constrained it.
use this http://developer.android.com/reference/android/text/Html.html#fromHtml(java.lang.String, android.text.Html.ImageGetter, android.text.Html.TagHandler), pass null for ImageGetter and your custom TagHandler
see the example
String source = "<b>bold</b> <i>italic</i> <unk>unknown</unk>";
TagHandler tagHandler = new TagHandler() {
Stack<Integer> starts = new Stack<Integer>();
#Override
public void handleTag(boolean opening, String tag, Editable output, XMLReader xmlReader) {
if (tag.equals("unk")) {
if (opening) {
starts.add(output.length());
} else {
int start = starts.pop();
int end = output.length();
Object what = new Object();
output.setSpan(what, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
}
};
Spanned spanned = Html.fromHtml(source, null, tagHandler);
TextUtils.dumpSpans(spanned, new LogPrinter(Log.DEBUG, TAG), "span ");
All,
I have a database that will store an HTML tagged text to retain formatting information from an EditText. I create this string using HTML.toHtml(EditText.getText). I notice this method wraps whatever Spanned Text is put in it with <p> and </p>. The issue with that is when I got to use the method HTML.fromHtml(HTMLFormattedString) and then use the setText method of either a TextView or EditText there are two extra lines at the end of my actual text, which makes sense because that is how the paragraph tag works with HTML.
My question is is there anyway to make the textView or EditText shrink to not display the extra blank lines? What is the simplest way to do this? I have experimented with just removing the last <p> and </p>, but that only works if the user did not enter 3 or more new lines with the return key.
I ended up searching for white space at the end of the spanned text that was created and removed it. This took care of extra spaces due to the <p> </p> and was less time consuming than overriding the mentioned class to achieve the same results.
public SpannableStringBuilder trimTrailingWhitespace(
SpannableStringBuilder spannableString) {
if (spannableString == null)
return new SpannableStringBuilder("");
int i = spannableString.length();
// loop back to the first non-whitespace character
while (--i >= 0 && Character.isWhitespace(spannableString.charAt(i))) {
}
return new SpannableStringBuilder(spannableString.subSequence(0, i + 1));
}
Well this is just a round about approach. I had the same issue. And you are provided with two options,
1)As you said that paragraph tag works the way what you have suspected. What it does , it appends two "\n" values to the end of each <\p> tag. So you can convert the html to string and remove the last two characters which are usually two "\n"s
or
2) You have get into the Html Class itself. That is, you have to override the HTML class and look for handleP(SpannableStringBuilder text) and change its core logic a little bit.
private static void handleP(SpannableStringBuilder text) {
int len = text.length();
if (len >= 1 && text.charAt(len - 1) == '\n') {
if (len >= 2 && text.charAt(len - 2) == '\n') {
return;
}
text.append("\n");
return;
}
if (len != 0) {
text.append("\n\n");
}
}
As you can see here, it appends two "\n" in len!=0 which is were you have to do the change.