两种方案实现
完整代码:
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:
方案一在小米手机上不会出现记笔记的选项,原因是小米的定制系统禁用了该事件。
使用:
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);
}
布局文件: