TextView 可以选择并包含链接吗?
Posted
技术标签:
【中文标题】TextView 可以选择并包含链接吗?【英文标题】:Can a TextView be selectable AND contain links? 【发布时间】:2013-04-05 14:19:41 【问题描述】:我遇到了TextView
的问题。我可以使用setTextIsSelectable(true)
使其可选择,但是当我通过setMovementMethod(LinkMovementMethod.getInstance())
启用要单击的链接时,它不再可选择。
请注意,我的意思不是让原始链接可点击,而是通过使用类似 setText(html.fromHtml("<a href='http://***.com'>Hello World!</a>"))
的 HTML 标记加载 TextView
来使实际单词可点击。
【问题讨论】:
你用 Linkify 试过了吗? 根据文档,Linkify
仅适用于带有 URL 方案前缀的文本,不适用于您想要转换为链接的任意文本。
我认为你必须创建自定义 TextView
是否可以继承 LinkMovementMethod 并对其进行修改以允许选择?
TextView that is linkified and selectable?的可能重复
【参考方案1】:
oakes's 回答导致在 textview 上双击时出现异常
java.lang.IndexOutOfBoundsException: setSpan (-1 ... -1) 开始于 0...
我查看了 LinkMovementMethod 中的 onTouchEvent 实现,发现当 textview 不包含链接时它会删除选择。在这种情况下,选择从空值开始,当用户尝试更改它时应用程序崩溃。
...
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);
...
所以我重写了 onTouchEvent 方法,它工作正常。
public class CustomMovementMethod extends LinkMovementMethod
@Override
public boolean canSelectArbitrarily ()
return true;
@Override
public void initialize(TextView widget, Spannable text)
Selection.setSelection(text, text.length());
@Override
public void onTakeFocus(TextView view, Spannable text, int dir)
if ((dir & (View.FOCUS_FORWARD | View.FOCUS_DOWN)) != 0)
if (view.getLayout() == null)
// This shouldn't be null, but do something sensible if it is.
Selection.setSelection(text, text.length());
else
Selection.setSelection(text, text.length());
@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;
return Touch.onTouchEvent(widget, buffer, event);
希望对某人有所帮助。
【讨论】:
这个解决方案对我有用,而不是下一个!非常感谢您的分享-您拯救了我的一天!在 2 台设备上测试。【参考方案2】:我想通了。您需要继承 LinkMovementMethod 并添加对文本选择的支持。很遗憾它本身不支持它。我只是使用source code 中的等效方法为ArrowKeyMovementMethod
覆盖了相关方法。我想这是 android 开源的好处之一!
public class CustomMovementMethod extends LinkMovementMethod
@Override
public boolean canSelectArbitrarily ()
return true;
@Override
public void initialize(TextView widget, Spannable text)
Selection.setSelection(text, text.length());
@Override
public void onTakeFocus(TextView view, Spannable text, int dir)
if ((dir & (View.FOCUS_FORWARD | View.FOCUS_DOWN)) != 0)
if (view.getLayout() == null)
// This shouldn't be null, but do something sensible if it is.
Selection.setSelection(text, text.length());
else
Selection.setSelection(text, text.length());
要使用它,只需直接实例化它,如下所示:
textView.setMovementMethod(new CustomMovementMethod());
【讨论】:
不错!非常感谢。 老兄,你太棒了!我开始认为这是不可能的,但后来我偶然发现了你的问题。一定要喜欢开源! 对我们来说这是可行的,但我们必须将 textIsSelectable 设置为 true 仅供参考,在某些情况下,此解决方案有时会导致“java.lang.IndexOutOfBoundsException: setSpan (-1 ... -1) 在 0 之前开始”,例如在 Nexus 5 设备上双击时。 完美!你拯救了我的一天。【参考方案3】:TL;DR:只需在此答案末尾使用 LinkArrowKeyMovementMethod
即可获得完美解决方案。
如果您曾经尝试使用扩展 LinkMovementMethod
的投票最多的答案,则会出现一个烦人的错误 - 当您通过单击一些未选择的文本取消选择时,整个选择会从一开始就闪烁到选择结束,然后什么都没有。这是因为 LinkMovementMethod
实际上无法像 ArrowKeyMovementMethod
那样处理选择。
如果您已将android:autoLink
设置为true
,则另一种方法可能是使用TextView
自己的解决方法,如TextView
的以下来源:
final boolean textIsSelectable = isTextSelectable();
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;
但我个人不想要自动链接功能(我有我自己的链接信息),所以在@Weidian Huang 的想法的基础上,我将LinkMovementMethod
的功能合并到ArrowKeyMovementMethod
中并构建了一个新的运动方法:
/**
* @see LinkMovementMethod
* @see ArrowKeyMovementMethod
*/
public class LinkArrowKeyMovementMethod extends ArrowKeyMovementMethod
private static final int CLICK = 1;
private static final int UP = 2;
private static final int DOWN = 3;
private static Object FROM_BELOW = new NoCopySpan.Concrete();
private static LinkArrowKeyMovementMethod sInstance;
public static LinkArrowKeyMovementMethod getInstance()
if (sInstance == null)
sInstance = new LinkArrowKeyMovementMethod();
return sInstance;
@Override
public void initialize(TextView widget, Spannable text)
super.initialize(widget, text);
text.removeSpan(FROM_BELOW);
@Override
public void onTakeFocus(TextView view, Spannable text, int dir)
super.onTakeFocus(view, text, dir);
if ((dir & View.FOCUS_BACKWARD) != 0)
text.setSpan(FROM_BELOW, 0, 0, Spannable.SPAN_POINT_POINT);
else
text.removeSpan(FROM_BELOW);
@Override
protected boolean handleMovementKey(TextView widget, Spannable buffer, int keyCode,
int movementMetaState, KeyEvent event)
switch (keyCode)
case KeyEvent.KEYCODE_DPAD_CENTER:
case KeyEvent.KEYCODE_ENTER:
if (KeyEvent.metaStateHasNoModifiers(movementMetaState))
if (event.getAction() == KeyEvent.ACTION_DOWN &&
event.getRepeatCount() == 0 && action(CLICK, widget, buffer))
return true;
break;
return super.handleMovementKey(widget, buffer, keyCode, movementMetaState, event);
@Override
protected boolean up(TextView widget, Spannable buffer)
if (action(UP, widget, buffer))
return true;
return super.up(widget, buffer);
@Override
protected boolean down(TextView widget, Spannable buffer)
if (action(DOWN, widget, buffer))
return true;
return super.down(widget, buffer);
@Override
protected boolean left(TextView widget, Spannable buffer)
if (action(UP, widget, buffer))
return true;
return super.left(widget, buffer);
@Override
protected boolean right(TextView widget, Spannable buffer)
if (action(DOWN, widget, buffer))
return true;
return super.right(widget, buffer);
private boolean action(int what, TextView widget, Spannable buffer)
Layout layout = widget.getLayout();
int padding = widget.getTotalPaddingTop() +
widget.getTotalPaddingBottom();
int areaTop = widget.getScrollY();
int areaBot = areaTop + widget.getHeight() - padding;
int lineTop = layout.getLineForVertical(areaTop);
int lineBot = layout.getLineForVertical(areaBot);
int first = layout.getLineStart(lineTop);
int last = layout.getLineEnd(lineBot);
ClickableSpan[] candidates = buffer.getSpans(first, last, ClickableSpan.class);
int a = Selection.getSelectionStart(buffer);
int b = Selection.getSelectionEnd(buffer);
int selStart = Math.min(a, b);
int selEnd = Math.max(a, b);
if (selStart < 0)
if (buffer.getSpanStart(FROM_BELOW) >= 0)
selStart = selEnd = buffer.length();
if (selStart > last)
selStart = selEnd = Integer.MAX_VALUE;
if (selEnd < first)
selStart = selEnd = -1;
switch (what)
case CLICK:
if (selStart == selEnd)
return false;
ClickableSpan[] link = buffer.getSpans(selStart, selEnd, ClickableSpan.class);
if (link.length != 1)
return false;
link[0].onClick(widget);
break;
case UP:
int bestStart, bestEnd;
bestStart = -1;
bestEnd = -1;
for (int i = 0; i < candidates.length; i++)
int end = buffer.getSpanEnd(candidates[i]);
if (end < selEnd || selStart == selEnd)
if (end > bestEnd)
bestStart = buffer.getSpanStart(candidates[i]);
bestEnd = end;
if (bestStart >= 0)
Selection.setSelection(buffer, bestEnd, bestStart);
return true;
break;
case DOWN:
bestStart = Integer.MAX_VALUE;
bestEnd = Integer.MAX_VALUE;
for (int i = 0; i < candidates.length; i++)
int start = buffer.getSpanStart(candidates[i]);
if (start > selStart || selStart == selEnd)
if (start < bestStart)
bestStart = start;
bestEnd = buffer.getSpanEnd(candidates[i]);
if (bestEnd < Integer.MAX_VALUE)
Selection.setSelection(buffer, bestStart, bestEnd);
return true;
break;
return false;
@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[] links = buffer.getSpans(off, off, ClickableSpan.class);
if (links.length != 0)
if (action == MotionEvent.ACTION_UP)
links[0].onClick(widget);
else if (action == MotionEvent.ACTION_DOWN)
Selection.setSelection(buffer,
buffer.getSpanStart(links[0]),
buffer.getSpanEnd(links[0]));
return true;
// Removed
//else
// Selection.removeSelection(buffer);
//
return super.onTouchEvent(widget, buffer, event);
要使用它,只需调用:
textView.setTextIsSelectable(true);
textView.setMovementMethod(LinkArrowKeyMovementMethod.getInstance());
这对我来说非常有效。
【讨论】:
【参考方案4】:LinkMovementMethod()
不太支持文本选择,即使我们可以选择文本,但是滚动textview后,选择会丢失。
最好的实现是从ArrowKeyMovementMethod
扩展而来,它很好地支持了文本选择。
详情请看here
【讨论】:
【参考方案5】:XML TextView 不应有任何链接或任何可以选择的属性:
<TextView
android:layout_
android:layout_/>
然后,按照以下顺序以编程方式设置所有内容:
textView.setText(Html.fromHtml(myHtml));
Linkify.addLinks(textView, Linkify.WEB_URLS);
textView.setTextIsSelectable(true); // API-11 and above
textView.setMovementMethod(LinkMovementMethod.getInstance());
【讨论】:
没想到这个排序修复会起作用,但它确实起作用了!【参考方案6】:另外,订单很重要
textView.setTextIsSelectable(true);
textView.setMovementMethod(LinkMovementMethod.getInstance());
允许选择内容并且链接点击工作完美
【讨论】:
【参考方案7】:这是我对 Kotlin 的看法(大致基于 @hai-zhang 的回答)。 简化!请参阅我的要点以获得更好的版本。我目前将它用于自定义跨度,而不是 HTML,它仍然对我有用,尤其是当我需要将用户点击的位置传递给跨度对象时。
需要设置移动方法after setTextIsSelectable(true)
/** Minimal version of Smart Movement that only has limited support of [ClickableSpan] */
object SmartMovementMethodMinimal : ArrowKeyMovementMethod()
override fun onTouchEvent(widget: TextView?, buffer: Spannable?, event: MotionEvent?) =
handleMotion(event!!, widget!!, buffer!!) || super.onTouchEvent(widget, buffer, event)
private fun handleMotion(event: MotionEvent, widget: TextView, buffer: Spannable): Boolean
if (event.action == MotionEvent.ACTION_UP)
// Get click position
val target = Point().apply
x = event.x.toInt() - widget.totalPaddingLeft + widget.scrollX
y = event.y.toInt() - widget.totalPaddingTop + widget.scrollY
// Get span line and offset
val line = widget.layout.getLineForVertical(target.y)
val offset = widget.layout.getOffsetForHorizontal(line, target.x.toFloat())
if (event.action == MotionEvent.ACTION_UP)
val spans = buffer.getSpans<ClickableSpan>(offset, offset)
if (spans.isNotEmpty())
spans.forEach it.onClick(widget)
return true
return false
更详细和更复杂的代码和示例在这里:https://gist.github.com/sQu1rr/210f7e08dd939fa30dcd2209177ba875
【讨论】:
【参考方案8】:是否可以将 TextView 与 URL 关联? 如果您有 10 个 TextView 和 10 个 URL,编写代码应该很简单,如果单击 TextView[3],它会触发带有 URL[3] 的 webview(或浏览器)的意图
【讨论】:
不幸的是,我需要在TextView
中拥有多个链接的能力,就像在普通的 HTML 标记中一样。我知道我可以使用WebView
,但我试图避免这种资源密集型控制。以上是关于TextView 可以选择并包含链接吗?的主要内容,如果未能解决你的问题,请参考以下文章