TextView with anchor and raw links (SpannableString issue) - android

In my textview, I want both anchors and raw links to be clickable. I'm letting TextView.setAutoLinkMask(Linkify.ALL) handle all of the raw links and then retrieving the spans and setting them with SpannableString for all of the anchors.
I would like to be able to 'linkify' the anchors in my custom TextView implementation (extends TextView) in the setText(CharSequence text, BufferType type) call. Everything seems to be working properly except for the setSpan() call (end, start, and span are all correct). At the end of setText(...), I'm assigning the global CharSequence var to the new SpannableString. When setText() is called from my adapter, the spans identified in Object[]spans are not linked.
#Override
public void setText(CharSequence text, BufferType type) {
Spanned html = Html.fromHtml(text.toString().replace("\n", "<br />"));
Object[] spans = html.getSpans(0, html.length(), URLSpan.class);
SpannableString s = new SpannableString(html);
for (int i = 0; i < spans.length; i++) {
URLSpan span = (URLSpan) spans[i];
int end = html.getSpanEnd(span);
int start = html.getSpanStart(span);
int flags = html.getSpanFlags(span);
Log.i(LOG_TAG, "span: " + span.getURL() + "\nstart: " + start + "\nend: " + end);
s.setSpan(span, start, end, flags);
}
mText = s;
}
I found a slight workaround to this issue in the code listed below. In this implementation, I'm setting the spans in the adapter instead of the CustomTextView. This method works fine but doesn't entirely fit my needs because my TextView is used as an ExpandableTextView, meaning that there are 2 sets of texts (trimmed and full) and often times the trimmed version is returned (which is fine in typical scenarios, except that I want to add tags to the full text). This implementation often crashes because the spans that were originally identified were for the fullText and getText() is returning the trimmedText. So I think it'll be necessary for me to be able to do this in my custom TextView's setText(). BUT, the below method does work granted that trimmedText and fullText are the same length.
Spanned html = Html.fromHtml(postText.replace("\n", "<br />"));
Object[] spans = html.getSpans(0, html.length(), URLSpan.class);
h.content.setAutoLinkMask(Linkify.ALL);
h.content.setMovementMethod(LinkMovementMethod.getInstance());
h.content.setText(html);
h.content.setLinkTextColor(Color.rgb(136, 194, 226));
h.content.setVisibility(View.VISIBLE); // Need this otherwise the view disappears....
SpannableString ss = (SpannableString) h.content.getText();
for (int i = 0; i < spans.length; i++) {
URLSpan span = (URLSpan) spans[i];
int end = html.getSpanEnd(span);
int start = html.getSpanStart(span);
int flags = html.getSpanFlags(span);
ss.setSpan(span, start, end, flags);
}

Here's how I solved my problem:
At the point of my problem, I had already parsed through the content and added anchors (HTML links that would launch an activity when clicked) to my tags (# and #). I also wanted to make raw (e.g., www.google.com) clickable, so I tried setting my TextView to setAutoLinkMask(Linkify.ALL) but this broke the anchor links.
So this is what I did:
SpannableStringBuilder ssb =
new SpannableStringBuilder(Html.fromHtml
(this.content.replace("\n", "<br />")));
Pattern urlPattern = android.util.Patterns.WEB_URL;
Linkify.addLinks(ssb, urlPattern, "youractivityhere://");
myTextView.setText(ssb);
Basically I'm using a regex matcher to find any URLs within the String and then add a link using Linkify.addLinks(...).
I was making the problem way more difficult than needed.

Related

How to make all 3 types of links in textview

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"

Android - Ellipsize & Truncate all long Urls in a Textview

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

Android - Breaking down a Spanned object

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 ");

Highlight multiple words in a string using SpannableString

I'm using a SpannableString to underline certain words, however, I realized the code I have only highlights the first word if there are multiple words. Not exactly sure how to accomplish highlighting multiple words:
String keyword = "test";
String text = "This is a test to underline the three test words in this test";
SpannableString output = new SpannableString(text);
if (text.indexOf(keyword) > -1)
{
int keywordIndex = text.indexOf(keyword);
int keywordLength = keyword.length();
int start = keywordIndex;
int end = keywordIndex + (keywordLength);
output.setSpan(new UnderlineSpan(), start, end, 0);
}
I was thinking I could split the text at every space and loop through it, but wasn't sure if there was a better way.
I do have this code to highlight multiple words using a regular expression, however, I'm try to avoid regular expressions since it's in an Android app and I'm using it in a ListView and I'm told they are very expensive. Also this code I have only highlight whole words, so using the example text above, if the word "protest" was in the sentence, it wouldn't get highlighted using this code:
Matcher matcher = Pattern.compile("\\b(?:test")\\b").matcher(text);
while (matcher.find())
{
output.setSpan(new UnderlineSpan(), matcher.start(), matcher.end(), 0);
}

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