Lollipop 上的 TextView fromHtml 链接断开

TextView fromHtml links broken on Lollipop

我们的应用程序有几个 TextViews 实例,其内容由 myTv.setText(Html.fromHtml()); 设置,这些实例一直在 Android 4.4.0 及以下版本中工作。

从 4.4.2 和 Lollypop 开始,这些链接已停止工作。文本仍然带有下划线和超链接颜色,但点击它们没有任何结果。

不得不说,这些字段被标记为可复制粘贴,众所周知,这与那些 spannables 有交互。

有没有人能解决这个问题?

解决方案

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

有了这个,您可以通过长按选择您的文本,并且可以通过单击打开 link。您也可以进行更高级的实现,以允许双击文本选择。

问题

问题似乎出在以下代码中。它是默认 TextViewonTouchEvent:

的一部分
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;
    }
}

我们可以看出if块为运行的条件之一是mAutoLinkMask != 0

如果您将 android:autoLink 更改为默认值 0 或 none 以外的其他内容,if 块 运行s 但没有 links。

我想这被开发人员遗漏了,我认为应该将其呈现为错误。

旁注

如果您只需要像 http://google.com 这样的简单 link 而不改变它们的外观,您可以在 TextView 上设置 android:autoLink="web" 并完成。 link 将自动找到并可点击。不过,这不适用于 <a href="..."></a> 类型的 links。

问题是在 TextView 中启用复制和粘贴时,Android 将使用支持 select 文本但不支持点击链接的 ArrowKeyMovementMethod。当您使用 LinkMovementMethod 时,您可以单击链接但不能单击 select 文本(无论您使用的是 Lollipop、KitKat 还是更低的 Android 版本)。

为了解决这个问题,我扩展了 ArrayKeyMovementMethod class 并用 LinkMovementMethod onTouchEvent 覆盖了 onTouchEvent。为了允许文本 selection,我不得不删除三行代码。由于我在具有大量文本格式的富文本编辑器中使用 class,因此我还添加了逻辑来查找单击的字符,而不管文本大小、缩进或文本对齐方式如何。如果您想要适用于纯文本的简单解决方案,请在自定义 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);
}

别忘了打电话: myTv.setMovementMethod(新的 ClickAndSelectMovementMethod());

如果您想要支持各种文本格式的版本,请改用此版本:

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

试试这个,对我来说效果很好。我根据需要从我的资产文件夹加载 html 文件,但您只需要设置 textview 的所有可链接属性,仅此而已。它会起作用...

<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" />

并在代码中尝试像这样设置为 html

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

默认情况下,Material ButtonsTextviews 的样式显示为全部大写。但是,AllCapsTransformationMethod 中存在一个错误,会导致丢弃其他文本格式,例如。 Spannable。 因此,当您尝试在 Lollipop 上更改 Button 的字体大小时,如下所示:

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

它也不起作用(仅适用于 Lollipop)。

解决方法:

针对您的案例和描述的 Spannable 案例的解决方法是将 textAllCaps 设置为 false:

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