android之TextView自由选择复制

2023-10-13 17:30

本文主要是介绍android之TextView自由选择复制,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

  • 前言
  • 一、效果图
  • 二、实现步骤
    • 1.OnSelectListener
    • 2.SelectionInfo类
    • 3.TextLayoutUtil类
    • 4.复制弹框的xml布局
    • 5.弹框背景Drawable
    • 6.倒三角Drawable
    • 7.复制工具类
    • 8.调用
  • 总结


前言

根据时代进步,那些干产品的也叼砖起来了,今天就遇到一个需求,需要对TextView的文案进行自由选择复制,不怕,我们是勇敢牛牛。


一、效果图

在这里插入图片描述

二、实现步骤

1.OnSelectListener

public interface OnSelectListener {void onTextSelected(CharSequence content);
}

2.SelectionInfo类

代码如下(示例):

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

3.TextLayoutUtil类

package com.example.merchant.utils;import android.content.Context;
import android.text.Layout;
import android.widget.TextView;public class TextLayoutUtil {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);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 characterif (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 (((line == previousLine + 1) && ((y - previousLineBottom) < hysteresisThreshold)) || ((line == previousLine - 1) && ((previousLineTop- y) < hysteresisThreshold))) {line = previousLine;}int offset = layout.getOffsetForHorizontal(line, x);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 characterif (x > right - threshold) {offset += 1;}}}return offset;}private static boolean isEndOfLineOffset(Layout layout, int offset) {return offset > 0 && layout.getLineForOffset(offset) == layout.getLineForOffset(offset - 1) + 1;}public static int dp2px(Context context, float dpValue) {final float scale = context.getResources().getDisplayMetrics().density;return (int) (dpValue * scale + 0.5f);}
}

4.复制弹框的xml布局

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="wrap_content"android:layout_height="match_parent"><LinearLayoutandroid:id="@+id/linearLayout"android:layout_width="wrap_content"android:layout_height="wrap_content"android:background="@drawable/bg_operate_window"android:orientation="horizontal"android:paddingLeft="5dp"android:paddingRight="5dp"><TextViewandroid:id="@+id/tv_copy"style="@style/OperateTextView"android:text="@string/Copy" /><TextViewandroid:id="@+id/tv_select_all"style="@style/OperateTextView"android:text="@string/SelectAll" /></LinearLayout><ImageViewandroid:id="@+id/iv_triangle"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_below="@+id/linearLayout"android:layout_centerHorizontal="true"android:src="@drawable/triangle_down" />
</RelativeLayout>

5.弹框背景Drawable

<?xml version="1.0" encoding="utf-8"?><shape xmlns:android="http://schemas.android.com/apk/res/android"android:shape="rectangle"><corners android:radius="5dp" /><solid android:color="#454545" /></shape>

6.倒三角Drawable

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"><item><rotateandroid:fromDegrees="45"android:pivotX="135%"android:pivotY="15%"><shape android:shape="rectangle"><sizeandroid:width="16dp"android:height="16dp" /><solid android:color="#454545" /></shape></rotate></item>
</layer-list>

7.复制工具类

package com.example.merchant.utils;import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.os.Build;
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.view.ViewTreeObserver;
import android.widget.PopupWindow;
import android.widget.TextView;import androidx.annotation.ColorInt;import com.example.merchant.R;/*** 复制utils*/
public class SelectableTextHelper {private final static int DEFAULT_SELECTION_LENGTH = 1;private static final int DEFAULT_SHOW_DURATION = 100;private CursorHandle mStartHandle;private CursorHandle mEndHandle;private OperateWindow mOperateWindow;private SelectionInfo mSelectionInfo = new SelectionInfo();private OnSelectListener mSelectListener;private Context mContext;private TextView mTextView;private Spannable mSpannable;private int mTouchX;private int mTouchY;private int mSelectedColor;private int mCursorHandleColor;private int mCursorHandleSize;private BackgroundColorSpan mSpan;private boolean isHideWhenScroll;private boolean isHide = true;private ViewTreeObserver.OnPreDrawListener mOnPreDrawListener;ViewTreeObserver.OnScrollChangedListener mOnScrollChangedListener;public SelectableTextHelper(Builder builder) {mTextView = builder.mTextView;mContext = mTextView.getContext();mSelectedColor = builder.mSelectedColor;mCursorHandleColor = builder.mCursorHandleColor;mCursorHandleSize = TextLayoutUtil.dp2px(mContext, builder.mCursorHandleSizeInDp);init();}private void init() {mTextView.setText(mTextView.getText(), TextView.BufferType.SPANNABLE);mTextView.setOnLongClickListener(new View.OnLongClickListener() {@Overridepublic boolean onLongClick(View v) {showSelectView(mTouchX, mTouchY);return true;}});mTextView.setOnTouchListener(new View.OnTouchListener() {@Overridepublic boolean onTouch(View v, MotionEvent event) {mTouchX = (int) event.getX();mTouchY = (int) event.getY();return false;}});mTextView.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {resetSelectionInfo();hideSelectView();}});mTextView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {@Overridepublic void onViewAttachedToWindow(View v) {}@Overridepublic void onViewDetachedFromWindow(View v) {destroy();}});mOnPreDrawListener = new ViewTreeObserver.OnPreDrawListener() {@Overridepublic boolean onPreDraw() {if (isHideWhenScroll) {isHideWhenScroll = false;postShowSelectView(DEFAULT_SHOW_DURATION);}return true;}};mTextView.getViewTreeObserver().addOnPreDrawListener(mOnPreDrawListener);mOnScrollChangedListener = new ViewTreeObserver.OnScrollChangedListener() {@Overridepublic void onScrollChanged() {if (!isHideWhenScroll && !isHide) {isHideWhenScroll = true;if (mOperateWindow != null) {mOperateWindow.dismiss();}if (mStartHandle != null) {mStartHandle.dismiss();}if (mEndHandle != null) {mEndHandle.dismiss();}}}};mTextView.getViewTreeObserver().addOnScrollChangedListener(mOnScrollChangedListener);mOperateWindow = new OperateWindow(mContext);}private void postShowSelectView(int duration) {mTextView.removeCallbacks(mShowSelectViewRunnable);if (duration <= 0) {mShowSelectViewRunnable.run();} else {mTextView.postDelayed(mShowSelectViewRunnable, duration);}}private final Runnable mShowSelectViewRunnable = new Runnable() {@Overridepublic void run() {if (isHide) return;if (mOperateWindow != null) {mOperateWindow.show();}if (mStartHandle != null) {showCursorHandle(mStartHandle);}if (mEndHandle != null) {showCursorHandle(mEndHandle);}}};private void hideSelectView() {isHide = true;if (mStartHandle != null) {mStartHandle.dismiss();}if (mEndHandle != null) {mEndHandle.dismiss();}if (mOperateWindow != null) {mOperateWindow.dismiss();}}private void resetSelectionInfo() {mSelectionInfo.mSelectionContent = null;if (mSpannable != null && mSpan != null) {mSpannable.removeSpan(mSpan);mSpan = null;}}private void showSelectView(int x, int y) {hideSelectView();resetSelectionInfo();isHide = false;if (mStartHandle == null) mStartHandle = new CursorHandle(true);if (mEndHandle == null) mEndHandle = new CursorHandle(false);int startOffset = TextLayoutUtil.getPreciseOffset(mTextView, x, y);int endOffset = startOffset + DEFAULT_SELECTION_LENGTH;if (mTextView.getText() instanceof Spannable) {mSpannable = (Spannable) mTextView.getText();}if (mSpannable == null || startOffset >= mTextView.getText().length()) {return;}selectText(startOffset, endOffset);showCursorHandle(mStartHandle);showCursorHandle(mEndHandle);mOperateWindow.show();}private void showCursorHandle(CursorHandle cursorHandle) {Layout layout = mTextView.getLayout();int offset = cursorHandle.isLeft ? mSelectionInfo.mStart : mSelectionInfo.mEnd;cursorHandle.show((int) layout.getPrimaryHorizontal(offset), layout.getLineBottom(layout.getLineForOffset(offset)));}private void selectText(int startPos, int endPos) {if (startPos != -1) {mSelectionInfo.mStart = startPos;}if (endPos != -1) {mSelectionInfo.mEnd = endPos;}if (mSelectionInfo.mStart > mSelectionInfo.mEnd) {int temp = mSelectionInfo.mStart;mSelectionInfo.mStart = mSelectionInfo.mEnd;mSelectionInfo.mEnd = temp;}if (mSpannable != null) {if (mSpan == null) {mSpan = new BackgroundColorSpan(mSelectedColor);}mSelectionInfo.mSelectionContent = mSpannable.subSequence(mSelectionInfo.mStart, mSelectionInfo.mEnd).toString();mSpannable.setSpan(mSpan, mSelectionInfo.mStart, mSelectionInfo.mEnd, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);if (mSelectListener != null) {mSelectListener.onTextSelected(mSelectionInfo.mSelectionContent);}}}public void setSelectListener(OnSelectListener selectListener) {mSelectListener = selectListener;}public void destroy() {mTextView.getViewTreeObserver().removeOnScrollChangedListener(mOnScrollChangedListener);mTextView.getViewTreeObserver().removeOnPreDrawListener(mOnPreDrawListener);resetSelectionInfo();hideSelectView();mStartHandle = null;mEndHandle = null;mOperateWindow = null;}/*** Operate windows : copy, select all*/private class OperateWindow {private PopupWindow mWindow;private int[] mTempCoors = new int[2];private int mWidth;private int mHeight;public OperateWindow(final Context context) {View contentView = LayoutInflater.from(context).inflate(R.layout.layout_operate_windows2, null);contentView.measure(View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));mWidth = contentView.getMeasuredWidth();mHeight = contentView.getMeasuredHeight();mWindow =new PopupWindow(contentView, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, false);mWindow.setClippingEnabled(false);TextView tv_copy = contentView.findViewById(R.id.tv_copy);TextView tv_select_all = contentView.findViewById(R.id.tv_select_all);tv_copy.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {//复制点击实现功能AppTk.Companion.showTimeDailog(mSelectionInfo.mSelectionContent, mContext);if (mSelectListener != null) {mSelectListener.onTextSelected(mSelectionInfo.mSelectionContent);}SelectableTextHelper.this.resetSelectionInfo();SelectableTextHelper.this.hideSelectView();}});tv_select_all.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {hideSelectView();selectText(0, mTextView.getText().length());isHide = false;showCursorHandle(mStartHandle);showCursorHandle(mEndHandle);mOperateWindow.show();}});}public void show() {mTextView.getLocationInWindow(mTempCoors);Layout layout = mTextView.getLayout();int posX = (int) layout.getPrimaryHorizontal(mSelectionInfo.mStart) + mTempCoors[0];int posY = layout.getLineTop(layout.getLineForOffset(mSelectionInfo.mStart)) + mTempCoors[1] - mHeight - 16;if (posX <= 0) posX = 16;if (posY < 0) posY = 16;if (posX + mWidth > TextLayoutUtil.getScreenWidth(mContext)) {posX = TextLayoutUtil.getScreenWidth(mContext) - mWidth - 16;}if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {mWindow.setElevation(8f);}mWindow.showAtLocation(mTextView, Gravity.NO_GRAVITY, posX, posY);}public void dismiss() {mWindow.dismiss();}public boolean isShowing() {return mWindow.isShowing();}}private class CursorHandle extends View {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(mCursorHandleColor);mPopupWindow = new PopupWindow(this);mPopupWindow.setClippingEnabled(false);mPopupWindow.setWidth(mWidth + mPadding * 2);mPopupWindow.setHeight(mHeight + mPadding / 2);invalidate();}@Overrideprotected 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;@Overridepublic boolean onTouchEvent(MotionEvent event) {switch (event.getAction()) {case MotionEvent.ACTION_DOWN:mBeforeDragStart = mSelectionInfo.mStart;mBeforeDragEnd = mSelectionInfo.mEnd;mAdjustX = (int) event.getX();mAdjustY = (int) event.getY();break;case MotionEvent.ACTION_UP:case MotionEvent.ACTION_CANCEL:mOperateWindow.show();break;case MotionEvent.ACTION_MOVE:mOperateWindow.dismiss();int rawX = (int) event.getRawX();int rawY = (int) event.getRawY();update(rawX + mAdjustX - mWidth, 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.mStart;} else {oldOffset = mSelectionInfo.mEnd;}y -= mTempCoors[1];int offset = TextLayoutUtil.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.mStart) - mWidth + getExtraX(),layout.getLineBottom(layout.getLineForOffset(mSelectionInfo.mStart)) + getExtraY(), -1, -1);} else {mPopupWindow.update((int) layout.getPrimaryHorizontal(mSelectionInfo.mEnd) + getExtraX(),layout.getLineBottom(layout.getLineForOffset(mSelectionInfo.mEnd)) + 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;}}public static class Builder {private TextView mTextView;private int mCursorHandleColor = 0xFF1379D6;private int mSelectedColor = 0xFFAFE1F4;private float mCursorHandleSizeInDp = 24;public Builder(TextView textView) {mTextView = textView;}public Builder setCursorHandleColor(@ColorInt int cursorHandleColor) {mCursorHandleColor = cursorHandleColor;return this;}public Builder setCursorHandleSizeInDp(float cursorHandleSizeInDp) {mCursorHandleSizeInDp = cursorHandleSizeInDp;return this;}public Builder setSelectedColor(@ColorInt int selectedBgColor) {mSelectedColor = selectedBgColor;return this;}public SelectableTextHelper build() {return new SelectableTextHelper(this);}}
}

8.调用

private var mSelectableTextHelper: SelectableTextHelper? = null//实例化
//text为文案
mSelectableTextHelper = SelectableTextHelper.Builder(text).setSelectedColor(Color.parseColor("#afe1f4")).setCursorHandleSizeInDp(20f).setCursorHandleColor(Color.parseColor("#0d7aff")).build()

总结

感觉东西是有点多,但比较实用,而且直接复制去就可以用,自己写主要费大脑不是。

这篇关于android之TextView自由选择复制的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/204769

相关文章

Android实现悬浮按钮功能

《Android实现悬浮按钮功能》在很多场景中,我们希望在应用或系统任意界面上都能看到一个小的“悬浮按钮”(FloatingButton),用来快速启动工具、展示未读信息或快捷操作,所以本文给大家介绍... 目录一、项目概述二、相关技术知识三、实现思路四、整合代码4.1 Java 代码(MainActivi

Android Mainline基础简介

《AndroidMainline基础简介》AndroidMainline是通过模块化更新Android核心组件的框架,可能提高安全性,本文给大家介绍AndroidMainline基础简介,感兴趣的朋... 目录关键要点什么是 android Mainline?Android Mainline 的工作原理关键

如何解决idea的Module:‘:app‘platform‘android-32‘not found.问题

《如何解决idea的Module:‘:app‘platform‘android-32‘notfound.问题》:本文主要介绍如何解决idea的Module:‘:app‘platform‘andr... 目录idea的Module:‘:app‘pwww.chinasem.cnlatform‘android-32

Android实现打开本地pdf文件的两种方式

《Android实现打开本地pdf文件的两种方式》在现代应用中,PDF格式因其跨平台、稳定性好、展示内容一致等特点,在Android平台上,如何高效地打开本地PDF文件,不仅关系到用户体验,也直接影响... 目录一、项目概述二、相关知识2.1 PDF文件基本概述2.2 android 文件访问与存储权限2.

Android Studio 配置国内镜像源的实现步骤

《AndroidStudio配置国内镜像源的实现步骤》本文主要介绍了AndroidStudio配置国内镜像源的实现步骤,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,... 目录一、修改 hosts,解决 SDK 下载失败的问题二、修改 gradle 地址,解决 gradle

在Android平台上实现消息推送功能

《在Android平台上实现消息推送功能》随着移动互联网应用的飞速发展,消息推送已成为移动应用中不可或缺的功能,在Android平台上,实现消息推送涉及到服务端的消息发送、客户端的消息接收、通知渠道(... 目录一、项目概述二、相关知识介绍2.1 消息推送的基本原理2.2 Firebase Cloud Me

Android中Dialog的使用详解

《Android中Dialog的使用详解》Dialog(对话框)是Android中常用的UI组件,用于临时显示重要信息或获取用户输入,本文给大家介绍Android中Dialog的使用,感兴趣的朋友一起... 目录android中Dialog的使用详解1. 基本Dialog类型1.1 AlertDialog(

Android Kotlin 高阶函数详解及其在协程中的应用小结

《AndroidKotlin高阶函数详解及其在协程中的应用小结》高阶函数是Kotlin中的一个重要特性,它能够将函数作为一等公民(First-ClassCitizen),使得代码更加简洁、灵活和可... 目录1. 引言2. 什么是高阶函数?3. 高阶函数的基础用法3.1 传递函数作为参数3.2 Lambda

Android自定义Scrollbar的两种实现方式

《Android自定义Scrollbar的两种实现方式》本文介绍两种实现自定义滚动条的方法,分别通过ItemDecoration方案和独立View方案实现滚动条定制化,文章通过代码示例讲解的非常详细,... 目录方案一:ItemDecoration实现(推荐用于RecyclerView)实现原理完整代码实现

Android App安装列表获取方法(实践方案)

《AndroidApp安装列表获取方法(实践方案)》文章介绍了Android11及以上版本获取应用列表的方案调整,包括权限配置、白名单配置和action配置三种方式,并提供了相应的Java和Kotl... 目录前言实现方案         方案概述一、 androidManifest 三种配置方式