Android TextView长按选择复制工具类

开篇废话

最近有个需求,需要做一个像微信聊天一样可以长按可以任意选择复制的功能,这就要用到了Spannable了,但不止止的Spannable,在写的过程中也是遇到了很多的坑,为了避免大家踩坑,把我写的SelectableTextHelper分享给大家。
SelectableTextHelper之GitHub地址,帮我点个Star,赠人玫瑰,手留余香,谢谢。

先讲一下大致思路

首先需要三个弹窗,分别是选中文字左边和角标、选中文字右边的角标、带复制全选按钮的弹窗。
我们可以通过选中的文字的区域去算出角标的位置,同时我们在移动角标时也需要算出角标停留位置选中的文字范围。


效果图

选择复制工具类

SelectableTextHelper.java

package com.cc.selectable_text_helper.java;

import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.text.Layout;
import android.text.Spannable;
import android.text.Spanned;
import android.text.style.BackgroundColorSpan;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.PopupWindow;
import android.widget.TextView;

import androidx.annotation.DrawableRes;
import androidx.cardview.widget.CardView;

import com.cc.selectable_text_helper.R;

/**
 * Created by guoshichao on 2021/3/17
 * 

* 此View只能包含一个子View * CursorHandle 是两个游标 * OperateWindow 是弹出的操作框 * FullScreenWindow 全屏弹窗,点击空白全部弹窗消失 */ public class SelectableTextHelper { private Context mContext; private TextView mTextView; private View mOperateView; private int mArrowRes; private Spannable mSpannable; private final SelectionInfo mSelectionInfo = new SelectionInfo(); private final static int DEFAULT_SELECTION_LENGTH = 1; private BackgroundColorSpan mSpan; private final int mCursorHandleColor = R.color.selectable_cursor; private final int mSelectedColor = R.color.selectable_select_text_bg; private CursorHandle mStartHandle; private CursorHandle mEndHandle; private boolean isShow = true; private OperateWindow mOperateWindow; private FullScreenWindow mFullScreenWindow; private SelectableOnChangeListener onChangeListener; public SelectableTextHelper(View operateView, @DrawableRes int arrowRes) { if (operateView == null) { throw new SelectFrameLayoutException("操作框View不可为null"); } this.mOperateView = operateView; this.mArrowRes = arrowRes; } public String getSelectedText() { return mSelectionInfo.mSelectionContent; } public void setSelectableOnChangeListener(SelectableOnChangeListener onChangeListener) { this.onChangeListener = onChangeListener; } public void showSelectView(TextView textView, int x, int y) { if (textView.getPaddingLeft() > 0 || textView.getPaddingRight() > 0 || textView.getPaddingTop() > 0 || textView.getPaddingBottom() > 0 || textView.getPaddingStart() > 0 || textView.getPaddingEnd() > 0) { throw new SelectFrameLayoutException("不可给TextView设置padding"); } mContext = textView.getContext(); mTextView = textView; mTextView.setText(mTextView.getText(), TextView.BufferType.SPANNABLE); if (mOperateWindow == null) mOperateWindow = new OperateWindow(mContext); if (mFullScreenWindow == null) mFullScreenWindow = new FullScreenWindow(mContext); hideSelectView(); resetSelectionInfo(); isShow = true; if (mStartHandle == null) mStartHandle = new CursorHandle(true); if (mEndHandle == null) mEndHandle = new CursorHandle(false); //点哪选哪 // int startOffset = TextLayoutUtil.getPreciseOffset(this, x, y); // int endOffset = startOffset + DEFAULT_SELECTION_LENGTH; //全选 int startOffset = 0; int endOffset = mTextView.length(); if (mTextView.getText() instanceof Spannable) { mSpannable = (Spannable) mTextView.getText(); } if (mSpannable == null || startOffset >= mTextView.getText().length()) { return; } selectText(startOffset, endOffset); mFullScreenWindow.show(); showCursorHandle(mStartHandle); showCursorHandle(mEndHandle); mOperateWindow.firstShowWithTextView(); } private void showCursorHandle(CursorHandle cursorHandle) { Layout layout = mTextView.getLayout(); int offset = cursorHandle.isLeft ? mSelectionInfo.getStart(mTextView) : mSelectionInfo.getEnd(mTextView); cursorHandle.show((int) layout.getPrimaryHorizontal(offset), layout.getLineBottom(layout.getLineForOffset(offset))); } public void copyText() { ClipboardManager clip = (ClipboardManager) mContext .getSystemService(Context.CLIPBOARD_SERVICE); clip.setPrimaryClip(ClipData.newPlainText( mSelectionInfo.mSelectionContent, mSelectionInfo.mSelectionContent)); } public void selectAll() { hideSelectView(); selectText(0, mTextView.getText().length()); isShow = true; mFullScreenWindow.show(); showCursorHandle(mStartHandle); showCursorHandle(mEndHandle); mOperateWindow.showWithTextView(); } public void dismiss() { resetSelectionInfo(); hideSelectView(); } public void resetSelectionInfo() { mSelectionInfo.mSelectionContent = null; if (mSpannable != null && mSpan != null) { mSpannable.removeSpan(mSpan); mSpan = null; } } public void hideSelectView() { isShow = false; if (mStartHandle != null) { mStartHandle.dismiss(); } if (mEndHandle != null) { mEndHandle.dismiss(); } if (mOperateWindow != null) { mOperateWindow.dismiss(); } if (mFullScreenWindow != null) { mFullScreenWindow.dismiss(); } } /* * startPos:起始索引 endPos:尾部索引 */ private void selectText(int startPos, int endPos) { if (startPos != -1) { mSelectionInfo.setStart(startPos); } if (endPos != -1) { mSelectionInfo.setEnd(endPos); } if (mSelectionInfo.getStart(mTextView) > mSelectionInfo.getEnd(mTextView)) { int temp = mSelectionInfo.getStart(mTextView); mSelectionInfo.setStart(mSelectionInfo.getEnd(mTextView)); mSelectionInfo.setEnd(temp); } if (mSpannable != null) { if (mSpan == null) { mSpan = new BackgroundColorSpan(mContext.getResources().getColor(mSelectedColor)); } mSelectionInfo.mSelectionContent = mSpannable.subSequence( mSelectionInfo.getStart(mSpannable), mSelectionInfo.getEnd(mSpannable)).toString(); // 调用系统方法设置选中文本的状态 mSpannable.setSpan(mSpan, mSelectionInfo.getStart(mTextView), mSelectionInfo.getEnd(mTextView), Spanned.SPAN_INCLUSIVE_EXCLUSIVE); if (onChangeListener != null) { onChangeListener.onChange(mSelectionInfo.mSelectionContent, startPos == 0 && endPos == mTextView.getText().length()); } } } public int getTextViewX() { int[] location = new int[2]; mTextView.getLocationOnScreen(location); return location[0]; } public int getTextViewY() { int[] location = new int[2]; mTextView.getLocationOnScreen(location); return location[1]; } /* * 游标类 */ class CursorHandle extends View { private final int mCursorHandleSize = 48; private PopupWindow mPopupWindow; private Paint mPaint; private int mCircleRadius = mCursorHandleSize / 2; private int mWidth = mCircleRadius * 2; private int mHeight = mCircleRadius * 2; private int mPadding = 25; private boolean isLeft; public CursorHandle(boolean isLeft) { super(mContext); this.isLeft = isLeft; mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mPaint.setColor(mContext.getResources().getColor(mCursorHandleColor)); mPopupWindow = new PopupWindow(this); mPopupWindow.setClippingEnabled(false); mPopupWindow.setWidth(mWidth + mPadding * 2); mPopupWindow.setHeight(mHeight + mPadding / 2); invalidate(); } @Override protected void onDraw(Canvas canvas) { canvas.drawCircle(mCircleRadius + mPadding, mCircleRadius, mCircleRadius, mPaint); if (isLeft) { canvas.drawRect(mCircleRadius + mPadding, 0, mCircleRadius * 2 + mPadding, mCircleRadius, mPaint); } else { canvas.drawRect(mPadding, 0, mCircleRadius + mPadding, mCircleRadius, mPaint); } } private int mAdjustX; private int mAdjustY; private int mBeforeDragStart; private int mBeforeDragEnd; @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: mBeforeDragStart = mSelectionInfo.getStart(mTextView); mBeforeDragEnd = mSelectionInfo.getEnd(mTextView); mAdjustX = (int) event.getX(); mAdjustY = (int) event.getY(); break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: mOperateWindow.showWithTextView(); break; case MotionEvent.ACTION_MOVE: mOperateWindow.dismiss(); int rawX = (int) event.getRawX(); int rawY = (int) event.getRawY(); update(rawX + mAdjustX - mWidth - getTextViewX(), rawY + mAdjustY - mHeight); break; } return true; } private void changeDirection() { isLeft = !isLeft; invalidate(); } public void dismiss() { mPopupWindow.dismiss(); } private int[] mTempCoors = new int[2]; public void update(int x, int y) { mTextView.getLocationInWindow(mTempCoors); int oldOffset; if (isLeft) { oldOffset = mSelectionInfo.getStart(mTextView); } else { oldOffset = mSelectionInfo.getEnd(mTextView); } y -= mTempCoors[1]; int offset = TextLayoutUtils.getHysteresisOffset(mTextView, x, y, oldOffset); if (offset != oldOffset) { resetSelectionInfo(); if (isLeft) { if (offset > mBeforeDragEnd) { CursorHandle handle = getCursorHandle(false); changeDirection(); handle.changeDirection(); mBeforeDragStart = mBeforeDragEnd; selectText(mBeforeDragEnd, offset); handle.updateCursorHandle(); } else { selectText(offset, -1); } updateCursorHandle(); } else { if (offset < mBeforeDragStart) { CursorHandle handle = getCursorHandle(true); handle.changeDirection(); changeDirection(); mBeforeDragEnd = mBeforeDragStart; selectText(offset, mBeforeDragStart); handle.updateCursorHandle(); } else { selectText(mBeforeDragStart, offset); } updateCursorHandle(); } } } private void updateCursorHandle() { mTextView.getLocationInWindow(mTempCoors); Layout layout = mTextView.getLayout(); if (isLeft) { mPopupWindow.update( (int) layout .getPrimaryHorizontal(mSelectionInfo.getStart(mTextView)) - mWidth + getExtraX(), layout.getLineBottom(layout .getLineForOffset(mSelectionInfo.getStart(mTextView))) + getExtraY(), -1, -1); } else { mPopupWindow.update( (int) layout.getPrimaryHorizontal(mSelectionInfo.getEnd(mTextView)) + getExtraX(), layout.getLineBottom(layout .getLineForOffset(mSelectionInfo.getEnd(mTextView))) + getExtraY(), -1, -1); } } public void show(int x, int y) { mTextView.getLocationInWindow(mTempCoors); int offset = isLeft ? mWidth : 0; mPopupWindow.showAtLocation(mTextView, Gravity.NO_GRAVITY, x - offset + getExtraX(), y + getExtraY()); } public int getExtraX() { return mTempCoors[0] - mPadding + mTextView.getPaddingLeft(); } public int getExtraY() { return mTempCoors[1] + mTextView.getPaddingTop(); } } private CursorHandle getCursorHandle(boolean isLeft) { if (mStartHandle.isLeft == isLeft) { return mStartHandle; } else { return mEndHandle; } } /* * 操作框 */ private class OperateWindow { private int screenWidth; private int paddingLR; private PopupWindow mWindow; private View contentView; private CardView cvRoot; private ImageView ivArrow; public OperateWindow(final Context context) { screenWidth = TextLayoutUtils.getScreenWidth(mContext); paddingLR = TextLayoutUtils.dip2px(mContext, 13); contentView = LayoutInflater.from(context).inflate( R.layout.select_text_operate_windows, null); contentView.measure(View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), View.MeasureSpec .makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)); mWindow = new PopupWindow(contentView, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, false); mWindow.setClippingEnabled(false); cvRoot = contentView.findViewById(R.id.cv_root); cvRoot.addView(mOperateView); ivArrow = contentView.findViewById(R.id.iv_arrow); if (mArrowRes > 0) { ivArrow.setVisibility(View.VISIBLE); ivArrow.setImageResource(mArrowRes); } else { ivArrow.setVisibility(View.GONE); } } private int getWindowWidth() { return contentView.getMeasuredWidth(); } private int getWindowHeight() { return contentView.getMeasuredHeight(); } private int getWindowRemoveRight() { int removeX = 0; Layout layout = mTextView.getLayout(); int start = (int) layout.getPrimaryHorizontal(mSelectionInfo.getStart(mTextView)); int end = (int) layout.getPrimaryHorizontal(mSelectionInfo.getEnd(mTextView)); boolean isSameLine = layout.getLineTop(layout.getLineForOffset(mSelectionInfo.getStart(mTextView))) == layout.getLineTop(layout.getLineForOffset(mSelectionInfo.getEnd(mTextView))); if (end > start && isSameLine) { removeX = end - start; } else { removeX = mTextView.getWidth() - start; } return removeX / 2; } public void firstShowWithTextView() { showWithTextView(); mTextView.post(new Runnable() { @Override public void run() { dismiss(); showWithTextView(); } }); } public void showWithTextView() { Layout layout = mTextView.getLayout(); int posX = (int) layout.getPrimaryHorizontal(mSelectionInfo.getStart(mTextView)) + getTextViewX() - getWindowWidth() / 2 + getWindowRemoveRight(); int posY = layout.getLineTop(layout.getLineForOffset(mSelectionInfo.getStart(mTextView))) + getTextViewY() - getWindowHeight() - paddingLR; int removeArrow = 0; if (posX < paddingLR) { removeArrow = posX - paddingLR; posX = paddingLR; } if (posY < 0) { posY = paddingLR; } if (posX + getWindowWidth() > screenWidth - paddingLR) { removeArrow = posX - (screenWidth - getWindowWidth() - paddingLR); posX = screenWidth - getWindowWidth() - paddingLR; } ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) ivArrow.getLayoutParams(); lp.leftMargin = removeArrow; ivArrow.setLayoutParams(lp); mWindow.showAtLocation(mTextView, Gravity.NO_GRAVITY, posX, posY); } public void showWithView() { int posX = getTextViewX() - getWindowWidth() / 2 + mTextView.getMeasuredWidth() / 2; int posY = getTextViewY() - getWindowHeight() + mTextView.getPaddingTop() - paddingLR; int removeArrow = 0; if (posX < paddingLR) { removeArrow = posX - paddingLR; posX = paddingLR; } if (posY < paddingLR) { posY = paddingLR; } if (posX + getWindowWidth() > screenWidth - paddingLR) { removeArrow = posX - (screenWidth - getWindowWidth() - paddingLR); posX = screenWidth - getWindowWidth() - paddingLR; } ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) ivArrow.getLayoutParams(); lp.leftMargin = removeArrow; ivArrow.setLayoutParams(lp); mWindow.showAtLocation(mTextView, Gravity.NO_GRAVITY, posX, posY); } public void dismiss() { mWindow.dismiss(); } public boolean isShowing() { return mWindow.isShowing(); } } /* * 全屏Window,用来点击空白使其它弹窗消失 */ private class FullScreenWindow { private PopupWindow mFullScreenWindow; public FullScreenWindow(Context context) { View contentView = LayoutInflater.from(context).inflate( R.layout.select_text_full_screen_windows, null); mFullScreenWindow = new PopupWindow(contentView, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, false); mFullScreenWindow.setClippingEnabled(false); mFullScreenWindow.setTouchInterceptor(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { if (mOperateWindow == null || mOperateWindow.contentView == null) { dismiss(); } if (!TextLayoutUtils.isInView(mOperateWindow.contentView, event)) { if (mStartHandle != null && mEndHandle != null) { if (!TextLayoutUtils.isInView(mStartHandle, event) && !TextLayoutUtils.isInView(mEndHandle, event)) { resetSelectionInfo(); hideSelectView(); } } else { hideSelectView(); } } return true; } }); } public void show() { mFullScreenWindow.showAtLocation(mTextView, Gravity.NO_GRAVITY, 0, 0); } public void dismiss() { mFullScreenWindow.dismiss(); } } }

关键的计算角标及选中文字位置的类

TextLayoutUtils.java

package com.cc.selectable_text_helper.java;

import android.content.Context;
import android.graphics.Rect;
import android.text.Layout;
import android.view.MotionEvent;
import android.view.View;
import android.widget.TextView;

public class TextLayoutUtils {

    public static int getScreenWidth(Context context) {
        return context.getResources().getDisplayMetrics().widthPixels;
    }

    public static int getPreciseOffset(TextView textView, int x, int y) {
        Layout layout = textView.getLayout();
        if (layout != null) {
            int topVisibleLine = layout.getLineForVertical(y);
            int offset = layout.getOffsetForHorizontal(topVisibleLine, x);

            int offsetX = (int) layout.getPrimaryHorizontal(offset);

            if (offsetX > x) {
                return layout.getOffsetToLeftOf(offset);
            } else {
                return offset;
            }
        } else {
            return -1;
        }
    }

    public static int getHysteresisOffset(TextView textView, int x, int y, int previousOffset) {
        final Layout layout = textView.getLayout();
        if (layout == null) return -1;

        int line = layout.getLineForVertical(y);

        // The "HACK BLOCK"S in this function is required because of how Android Layout for
        // TextView works - if 'offset' equals to the last character of a line, then
        //
        // * getLineForOffset(offset) will result the NEXT line
        // * getPrimaryHorizontal(offset) will return 0 because the next insertion point is on the next line
        // * getOffsetForHorizontal(line, x) will not return the last offset of a line no matter where x is
        // These are highly undesired and is worked around with the HACK BLOCK
        //
        // @see Moon+ Reader/Color Note - see how it can't select the last character of a line unless you move
        // the cursor to the beginning of the next line.
        //
        ////////////////////HACK BLOCK////////////////////////////////////////////////////

        if (isEndOfLineOffset(layout, previousOffset)) {
            // we have to minus one from the offset so that the code below to find
            // the previous line can work correctly.
            int left = (int) layout.getPrimaryHorizontal(previousOffset - 1);
            int right = (int) layout.getLineRight(line);
            int threshold = (right - left) / 2; // half the width of the last character
            if (x > right - threshold) {
                previousOffset -= 1;
            }
        }
        ///////////////////////////////////////////////////////////////////////////////////

        final int previousLine = layout.getLineForOffset(previousOffset);
        final int previousLineTop = layout.getLineTop(previousLine);
        final int previousLineBottom = layout.getLineBottom(previousLine);
        final int hysteresisThreshold = (previousLineBottom - previousLineTop) / 2;

        // If new line is just before or after previous line and y position is less than
        // hysteresisThreshold away from previous line, keep cursor on previous line.
        if (((line == previousLine + 1) && ((y - previousLineBottom) < hysteresisThreshold)) || ((line == previousLine - 1) && ((
            previousLineTop
                - y) < hysteresisThreshold))) {
            line = previousLine;
        }

        int offset = layout.getOffsetForHorizontal(line, x);

        // This allow the user to select the last character of a line without moving the
        // cursor to the next line. (As Layout.getOffsetForHorizontal does not return the
        // offset of the last character of the specified line)
        //
        // But this function will probably get called again immediately, must decrement the offset
        // by 1 to compensate for the change made below. (see previous HACK BLOCK)
        /////////////////////HACK BLOCK///////////////////////////////////////////////////
        if (offset < textView.getText().length() - 1) {
            if (isEndOfLineOffset(layout, offset + 1)) {
                int left = (int) layout.getPrimaryHorizontal(offset);
                int right = (int) layout.getLineRight(line);
                int threshold = (right - left) / 2; // half the width of the last character
                if (x > right - threshold) {
                    offset += 1;
                }
            }
        }
        //////////////////////////////////////////////////////////////////////////////////

        if (offset > textView.getText().length()) {
            offset = textView.getText().length();
        }

        return offset;
    }

    private static boolean isEndOfLineOffset(Layout layout, int offset) {
        return offset > 0 && layout.getLineForOffset(offset) == layout.getLineForOffset(offset - 1) + 1;
    }

    /**
     * 判断触摸的点是否在View范围内
     */
    public static boolean isInView(View view, MotionEvent event) {
        int[] location = {0, 0};
        view.getLocationInWindow(location);
        int left = location[0], top = location[1], bottom = top + view.getHeight(), right = left + view.getWidth();
        float eventX = event.getX();
        float eventY = event.getY();
        Rect rect = new Rect(left, top, right, bottom);
        return rect.contains((int) eventX, (int) eventY);
    }

    public static int dip2px(Context context, float dpValue) {
        final float scale = context.getResources().getDisplayMetrics().density;
        return (int) (dpValue * scale + 0.5f);
    }
}

其它类

SelectionInfo.java

package com.cc.selectable_text_helper.java;

import android.widget.TextView;

public class SelectionInfo {
    private int mStart;
    private int mEnd;
    public String mSelectionContent;

    public int getStart(TextView textView) {
        if (textView == null) {
            return 0;
        }
        if (mStart > textView.length()) {
            return textView.length();
        }
        if (mStart < 0) {
            return 0;
        }
        return mStart;
    }

    public int getStart(CharSequence charSequence) {
        if (charSequence == null) {
            return 0;
        }
        if (mStart > charSequence.length()) {
            return charSequence.length();
        }
        if (mStart < 0) {
            return 0;
        }
        return mStart;
    }

    public void setStart(int start) {
        this.mStart = start;
    }

    public int getEnd(TextView textView) {
        if (textView == null) {
            return 0;
        }
        if (mEnd > textView.length()) {
            return textView.length();
        }
        if (mEnd < 0) {
            return 0;
        }
        return mEnd;
    }

    public int getEnd(CharSequence charSequence) {
        if (charSequence == null) {
            return 0;
        }
        if (mEnd > charSequence.length()) {
            return charSequence.length();
        }
        if (mEnd < 0) {
            return 0;
        }
        return mEnd;
    }

    public void setEnd(int end) {
        this.mEnd = end;
    }
}

SelectFrameLayoutException.java

package com.cc.selectable_text_helper.java;

/**
 * Created by guoshichao on 2021/3/17
 */
public class SelectFrameLayoutException extends RuntimeException {

    private static final long serialVersionUID = 20210317L;

    public SelectFrameLayoutException() {
        super();
    }

    public SelectFrameLayoutException(String string) {
        super(string);
    }

}

SelectableOnChangeListener.java

package com.cc.selectable_text_helper.java;

/**
 * Created by guoshichao on 2021/3/9
 */
public interface SelectableOnChangeListener {

    void onChange(CharSequence text, boolean isSelectAll);

}

其它xml

select_text_operate_windows.xml




    

    

    


select_text_full_screen_windows.xml





使用案例

MainActivity.kt

package com.cc.selectabletexthelper

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View.OnLongClickListener
import android.view.View.OnTouchListener
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import com.cc.selectable_text_helper.java.SelectableTextHelper

class MainActivity : AppCompatActivity() {

    var tvSelect : TextView? = null
    var tvSelectable : TextView? = null
    var selectableTextHelper : SelectableTextHelper? = null
    var mTouchX = 0
    var mTouchY = 0

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val operateView = LayoutInflater.from(this).inflate(R.layout.view_select_text_operate, null)
        selectableTextHelper = SelectableTextHelper(operateView, R.drawable.select_text_view_arrow)
        val itCopy = operateView.findViewById(R.id.it_copy)
        itCopy.setOnClickListener {
            selectableTextHelper?.copyText()
            selectableTextHelper?.dismiss()
        }
        val itSelectAll = operateView.findViewById(R.id.it_select_all)
        itSelectAll.setOnClickListener {
            selectableTextHelper?.selectAll()
        }
        val itCancel = operateView.findViewById(R.id.it_cancel)
        itCancel.setOnClickListener {
            selectableTextHelper?.dismiss()
        }

        tvSelect = findViewById(R.id.tv_select)
        tvSelect?.setText(R.string.app_name)

        tvSelect?.setOnLongClickListener(OnLongClickListener {
            selectableTextHelper?.showSelectView(tvSelect, mTouchX, mTouchY)
            true
        })
        tvSelect?.setOnTouchListener(OnTouchListener { arg0, event ->
            mTouchX = event.x.toInt()
            mTouchY = event.y.toInt()
            false
        })

        tvSelect?.setOnClickListener {
            selectableTextHelper?.resetSelectionInfo()
            selectableTextHelper?.hideSelectView()
        }


        tvSelectable = findViewById(R.id.tv_selectable)
        tvSelectable?.setOnLongClickListener(OnLongClickListener {
            selectableTextHelper?.showSelectView(tvSelectable, mTouchX, mTouchY)
            true
        })
        tvSelectable?.setOnTouchListener(OnTouchListener { arg0, event ->
            mTouchX = event.x.toInt()
            mTouchY = event.y.toInt()
            false
        })

        tvSelectable?.setOnClickListener {
            selectableTextHelper?.resetSelectionInfo()
            selectableTextHelper?.hideSelectView()
        }
    }

}

activity_main.xml




    

        

        

    


view_select_text_operate.xml




    

    

    


结束小语

文字选择本身并无太多难点,关键是要知道几个api,可以对角标进行计算,就可以实现出想要的效果。
最近我在使用其它app的时候,发现它的选择复制可以是多个TextView,这种实现我并没有通过代码写出来,我可以分享一下我的思路。
SelectableTextHelper维护一个TextView列表,在移动角标时将TextView在列表里添加或移除,通过手势的位置判断需要添加移除哪些,开始角标计算TextView列表中的第一个,结束角标计算TextView列表最后一个,在复制时将每个TextView选中的部分进行整合,中间加上回车符。
谢谢大家有耐心观看到最后。

更多内容戳这里(整理好的各种文集)

你可能感兴趣的:(Android TextView长按选择复制工具类)