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
* <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选中的部分进行整合,中间加上回车符。
谢谢大家有耐心观看到最后。