您的位置:首页 > 其它

如何使TextView可以选择复制又可以点击超链接

2016-09-03 22:54 826 查看

写在前面的话

代码都是别人的,我只不过是归总了一下,代码是别人的为什么还要写这篇博客,这是我苦苦搜索了一两天才找到的相对好的方案,希望使用中文搜索的人找到这篇博客后可以少走一些弯路,事半功倍,对自己也是总结。希望有遇到更好的解决方案的朋友评论一个链接

本文链接:http://blog.csdn.net/dreamsever/article/details/52425603

前言

最近在做社区,提出需求文章内容可以让选择复制,然后文章中可以加入超链接,点击去加载链接。使用英文说就是: How to make TextView selectable and contains links,或者android - Can a TextView be selectable AND contain links? 在做项目的过程中,我发现单独实现可选择复制,或者单独加入超链接可点击都是可以实现的,但是,当将这两个功能都设置到这个Textview的时候会有一些问题。就是有时复制的前后两个光标会跑到最前还在一起,这时候去点击或者触摸就会崩溃,还是android内部报的错误我们很无奈啊!下面是错误日志

java.lang.IndexOutOfBoundsException: setSpan (-1 ... -1) starts before 0
at android.text.SpannableStringInternal.checkRange(SpannableStringInternal.java:357)
at android.text.SpannableStringInternal.setSpan(SpannableStringInternal.java:79)
at android.text.SpannableString.setSpan(SpannableString.java:46)
at android.text.Selection.setSelection(Selection.java:76)
at android.widget.Editor$SelectionEndHandleView.updateSelection(Editor.java:4612)
at android.widget.Editor$HandleView.positionAtCursorOffset(Editor.java:4084)
at android.widget.Editor$SelectionEndHandleView.positionAndAdjustForCrossingHandles(Editor.java:4653)
at android.widget.Editor$SelectionEndHandleView.updatePosition(Editor.java:4643)
at android.widget.Editor$HandleView.onTouchEvent(Editor.java:4225)
at android.widget.Editor$SelectionHandleView.onTouchEvent(Editor.java:4670)
at android.view.View.dispatchTouchEvent(View.java:8491)
at android.view.View.dispatchPointerEvent(View.java:8686)
at android.view.ViewRootImpl$ViewPostImeInputStage.processPointerEvent(ViewRootImpl.java:4161)
at android.view.ViewRootImpl$ViewPostImeInputStage.onProcess(ViewRootImpl.java:4027)
at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:3577)
at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:3630)
at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:3596)
at android.view.ViewRootImpl$AsyncInputStage.forward(ViewRootImpl.java:3713)
at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:3604)
at android.view.ViewRootImpl$AsyncInputStage.apply(ViewRootImpl.java:3770)
at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:3577)
at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:3630)
at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:3596)
at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:3604)
at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:3577)
at android.view.ViewRootImpl.deliverInputEvent(ViewRootImpl.java:5845)
at android.view.ViewRootImpl.doProcessInputEvents(ViewRootImpl.java:5819)
at android.view.ViewRootImpl.enqueueInputEvent(ViewRootImpl.java:5790)
at android.view.ViewRootImpl$WindowInputEventReceiver.onInputEvent(ViewRootImpl.java:5935)
at android.view.InputEventReceiver.dispatchInputEvent(InputEventReceiver.java:185)
at android.view.InputEventReceiver.nativeConsumeBatchedInputEvents(Native Method)
at android.view.InputEventReceiver.consumeBatchedInputEvents(InputEventReceiver.java:176)
at android.view.ViewRootImpl.doConsumeBatchedInput(ViewRootImpl.java:5906)
at android.view.ViewRootImpl$ConsumeBatchedInputRunnable.run(ViewRootImpl.java:5958)
at android.view.Choreographer$CallbackRecord.run(Choreographer.java:769)
at android.view.Choreographer.doCallbacks(Choreographer.java:582)
at android.view.Choreographer.doFrame(Choreographer.java:550)
at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:755)
at android.os.Handler.handleCallback(Handler.java:739)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:135)
at android.app.ActivityThread.main(ActivityThread.java:5298)
at java.lang.reflect.Method.invoke(Native Method)
at java.lang.reflect.Method.invoke(Method.java:372)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:910)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:705)


下面先说这两个两个功能分别如何实现的:

超链接的实现

先实现TextView里面的链接可点击

首先需要对Textview的内容进行处理有两个方案:

第一个:

String content="这里是百度 <a href='http://www.baidu.com'>百度一下</a> 。。。后面一大堆";
tvContent.setText(Html.fromHtml(content));
tvContent.setMovementMethod(LinkMovementMethod.getInstance());


其中上面的Html.fromHtml()方法在build版本24会有过时,可以去查一下替代方案

另外也许有人需要点击超链接的时候跳转到自己的webviewactivity,而不是系统的浏览器,这个需要设置span的点击

选择复制的实现

选择复制的实现其实最简单,只需要在textview的属性里面加一句属性:

android:textIsSelectable=”true”,好像这句代码不兼容Android11以下,这个我想说Android11以下的真的没必要支持了吧

<TextView
android:id="@+id/post_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/post_name"
android:textIsSelectable="true"
android:text="这是测试的数据"/>


到现在可以点击超链接了,也可以选择复制了,功能算是实现了,但是实际使用的时候有很多问题,比如当用户选择复制了一部分内容的时候,你稍微一碰到这个textview,原来选择的那些东西就会消失,只有从头开始复制,真正的复制应该是可以滑动的。

然后我一不小心找到了这个博客:https://hwdtech.wordpress.com/2015/09/19/android-textview-from-html-with-clickable-links-and-text-selection/

这个博客里面使用了另外一个MovementMethod,设置方法如下

// tvContent.setMovementMethod(LinkMovementMethod.getInstance());

tvContent.setMovementMethod(CustomMovementMethod.getInstance());

CustomMovementMethod 继承自ArrowKeyMovementMethod,ArrowKeyMovementMethod可以实现滑动。我照着这个方法试了一下,选择复制用着挺好的,可以滑动了,滑动不会取消选择了但是,不可以点击超链接了

public class CustomMovementMethod extends ArrowKeyMovementMethod {

// The context we pass to the method
private static Context movementContext;
// A new LinkMovementMethod
private static CustomMovementMethod linkMovementMethod  = new CustomMovementMethod();

public static MovementMethod getInstance(Context c){
// Set the context
movementContext = c;
// Return this movement method
return linkMovementMethod;
}

public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event){
// Get the event action
int action = event.getAction();

// If action has finished
if(action == MotionEvent.ACTION_UP) {
// Locate the area that was pressed
int x = (int) event.getX();
int y = (int) event.getY();
x -= widget.getTotalPaddingLeft();
y -= widget.getTotalPaddingTop();
x += widget.getScrollX();
y += widget.getScrollY();

// Locate the URL text
Layout layout = widget.getLayout();
int line = layout.getLineForVertical(y);
int off = layout.getOffsetForHorizontal(line, x);

// Find the URL that was pressed
URLSpan[] link = buffer.getSpans(off, off, URLSpan.class);
// If we've found a URL
if (link.length != 0) {
// Find the URL
String url = link[0].getURL();
// If it's a valid URL
if (url.contains("https") | url.contains("tel") | url.contains("mailto") | url.contains("http") | url.contains("https") | url.contains("www")){
// Open it in an instance of InlineBrowser
movementContext.startActivity(new Intent(movementContext, MinimalBrowser.class).putExtra("url", url));
}
// If we're here, something's wrong
return true;
}
}
return super.onTouchEvent(widget, buffer, event);
}
}


finally

最终我找到了一个开源项目:

https://github.com/1gravity/Android-RTEditor

这个项目里的RTEditorMovementMethod正好满足我的需求,可滑动复制又不影响点击,有兴趣可以下载这个开源项目去学习下

/*
* Copyright (C) 2015-2016 Emanuel Moecklin
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*     http://www.apache.org/licenses/LICENSE-2.0 *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.onegravity.rteditor;

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 RTEditorMovementMethod extends ArrowKeyMovementMethod {

private static RTEditorMovementMethod sInstance;

private static Rect sLineBounds = new Rect();

public static synchronized  MovementMethod getInstance() {
if (sInstance == null) {
sInstance = new RTEditorMovementMethod();
}
return sInstance;
}

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

// TODO finding links doesn't work with right alignment and potentially other formatting options
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);
//Logger.e(Logger.LOG_TAG, "Found character: " + (text.length()>index ? text.charAt(index) : ""));
return index;
}
}

return -1;
}
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息