美文网首页简化开发Android开发Android开发经验谈
Android TextView长按选择复制工具类

Android TextView长按选择复制工具类

作者: 一个有故事的程序员 | 来源:发表于2021-06-07 12:26 被阅读0次

    开篇废话

    最近有个需求,需要做一个像微信聊天一样可以长按可以任意选择复制的功能,这就要用到了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
     * <p>
     * 此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

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical">
    
        <androidx.cardview.widget.CardView
            android:id="@+id/cv_root"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            app:cardBackgroundColor="@color/selectable_select_pop_bg"
            app:cardCornerRadius="3dp"
            app:cardElevation="3dp"
            app:cardMaxElevation="6dp">
    
        </androidx.cardview.widget.CardView>
    
        <ImageView
            android:id="@+id/iv_arrow"
            android:layout_width="19dp"
            android:layout_height="10dp"
            android:layout_gravity="center_horizontal"
            android:src="@color/selectable_select_pop_bg" />
    
    </LinearLayout>
    

    select_text_full_screen_windows.xml

    <?xml version="1.0" encoding="utf-8"?>
    <androidx.constraintlayout.widget.ConstraintLayout
        xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"
        android:layout_height="match_parent">
    
    </androidx.constraintlayout.widget.ConstraintLayout>
    

    使用案例

    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<TextView>(R.id.it_copy)
            itCopy.setOnClickListener {
                selectableTextHelper?.copyText()
                selectableTextHelper?.dismiss()
            }
            val itSelectAll = operateView.findViewById<TextView>(R.id.it_select_all)
            itSelectAll.setOnClickListener {
                selectableTextHelper?.selectAll()
            }
            val itCancel = operateView.findViewById<TextView>(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

    <?xml version="1.0" encoding="utf-8"?>
    <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">
    
        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent">
    
            <TextView
                android:id="@+id/tv_select"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="Hello World!"
                android:textSize="14sp"
                android:textColor="@color/black" />
    
            <TextView
                android:id="@+id/tv_selectable"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginTop="20dp"
                android:text="Hello World!"
                android:textSize="14sp"
                android:textColor="@color/black" />
    
        </LinearLayout>
    
    </androidx.constraintlayout.widget.ConstraintLayout>
    

    view_select_text_operate.xml

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:id="@+id/ll_it_all"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="horizontal">
    
        <TextView
            android:id="@+id/it_copy"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginRight="1dp"
            android:background="@color/black"
            android:paddingLeft="17dp"
            android:paddingTop="10dp"
            android:paddingRight="17dp"
            android:paddingBottom="10dp"
            android:text="复制"
            android:textColor="@color/white"
            android:textSize="14sp" />
    
        <TextView
            android:id="@+id/it_select_all"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginRight="1dp"
            android:background="@color/black"
            android:paddingLeft="17dp"
            android:paddingTop="10dp"
            android:paddingRight="17dp"
            android:paddingBottom="10dp"
            android:text="全选"
            android:textColor="@color/white"
            android:textSize="14sp" />
    
        <TextView
            android:id="@+id/it_cancel"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="@color/black"
            android:paddingLeft="17dp"
            android:paddingTop="10dp"
            android:paddingRight="17dp"
            android:paddingBottom="10dp"
            android:text="取消"
            android:textColor="@color/white"
            android:textSize="14sp"/>
    
    </LinearLayout>
    

    结束小语

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

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

    相关文章

      网友评论

        本文标题:Android TextView长按选择复制工具类

        本文链接:https://www.haomeiwen.com/subject/itmrsltx.html