Question

I have a TextView that displays some HTML code (images included, ImageGetter). The html is not mine, but I can ask them to include custom scheme links, maybe even tags.

Purpose: to display some dynamically generated content without the need to play with nested Android layouts.
Problem: some links must be handled in the application (new Fragment loaded).
Can't use a receiver for action.VIEW, since it's an Activity Intent, not a broadcast, and its use will be very contextual, so only a programmatically registered receiver would do.

I'm usingtextView.setText(Html.fromHtml(content.content, imageGetter, null). need everything to remain the same, except some spans should have my own onClick on it. I'm not very familiar with Spanned, so I see these options:

  1. Edit the SpannableStringBuilder returned from Html.fromHtml, and replace the URLSpans I want with a custom ClickableSpan (how?)
  2. As above, but copy everything to a new builder, exchanging the URLSpans for my own (how? append takes a CharSequence only, and I get RelativeSizeSpan, StyleSpan, ImageSpan, URLSpan...)
  3. Create a Spanned manually. I can do it for the custom scheme, but how to duplicate the effect of Html.fromHtml (or close enough) for all else?

[edit] Thanks to MH. for the info. I had tried that before, but failed. Now that i returned to it, I found i had made an error, passing the wrong item to the 1st argument of setSpan.
If anyone's interested, i now use this:

public static interface OnSpanClickListener {
    void onClick(String url);
}
public static SpannableStringBuilder getSpannable(String source, ImageGetter imageGetter, String scheme, final OnSpanClickListener clickHandler){
    SpannableStringBuilder b = (SpannableStringBuilder) Html.fromHtml(source, imageGetter, null);
    for(URLSpan s : b.getSpans(0, b.length(), URLSpan.class)){
        String s_url = s.getURL();
        if(s_url.startsWith(scheme+"://")){
            URLSpan newSpan = new URLSpan(s_url.substring(scheme.length()+3)){
                public void onClick(View view) {
                    clickHandler.onClick(getURL());
                }
            };
            b.setSpan(newSpan, b.getSpanStart(s), b.getSpanEnd(s), b.getSpanFlags(s));
            b.removeSpan(s);
        }
    }
    return b;
}

(...)

body.setText(getSpannable(content.content, imageGetter, getResources().getString(R.string.poster_scheme), new OnSpanClickListener(){
public void onClick(String url) {
    // do whatever
}
}));
Was it helpful?

Solution

Option 1 is probably most straightforward and most of the hard work for it has already been done before. You've got the general idea correct: after the HTML has been processed, you can request all the generated URLSpan instances and loop through them. You can then replace it with a customized clickable span to get full controls over any of the span clicks.

In the example below, I'm just replacing every URLSpan with a simple extension of that class that takes the original url (actually, I should probably say 'uri') and replace its scheme part. I've left the actual onClick() logic unimplemented, but I'll leave that up to your imagination.

SpannableStringBuilder builder = ...
URLSpan[] spans = builder .getSpans(0, builder .length(), URLSpan.class);
for (URLSpan span : spans) {
    int start = builder .getSpanStart(span);
    int end = builder .getSpanEnd(span);
    s.removeSpan(span);
    span = new CustomURLSpan(span.getURL().replace("http://", "scheme://"));
    s.setSpan(span, start, end, 0);
}
textView.setText(builder);

As mentioned earlier, here the CustomURLSpan class is a simple extension of URLSpan that takes a url and overrides the onClick() method so our own logic can be executed there.

public class CustomURLSpan extends URLSpan {

    public CustomURLSpan(String url) {
        super(url);
    }

    @Override public void onClick(View widget) {
        // custom on click behaviour here
    }
}

Some related Q&A's that basically do a similar thing (might be helpful for some more inspiration):

OTHER TIPS

As you will use TextView.setMovementMethod(LinkMovementMethod.getInstance()) to make it respond for the partly click, you can implement a customized LinkMovementMethod to intercept the click event and process your own logic.

public class CustomLinkMovementMethod extends LinkMovementMethod {

    private HashMap<Class, SpanProxy> spansProxy = new HashMap<>();

    public CustomLinkMovementMethod setSpanProxy(Class spanClazz, SpanProxy proxy) {
        spansProxy.put(spanClazz, proxy);
        return this;
    }

    public CustomLinkMovementMethod removeSpanProxy(Class spanClazz) {
        spansProxy.remove(spanClazz);
        return this;
    }

    @Override
    public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) {

        int action = event.getAction();

        if (action == MotionEvent.ACTION_UP ||
                action == MotionEvent.ACTION_DOWN) {
            int x = (int) event.getX();
            int y = (int) event.getY();

            x -= widget.getTotalPaddingLeft();
            y -= widget.getTotalPaddingTop();

            x += widget.getScrollX();
            y += widget.getScrollY();

            Layout layout = widget.getLayout();
            int line = layout.getLineForVertical(y);
            int off = layout.getOffsetForHorizontal(line, x);

            ClickableSpan[] link = buffer.getSpans(off, off, ClickableSpan.class);
            if (link.length != 0) {
                if (action == MotionEvent.ACTION_UP) {
                    SpanProxy spanProxy = spansProxy.get(ClickableSpan.class);
                    if(spanProxy != null) {
                        spanProxy.proxySpan(link[0], widget);
                    } else {
                        link[0].onClick(widget);
                    }
                    return true;
                }
            }
        }
            return super.onTouchEvent(widget, buffer, event);
    }


    public interface SpanProxy {
        void proxySpan(Object span, View widget);
    }
}

And used like this:

textView.setMovementMethod(new CustomLinkMovementMethod().setSpanProxy(ClickableSpan.class,
        new CustomLinkMovementMethod.SpanProxy() {
            @Override
            public void proxySpan(Object span, View widget) {
                if (span instanceof URLSpan) {
                    URLSpan urlSpan = (URLSpan) span;
                    String url = urlSpan.getURL();

                    // do what you want with the link
                    // tbd
                    return;
                }

                if (span instanceof ClickableSpan) {
                    ((ClickableSpan) span).onClick(widget);
                }
            }
        }));
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top