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。您也可以进行更高级的实现,以允许双击文本选择。
问题
问题似乎出在以下代码中。它是默认 TextView
的 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;
}
}
我们可以看出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 Buttons
和 Textviews
的样式显示为全部大写。但是,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" />
我们的应用程序有几个 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。您也可以进行更高级的实现,以允许双击文本选择。
问题
问题似乎出在以下代码中。它是默认 TextView
的 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;
}
}
我们可以看出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 Buttons
和 Textviews
的样式显示为全部大写。但是,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" />