如何使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; } }
相关文章推荐
- TextView识别超链接,点击可以打开浏览器进行跳转
- EditView与TextView如何实现长按复制、粘贴、选择
- 可以响应各个方向CompoundDrawables点击操作的TextView的使用
- TextView SpannableString 超链接点击相应
- TextView 如何实现复制
- 可以点击显示更多的textview
- 点击TextView 弹出复制选项
- 可以响应各个方向CompoundDrawables点击操作的TextView的实现原理
- android如果给TextView或EditText的email链接加下划线,并在点击在email连接上可以弹框显示
- Android TextView中实现点击文本超链接(无下划线)的封装类
- 识别链接,可以点击TextView
- [android]如何让TextView使用超链接
- Android自定义TextView中的超链接点击事件处理
- Android学习—— TextView ClickableSpan 点击链接事件 改超链接颜色
- 可以响应各个方向CompoundDrawables点击操作的TextView的实现原理
- TextView 超链接点击跳转到下一个Activity
- 28-Dialog点击选择确定,取消,显示在TextView中
- 如何通过点击使TextView文字颜色改变
- 超简单实现TextView中某段文字超链接点击打开浏览器跳转到网页