主页 > IT业界  > 

android之TextView自由选择复制

android之TextView自由选择复制

文章目录 前言一、效果图二、实现步骤1.OnSelectListener2.SelectionInfo类3.TextLayoutUtil类4.复制弹框的xml布局5.弹框背景Drawable6.倒三角Drawable7.复制工具类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 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 (((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 character if (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 /apk/res/android" android:layout_width="wrap_content" android:layout_height="match_parent"> <LinearLayout android: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"> <TextView android:id="@+id/tv_copy" style="@style/OperateTextView" android:text="@string/Copy" /> <TextView android:id="@+id/tv_select_all" style="@style/OperateTextView" android:text="@string/SelectAll" /> </LinearLayout> <ImageView android: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 /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 /apk/res/android"> <item> <rotate android:fromDegrees="45" android:pivotX="135%" android:pivotY="15%"> <shape android:shape="rectangle"> <size android: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() { @Override public boolean onLongClick(View v) { showSelectView(mTouchX, mTouchY); return true; } }); mTextView.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { mTouchX = (int) event.getX(); mTouchY = (int) event.getY(); return false; } }); mTextView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { resetSelectionInfo(); hideSelectView(); } }); mTextView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() { @Override public void onViewAttachedToWindow(View v) { } @Override public void onViewDetachedFromWindow(View v) { destroy(); } }); mOnPreDrawListener = new ViewTreeObserver.OnPreDrawListener() { @Override public boolean onPreDraw() { if (isHideWhenScroll) { isHideWhenScroll = false; postShowSelectView(DEFAULT_SHOW_DURATION); } return true; } }; mTextView.getViewTreeObserver().addOnPreDrawListener(mOnPreDrawListener); mOnScrollChangedListener = new ViewTreeObserver.OnScrollChangedListener() { @Override public 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() { @Override public 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() { @Override public 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() { @Override public 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(); } @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.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自由选择复制由讯客互联IT业界栏目发布,感谢您对讯客互联的认可,以及对我们原创作品以及文章的青睐,非常欢迎各位朋友分享到个人网站或者朋友圈,但转载请说明文章出处“android之TextView自由选择复制