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()

总结

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/133499.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

第一次汇报相关问题

深度学习现在已经学习到了Mini-Batch&#xff0c;early-stop等针对特定场景优化的算法了。 代码已经实现了一个L层的神经网络的构建了 论文看了一些综述 主要思考的两个方向&#xff1a;云计算和嵌入式 云计算&#xff1a;分布式机器学习、联邦学习、服务器负载均衡等 嵌入式&…

前端axios下载导出文件工具封装

使用示例&#xff1a; import { fileDownload } from /utils/fileDownloadfileDownload({ url: process.env.VUE_APP_BASE_URL /statistic/pageList/export, method: post, data: data })工具类&#xff1a; import store from ../store/index import {getAccessToken } fro…

【大数据】HBase入门指南

原创不易&#xff0c;注重版权。转载请注明原作者和原文链接 文章目录 HBase特性Hadoop的限制基本概念NameSpaceTableRowKeyColumnTimeStampCell 存储结构HBase 数据访问形式架构体系HBase组件HBase读写流程读流程写流程 MemStore Flush参数说明 StoreFile Compaction参数说明触…

ros学习笔记(1)Mac本地安装虚拟机,安装Ros2环境

Ros与Linux的关系 Ros环境基于Linux系统内核 我们平时用的是Linux发行版&#xff0c;centos&#xff0c;ubuntu等等&#xff0c;机器人就用了ubunut 有时候我们经常会听到ubunue的版本&#xff0c;众多版本中&#xff0c;有一些是长期维护版TLS&#xff0c;有一些是短期维护…

微信小程序 js中写一个px单位转rpx单位的函数

大家写东西自然还是会比较喜欢用rpx 但是 事实证明 在js中 还是px好用 因为很多单位交互的函数还是只返回px单位的 理论上将 750 rpx 是整个屏幕的宽度 那么 我们可以这样写一个函数 pxToRpx(px) {//获取整个屏幕的宽度单位 pxlet screenWidth wx.getSystemInfoSync().scree…

IDEA—java: 常量字符串过长问题解决

问题描述&#xff1a; Error: java: 常量字符串过长 问题分析&#xff1a; 字符串长度过长&#xff0c;导致 idea 默认使用的 javac 编译器编译不了。 解决办法&#xff1a; Javac 编译器改为 Eclipse 编译器。 File -> Settings -> Build,Execution,Deployment -&…

SQL中for xml path 的用法

1. 用法 是一种将查询结果转换为 XML 格式的方法。它可以将查询结果中的每一行转换为一个 XML 元素&#xff0c;并且可以指定元素的名称和属性。 2. 应用示例 有一张学生选修课程的表&#xff0c;如下图所示 希望整合成下图所示效果 --建表 if object_id(StudentInfo,u) is…

【22】c++设计模式——>外观模式

外观模式定义 为复杂系统提供一个简化接口&#xff0c;它通过创建一个高层接口(外观)&#xff0c;将多个子系统的复杂操作封装起来&#xff0c;以便客户端更容易使用。 简单实现 #include<iostream>// 子系统类 class SubsystemA { public:void operationA() {std::co…

【angular】实现简单的angular国际化(i18n)

文章目录 目标过程运行在TS中国际化参考 目标 实现简单的angular国际化。本博客实现中文版和法语版。 将Hello i18n!变为中文版&#xff1a;你好 i18n!或法语版:Bonjour l’i18n !。 过程 创建一个项目&#xff1a; ng new i18nDemo在集成终端中打开。 添加本地化包&#…

Python批量测试IP端口GUI程序(Tkinter)

一、实现样式 批量IP与端口中间用“,”分割&#xff0c;点击Telnet进行测试&#xff0c;前提是你电脑安装了telnet客户端&#xff0c;Clear按钮用来清空文本框。 二、核心点 1、使用Tkinter来制作桌面GUI页面 2、使用telnetlib模块测试telnet端口 三、困难点 1、测试结果…

这8款浏览器兼容性测试工具,用了以后测试效率可以“起飞”~~

浏览器的兼容性问题&#xff0c;是指不同浏览器使用内核及所支持的 HTML 等网页语言标准不同&#xff0c;用户客户端的环境不同造成的显示效果不能达到理想效果。 对于用户而言&#xff0c;无论使用哪款浏览器&#xff0c;期望看到的效果是正常的统一的。 市面上发布的浏览器…

[Spring] SpringMVC 简介(二)

目录 五、域对象共享数据 1、使用 ServletAPI 向 request 域对象共享数据 2、使用 ModelAndView 向 request 域对象共享数据 3、使用 Model、Map、ModelMap 向 request 域对象共享数据 4、向 session 域和 application 域共享数据 六、SpringMVC 的视图 1、ThymeleafVie…