Android TextView自定义选中弹出菜单记笔记功能
16lz
2021-01-23
效果图
两种方案实现
一、 通过onActionItemClicked
完整代码:
mManusTv.setCustomSelectionActionModeCallback(new ActionMode.Callback() { @Override public boolean onCreateActionMode(ActionMode mode, Menu menu) { MenuInflater inflater = mode.getMenuInflater(); if (inflater != null) { inflater.inflate(R.menu.manus_menu, menu); } return true; } @Override public boolean onPrepareActionMode(ActionMode mode, Menu menu) { return false; } @Override public boolean onActionItemClicked(ActionMode mode, MenuItem item) { if (item.getItemId() == R.id.notes) { //记笔记 if (mManusTv == null) { return false; } int min = 0; int max = mManusContent.length(); if (mManusTv.isFocused()) { final int selStart = mManusTv.getSelectionStart(); final int selEnd = mManusTv.getSelectionEnd(); min = Math.max(0, Math.min(selStart, selEnd)); max = Math.max(0, Math.max(selStart, selEnd)); } } return false; } @Override public void onDestroyActionMode(ActionMode mode) { } });
menu:
<?xml version="1.0" encoding="utf-8"?>
方案一在小米手机上不会出现记笔记的选项,原因是小米的定制系统禁用了该事件。
二、 SelectableTextHelper
使用:
mSelectableTextHelper = new SelectableTextHelper.Builder(mManusTv) .setSelectedColor(getResources().getColor(R.color.color_tv_theme_transparent15)) .setCursorHandleSizeInDp(20) .setCursorHandleColor(getResources().getColor(R.color.colotBtnTheme)) .build();
选中回调监听:
mSelectableTextHelper.setOnNotesClickListener(new OnNoteBookClickListener() { @Override public void onTextSelect(CharSequence charSequence) { LogUtils.e("记笔记:"+charSequence); String content = charSequence.toString(); } });
SelectableTextHelper完整代码:
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); } } } OnNoteBookClickListener mNoteBookClickListener; public void setSelectListener(OnSelectListener selectListener) { mSelectListener = selectListener; } public void setOnNotesClickListener(OnNoteBookClickListener notesClickListener) { mNoteBookClickListener = notesClickListener; } public void destroy() { mTextView.getViewTreeObserver().removeOnScrollChangedListener(mOnScrollChangedListener); mTextView.getViewTreeObserver().removeOnPreDrawListener(mOnPreDrawListener); resetSelectionInfo(); hideSelectView(); mStartHandle = null; mEndHandle = null; mOperateWindow = null; } public void dismiss() { SelectableTextHelper.this.resetSelectionInfo(); SelectableTextHelper.this.hideSelectView(); } /** * 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_windows, 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); contentView.findViewById(R.id.tv_copy).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { ClipboardManager clip = (ClipboardManager) mContext.getSystemService(Context.CLIPBOARD_SERVICE); clip.setPrimaryClip( ClipData.newPlainText(mSelectionInfo.mSelectionContent, mSelectionInfo.mSelectionContent)); if (mSelectListener != null) { mSelectListener.onTextSelected(mSelectionInfo.mSelectionContent); } SelectableTextHelper.this.resetSelectionInfo(); SelectableTextHelper.this.hideSelectView(); } }); contentView.findViewById(R.id.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(); } }); contentView.findViewById(R.id.tv_note).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (mNoteBookClickListener != null) { mNoteBookClickListener.onTextSelect(mSelectionInfo.mSelectionContent); } SelectableTextHelper.this.resetSelectionInfo(); SelectableTextHelper.this.hideSelectView(); } }); } 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); } }}
TextLayoutUtil
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); // 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; } } } ////////////////////////////////////////////////////////////////////////////////// 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); }}
SelectionInfo:
public class SelectionInfo { public int mStart; public int mEnd; public String mSelectionContent;}
OnNoteBookClickListener:
public interface OnNoteBookClickListener { void onTextSelect(CharSequence content);}
布局文件:
<?xml version="1.0" encoding="utf-8"?>
更多相关文章
- android 设置默认launcher 附上代码
- Android通过代码自动连接WiFi
- Android service: startService的代码实现
- 【代码】利用Android的Log 演示一个activity的生命周期
- Android Robotium的自动化代码
- Android代码实现飞行模式的打开
- Android对应用程序的资源文件xml解析的源代码在哪里