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

写在前面的话

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值