简体   繁体   中英

TextView fromHtml links broken on Lollipop

Our application had several instances of TextViews with its contents set by myTv.setText(Html.fromHtml()); that have been working for Android 4.4.0 and below.

Starting from 4.4.2 and Lollypop these links have stopped working. The text still appears underlined and with a hyperlink color, but tapping them yields no results.

It has to be said that those fields are marked as copy-pasteable, which is known to have interactions with those spannables.

Has anyone been able to solve this issue?

The problem is that when enabling copy&paste in a TextView, Android will use an ArrowKeyMovementMethod which supports selection of text but not clicking of links. When you use a LinkMovementMethod then you can click links but not select text (regardless whether you're on Lollipop, KitKat or a lower Android version).

In order so solve this I extended the ArrayKeyMovementMethod class and overrode the onTouchEvent with the LinkMovementMethod onTouchEvent. To allow text selection I had to remove but three lines of code. Since I'm using that class in a rich text editor with lots of text formatting I also added logic to find the clicked character regardless of the text size, its indentation or the text alignment. If you want the simple solution that works fine with plain text, use this in a custom ArrowKeyMovementMethod class:

@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) {
                link[0].onClick(widget);
            } else if (action == MotionEvent.ACTION_DOWN) {
                Selection.setSelection(buffer,
                        buffer.getSpanStart(link[0]),
                        buffer.getSpanEnd(link[0]));
            }

            return true;
        }
        /* These are the lines of code you want to remove
        else {
            Selection.removeSelection(buffer);
        }*/
    }

    return super.onTouchEvent(widget, buffer, event);
}

Don't forget to call: myTv.setMovementMethod(new ClickAndSelectMovementMethod());

If you want the version that supports all kinds of text formatting use this instead:

import android.graphics.Rect;
import android.text.Layout;
import android.text.Selection;
import android.text.Spannable;
import android.text.Spanned;
import android.text.TextPaint;
import android.text.method.ArrowKeyMovementMethod;
import android.text.method.MovementMethod;
import android.text.style.AbsoluteSizeSpan;
import android.text.style.ClickableSpan;
import android.text.style.LeadingMarginSpan;
import android.view.MotionEvent;
import android.widget.TextView;

/**
 * ArrowKeyMovementMethod does support selection of text but not the clicking of
 * links. LinkMovementMethod does support clicking of links but not the
 * selection of text. This class adds the link clicking to the
 * ArrowKeyMovementMethod. We basically take the LinkMovementMethod onTouchEvent
 * code and remove the line Selection.removeSelection(buffer); which de-selects
 * all text when no link was found.
 */
public class ClickAndSelectMovementMethod extends ArrowKeyMovementMethod {

    private static Rect sLineBounds = new Rect();

    @Override
    public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) {
        int action = event.getAction();

        if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) {

            int index = getCharIndexAt(widget, event);
            if (index != -1) {
                ClickableSpan[] link = buffer.getSpans(index, index, ClickableSpan.class);
                if (link.length != 0) {
                    if (action == MotionEvent.ACTION_UP) {
                        link[0].onClick(widget);
                    } else if (action == MotionEvent.ACTION_DOWN) {
                        Selection.setSelection(buffer, buffer.getSpanStart(link[0]), buffer.getSpanEnd(link[0]));
                    }
                    return true;
                }
            }
            /*
             * else { Selection.removeSelection(buffer); }
             */

        }

        return super.onTouchEvent(widget, buffer, event);
    }

    private int getCharIndexAt(TextView textView, MotionEvent event) {
        // get coordinates
        int x = (int) event.getX();
        int y = (int) event.getY();
        x -= textView.getTotalPaddingLeft();
        y -= textView.getTotalPaddingTop();
        x += textView.getScrollX();
        y += textView.getScrollY();

        /*
         * Fail-fast check of the line bound. If we're not within the line bound
         * no character was touched
         */
        Layout layout = textView.getLayout();
        int line = layout.getLineForVertical(y);
        synchronized (sLineBounds) {
            layout.getLineBounds(line, sLineBounds);
            if (!sLineBounds.contains(x, y)) {
                return -1;
            }
        }

        // retrieve line text
        Spanned text = (Spanned) textView.getText();
        int lineStart = layout.getLineStart(line);
        int lineEnd = layout.getLineEnd(line);
        int lineLength = lineEnd - lineStart;
        if (lineLength == 0) {
            return -1;
        }
        Spanned lineText = (Spanned) text.subSequence(lineStart, lineEnd);

        // compute leading margin and subtract it from the x coordinate
        int margin = 0;
        LeadingMarginSpan[] marginSpans = lineText.getSpans(0, lineLength, LeadingMarginSpan.class);
        if (marginSpans != null) {
            for (LeadingMarginSpan span : marginSpans) {
                margin += span.getLeadingMargin(true);
            }
        }
        x -= margin;

        // retrieve text widths
        float[] widths = new float[lineLength];
        TextPaint paint = textView.getPaint();
        paint.getTextWidths(lineText, 0, lineLength, widths);

        // scale text widths by relative font size (absolute size / default size)
        final float defaultSize = textView.getTextSize();
        float scaleFactor = 1f;
        AbsoluteSizeSpan[] absSpans = lineText.getSpans(0, lineLength, AbsoluteSizeSpan.class);
        if (absSpans != null) {
            for (AbsoluteSizeSpan span : absSpans) {
                int spanStart = lineText.getSpanStart(span);
                int spanEnd = lineText.getSpanEnd(span);
                scaleFactor = span.getSize() / defaultSize;
                int start = Math.max(lineStart, spanStart);
                int end = Math.min(lineEnd, spanEnd);
                for (int i = start; i < end; i++) {
                    widths[i] *= scaleFactor;
                }
            }
        }

        // find index of touched character
        float startChar = 0;
        float endChar = 0;
        for (int i = 0; i < lineLength; i++) {
            startChar = endChar;
            endChar += widths[i];
            if (endChar >= x) {
                // which "end" is closer to x, the start or the end of the character?
                int index = lineStart + (x - startChar < endChar - x ? i : i + 1);
                return index;
            }
        }

        return -1;
    }
}

By default, Material Buttons and Textviews are styled to show text in all-caps. However, there is a bug in the AllCapsTransformationMethod that causes discarding other text formatting eg. Spannable . So when you try change font size for Button on Lollipop like follow:

SpannableString span = new SpannableString(text);
span.setSpan(new AbsoluteSizeSpan(8, true), 5, 10, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
testButton.setText(span);

it also won't work (only for Lollipop).

Solution :

workaround for your case and described Spannable case is set textAllCaps to false :

<TextView
...
android:textAllCaps="false" />

Solution

final Spanned spanned = Html.fromHtml("<a href='http://google.com'>My link</a>");

textView.setText(spanned);
textView.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        ClickableSpan[] links = spanned.getSpans(0, spanned.length(), ClickableSpan.class);

        if (links.length > 0) {
            links[0].onClick(v);
        }
    }
});

With this your text can be selected with a long-click and the link can be opened with a single click. You can make fancier implementation to allow double-click text selection too.

Problem

The problem seems to reside in the following code. It's part of the default TextView 's onTouchEvent :

if (touchIsFinished && mLinksClickable && mAutoLinkMask != 0 && textIsSelectable) {
    // The LinkMovementMethod which should handle taps on links has not been installed
    // on non editable text that support text selection.
    // We reproduce its behavior here to open links for these.
    ClickableSpan[] links = ((Spannable) mText).getSpans(getSelectionStart(),
            getSelectionEnd(), ClickableSpan.class);

    if (links.length > 0) {
        links[0].onClick(this);
        handled = true;
    }
}

We can see that one of the conditions for the if block to be run is mAutoLinkMask != 0 .

If you change android:autoLink to be something other than the default 0 or none the if block runs but has no links.

I guess this was missed by the developers and should be rendered as a bug in my opinion.

Sidenote

If all you need are simple links like http://google.com without changing their appearance, you can set android:autoLink="web" on your TextView and be done. The links will be found automatically and made clickable. This won't work with <a href="..."></a> type of links though.

Try this, it worked fine for me. I load html file from my assets folder for my requirement but you just need to set all linkable properties of textview, that's it.. it will work...

<TextView
        android:id="@+id/txt_terms_and_conditions"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:text="@string/hello_world"
        android:linksClickable="true"
        android:autoLink="phone|email|web" />

and in code try setting as html like this

mTxtTearmsAndConditions.setText(Html.fromHtml(total.toString()));

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM