Android 文本视图是否可以选择并包含链接?
我遇到了TextView的问题。我可以使用setTextIsSelectabletrue使其可选择,但当我通过setMovementMethodLinkMovementMethod.getInstance启用单击链接时,它不再可选择Android 文本视图是否可以选择并包含链接?,android,textview,Android,Textview,我遇到了TextView的问题。我可以使用setTextIsSelectabletrue使其可选择,但当我通过setMovementMethodLinkMovementMethod.getInstance启用单击链接时,它不再可选择 请注意,我的意思不是让原始链接可点击,而是让实际单词可点击,方法是使用类似setTextHtml.fromHtml的东西加载带有HTML标记的TextView。是否可以将TextView与URL关联? 如果您有10个TextView和10个URL,那么编写代码应该很
请注意,我的意思不是让原始链接可点击,而是让实际单词可点击,方法是使用类似setTextHtml.fromHtml的东西加载带有HTML标记的TextView。是否可以将TextView与URL关联?
如果您有10个TextView和10个URL,那么编写代码应该很简单,如果单击TextView[3],它会激发webview或带有URL[3]的浏览器的意图。我已经找到了答案。您需要将LinkMovementMethod子类化,并添加对文本选择的支持。真的很不幸,它不能在本地支持它。我只是使用for 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());
双击textview时回答原因异常
java.lang.IndexOutOfBoundsException: setSpan (-1 ... -1) starts before 0...
所以我重写了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);
}
}
希望它能对其他人有所帮助。LinkMovementMethod不太支持文本选择,即使我们可以选择文本,但在滚动文本视图后,选择将丢失
最好的实现是从ArrowKeyMovementMethod扩展而来,它非常支持文本选择
请参见中的详细信息XML文本视图不应具有任何可选择的链接或任何属性:
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
TL;DR:只需在答案末尾使用LinkArrowKeyMovementMethod,即可获得完美的解决方案
如果您尝试使用扩展LinkMovementMethod的最受欢迎答案,则会出现一个恼人的错误-当您通过单击未选择的文本来取消选择时,整个选择会从开始到结束闪烁,然后变为零。这是因为LinkMovementMethod实际上不能像ArrowKeyMovementMethod那样处理选择
另一种解决方法可能是使用TextView自己的解决方法,如果您已将android:autoLink设置为true,如TextView的以下来源所示:
但我个人不想要自动链接功能,因为我有自己的链接信息,所以基于@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(LinkMovementMethod.getInstance());
允许内容可选择,链接点击效果完美以下是我对Kotlin的看法,大致基于@hai zhang的回答。简化!请参阅我的要点以获得更好的版本。我目前将其用于自定义跨距,而不是HTML,并且它仍然与我相关,特别是当我需要将用户单击的位置传递给跨距对象时 您需要在setTextIsSelectabletrue之后设置移动方法
/** 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
}
}
这里有更详细、更复杂的代码示例:您尝试过Linkify吗?根据文档,Linkify只适用于带有URL方案前缀的文本,不适用于要转换为链接的任意文本。我认为您必须创建自定义TextView是否可以将LinkMovementMethod子类化并对其进行修改以允许选择?很遗憾,可能是重复的。不,我需要能够在TextView中具有多个链接,就像在普通HTML标记中一样。我知道我可以使用WebView,但我正在努力避免这种资源密集型的控制。很好!非常感谢你,伙计,你太棒了!我开始认为这是不可能的,但后来我偶然发现了你的问题。必须热爱开源!对我们来说,这是可行的,但我们必须将textIsSelectable设置为trueFYI,这个解决方案有时会导致java.lang.IndexOutOfBoundsException:setSpan-1-在某些情况下,1在0之前启动,例如在Nexus 5设备上双击。完美!你救了我一天。这个解决方案比下一个更适合我!非常感谢你的分享-你救了我一天!在2台设备上测试。没想到这个订购修复程序会起作用,但它确实起作用了!
/**
* @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());
textView.setTextIsSelectable(true);
textView.setMovementMethod(LinkMovementMethod.getInstance());
/** 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
}
}