package org.telegram.ui.Cells; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; import android.app.Activity; import android.content.Context; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Path; import android.graphics.Rect; import android.graphics.RectF; import android.os.Build; import android.text.Layout; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.StaticLayout; import android.util.SparseArray; import android.util.SparseIntArray; import android.util.TypedValue; import android.view.ActionMode; import android.view.Gravity; import android.view.HapticFeedbackConstants; import android.view.Menu; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; import android.view.ViewParent; import android.view.animation.Interpolator; import android.view.animation.OvershootInterpolator; import android.widget.Magnifier; import android.widget.TextView; import androidx.recyclerview.widget.LinearLayoutManager; import org.jetbrains.annotations.NotNull; import org.telegram.messenger.AndroidUtilities; import org.telegram.messenger.ApplicationLoader; import org.telegram.messenger.Emoji; import org.telegram.messenger.MessageObject; import org.telegram.messenger.R; import org.telegram.messenger.SharedConfig; import org.telegram.ui.ActionBar.ActionBarPopupWindow; import org.telegram.ui.ActionBar.AlertDialog; import org.telegram.ui.ActionBar.FloatingActionMode; import org.telegram.ui.ActionBar.FloatingToolbar; import org.telegram.ui.ActionBar.Theme; import org.telegram.ui.ArticleViewer; import org.telegram.ui.Components.LayoutHelper; import org.telegram.ui.Components.RecyclerListView; import java.util.ArrayList; import tw.nekomimi.nekogram.transtale.TranslateDb; import tw.nekomimi.nekogram.transtale.Translator; import tw.nekomimi.nekogram.utils.AlertUtil; import tw.nekomimi.nekogram.utils.ProxyUtil; import static com.google.zxing.common.detector.MathUtils.distance; import static org.telegram.ui.ActionBar.FloatingToolbar.STYLE_THEME; import static org.telegram.ui.ActionBar.Theme.key_chat_inTextSelectionHighlight; public abstract class TextSelectionHelper { protected int textX; protected int textY; protected int maybeTextX; protected int maybeTextY; float movingOffsetX; float movingOffsetY; protected int[] tmpCoord = new int[2]; private boolean movingHandle; protected boolean movingHandleStart; private boolean isOneTouch; private int longpressDelay; private int touchSlop; protected PathWithSavedBottom path = new PathWithSavedBottom(); protected Paint selectionPaint = new Paint(); protected int capturedX; protected int capturedY; protected int selectionStart = -1; protected int selectionEnd = -1; protected int selectedCellId; private int topOffset; private boolean snap; private boolean tryCapture; private final ActionMode.Callback textSelectActionCallback = createActionCallback(); protected final Rect textArea = new Rect(); protected TextSelectionOverlay textSelectionOverlay; private Callback callback; protected RecyclerListView parentRecyclerView; protected ViewGroup parentView; private Magnifier magnifier; private float magnifierYanimated; private float magnifierY; private float magnifierDy; private boolean scrolling; private boolean scrollDown; protected boolean actionsIsShowing; private boolean parentIsScrolling; protected boolean movingDirectionSettling; private RectF startArea = new RectF(); private RectF endArea = new RectF(); protected float enterProgress; protected float handleViewProgress; protected Cell selectedView; protected Cell maybeSelectedView; private ActionMode actionMode; protected boolean multiselect; protected final LayoutBlock layoutBlock = new LayoutBlock(); private int lastX; private int lastY; private Interpolator interpolator = new OvershootInterpolator(); protected boolean showActionsAsPopupAlways = false; private Runnable scrollRunnable = new Runnable() { @Override public void run() { if (scrolling && parentRecyclerView != null) { int dy; if (multiselect && selectedView == null) { dy = AndroidUtilities.dp(8); } else if (selectedView != null) { dy = getLineHeight() >> 1; } else { return; } if (!multiselect) { if (scrollDown) { if (selectedView.getBottom() - dy < parentView.getMeasuredHeight()) { dy = selectedView.getBottom() - parentView.getMeasuredHeight(); } } else { if (selectedView.getTop() + dy > 0) { dy = -selectedView.getTop(); } } } parentRecyclerView.scrollBy(0, scrollDown ? dy : -dy); AndroidUtilities.runOnUIThread(this); } } }; final Runnable startSelectionRunnable = new Runnable() { @Override public void run() { if (maybeSelectedView == null || textSelectionOverlay == null) { return; } Cell oldView = selectedView; Cell newView = maybeSelectedView; CharSequence text = getText(maybeSelectedView, true); if (parentRecyclerView != null) { parentRecyclerView.cancelClickRunnables(false); } int x = capturedX; int y = capturedY; if (!textArea.isEmpty()) { if (x > textArea.right) x = textArea.right - 1; if (x < textArea.left) x = textArea.left + 1; if (y < textArea.top) y = textArea.top + 1; if (y > textArea.bottom) y = textArea.bottom - 1; } int offset = getCharOffsetFromCord(x, y, maybeTextX, maybeTextY, newView, true); if (offset >= text.length()) { fillLayoutForOffset(offset, layoutBlock, true); if (layoutBlock.layout == null) { selectionStart = selectionEnd = -1; return; } int endLine = layoutBlock.layout.getLineCount() - 1; x -= maybeTextX; if (x < layoutBlock.layout.getLineRight(endLine) + AndroidUtilities.dp(4) && x > layoutBlock.layout.getLineLeft(endLine)) { offset = text.length() - 1; } } if (offset >= 0 && offset < text.length() && text.charAt(offset) != '\n') { int maybeTextX = TextSelectionHelper.this.maybeTextX; int maybeTextY = TextSelectionHelper.this.maybeTextY; clear(); textSelectionOverlay.setVisibility(View.VISIBLE); onTextSelected(newView, oldView); selectionStart = offset; selectionEnd = selectionStart; if (text instanceof Spanned) { Emoji.EmojiSpan[] spans = ((Spanned) text).getSpans(0, text.length(), Emoji.EmojiSpan.class); for (Emoji.EmojiSpan emojiSpan : spans) { int s = ((Spanned) text).getSpanStart(emojiSpan); int e = ((Spanned) text).getSpanEnd(emojiSpan); if (offset >= s && offset <= e) { selectionStart = s; selectionEnd = e; break; } } } if (selectionStart == selectionEnd) { while (selectionStart > 0 && isInterruptedCharacter(text.charAt(selectionStart - 1))) { selectionStart--; } while (selectionEnd < text.length() && isInterruptedCharacter(text.charAt(selectionEnd))) { selectionEnd++; } } textX = maybeTextX; textY = maybeTextY; selectedView = newView; textSelectionOverlay.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); showActions(); invalidate(); if (oldView != null) { oldView.invalidate(); } if (callback != null) { callback.onStateChanged(true); } movingHandle = true; movingDirectionSettling = true; isOneTouch = true; movingOffsetY = 0; movingOffsetX = 0; onOffsetChanged(); } tryCapture = false; } }; public TextSelectionHelper() { longpressDelay = ViewConfiguration.getLongPressTimeout(); touchSlop = ViewConfiguration.get(ApplicationLoader.applicationContext).getScaledTouchSlop(); } public void setParentView(ViewGroup view) { if (view instanceof RecyclerListView) { parentRecyclerView = (RecyclerListView) view; } parentView = view; } public void setMaybeTextCord(int x, int y) { maybeTextX = x; maybeTextY = y; } public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: capturedX = (int) event.getX(); capturedY = (int) event.getY(); tryCapture = false; textArea.inset(-AndroidUtilities.dp(8), -AndroidUtilities.dp(8)); if (textArea.contains(capturedX, capturedY)) { textArea.inset(AndroidUtilities.dp(8), AndroidUtilities.dp(8)); int x = capturedX; int y = capturedY; if (x > textArea.right) x = textArea.right - 1; if (x < textArea.left) x = textArea.left + 1; if (y < textArea.top) y = textArea.top + 1; if (y > textArea.bottom) y = textArea.bottom - 1; int offset = getCharOffsetFromCord(x, y, maybeTextX, maybeTextY, maybeSelectedView, true); CharSequence text = getText(maybeSelectedView, true); if (offset >= text.length()) { fillLayoutForOffset(offset, layoutBlock, true); if (layoutBlock.layout == null) { tryCapture = false; return tryCapture; } int endLine = layoutBlock.layout.getLineCount() - 1; x -= maybeTextX; if (x < layoutBlock.layout.getLineRight(endLine) + AndroidUtilities.dp(4) && x > layoutBlock.layout.getLineLeft(endLine)) { offset = text.length() - 1; } } if (offset >= 0 && offset < text.length() && text.charAt(offset) != '\n') { AndroidUtilities.runOnUIThread(startSelectionRunnable, longpressDelay); tryCapture = true; } } return tryCapture; case MotionEvent.ACTION_MOVE: int y = (int) event.getY(); int x = (int) event.getX(); int r = (capturedY - y) * (capturedY - y) + (capturedX - x) * (capturedX - x); if (r > touchSlop) { AndroidUtilities.cancelRunOnUIThread(startSelectionRunnable); tryCapture = false; } return tryCapture; case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: AndroidUtilities.cancelRunOnUIThread(startSelectionRunnable); tryCapture = false; return false; } return false; } private void hideMagnifier() { if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) { if (magnifier != null) { magnifier.dismiss(); magnifier = null; } } } private void showMagnifier(int x) { if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) { if (selectedView == null || isOneTouch || !movingHandle || textSelectionOverlay == null) { return; } int offset = movingHandleStart ? selectionStart : selectionEnd; fillLayoutForOffset(offset, layoutBlock); StaticLayout layout = layoutBlock.layout; if (layout == null) { return; } int line = layout.getLineForOffset(offset); int lineHeight = layout.getLineBottom(line) - layout.getLineTop(line); int newY = (int) (layout.getLineTop(line) + textY + selectedView.getY()) - lineHeight - AndroidUtilities.dp(8); newY += layoutBlock.yOffset; if (magnifierY != newY) { magnifierY = newY; magnifierDy = (newY - magnifierYanimated) / 200f; } if (magnifier == null) { magnifier = new Magnifier(textSelectionOverlay); magnifierYanimated = magnifierY; } if (magnifierYanimated != magnifierY) { magnifierYanimated += magnifierDy * 16; } if (magnifierDy > 0 && magnifierYanimated > magnifierY) { magnifierYanimated = magnifierY; } else if (magnifierDy < 0 && magnifierYanimated < magnifierY) { magnifierYanimated = magnifierY; } int startLine; int endLine; if (selectedView instanceof ArticleViewer.BlockTableCell) { startLine = (int) selectedView.getX(); endLine = (int) selectedView.getX() + selectedView.getMeasuredWidth(); } else { startLine = (int) (selectedView.getX() + textX + layout.getLineLeft(line)); endLine = (int) (selectedView.getX() + textX + layout.getLineRight(line)); } if (x < startLine) { x = startLine; } else if (x > endLine) { x = endLine; } magnifier.show( x, magnifierYanimated + lineHeight * 1.5f + AndroidUtilities.dp(8) ); magnifier.update(); } } protected void showHandleViews() { if (handleViewProgress == 1f || textSelectionOverlay == null) { return; } ValueAnimator animator = ValueAnimator.ofFloat(0, 1f); animator.addUpdateListener(animation -> { handleViewProgress = (float) animation.getAnimatedValue(); textSelectionOverlay.invalidate(); }); animator.setDuration(250); animator.start(); } public boolean isSelectionMode() { return selectionStart >= 0 && selectionEnd >= 0; } private ActionBarPopupWindow popupWindow; private ActionBarPopupWindow.ActionBarPopupWindowLayout popupLayout; private TextView deleteView; private Rect popupRect; private void showActions() { if (textSelectionOverlay == null) { return; } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (!movingHandle && isSelectionMode() && canShowActions()) { if (!actionsIsShowing) { if (actionMode == null) { FloatingToolbar floatingToolbar = new FloatingToolbar(textSelectionOverlay.getContext(), textSelectionOverlay, STYLE_THEME); actionMode = new FloatingActionMode(textSelectionOverlay.getContext(), (ActionMode.Callback2) textSelectActionCallback, textSelectionOverlay, floatingToolbar); textSelectActionCallback.onCreateActionMode(actionMode, actionMode.getMenu()); } textSelectActionCallback.onPrepareActionMode(actionMode, actionMode.getMenu()); actionMode.hide(1); } AndroidUtilities.cancelRunOnUIThread(hideActionsRunnable); actionsIsShowing = true; } } else { if (!showActionsAsPopupAlways) { if (actionMode == null && isSelectionMode()) { actionMode = textSelectionOverlay.startActionMode(textSelectActionCallback); } } else { if (!movingHandle && isSelectionMode() && canShowActions()) { if (popupLayout == null) { popupRect = new android.graphics.Rect(); popupLayout = new ActionBarPopupWindow.ActionBarPopupWindowLayout(textSelectionOverlay.getContext()); popupLayout.setPadding(AndroidUtilities.dp(1), AndroidUtilities.dp(1), AndroidUtilities.dp(1), AndroidUtilities.dp(1)); popupLayout.setBackgroundDrawable(textSelectionOverlay.getContext().getResources().getDrawable(R.drawable.menu_copy)); popupLayout.setAnimationEnabled(false); popupLayout.setOnTouchListener((v, event) -> { if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { if (popupWindow != null && popupWindow.isShowing()) { v.getHitRect(popupRect); } } return false; }); popupLayout.setShowedFromBotton(false); deleteView = new TextView(textSelectionOverlay.getContext()); deleteView.setBackgroundDrawable(Theme.createSelectorDrawable(Theme.getColor(Theme.key_listSelector), 2)); deleteView.setGravity(Gravity.CENTER_VERTICAL); deleteView.setPadding(AndroidUtilities.dp(20), 0, AndroidUtilities.dp(20), 0); deleteView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 15); deleteView.setTypeface(AndroidUtilities.getTypeface("fonts/rmedium.ttf")); deleteView.setText(textSelectionOverlay.getContext().getString(android.R.string.copy)); deleteView.setTextColor(Theme.getColor(Theme.key_actionBarDefaultSubmenuItem)); deleteView.setOnClickListener(v -> { copyText(); }); popupLayout.addView(deleteView, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, 48)); popupWindow = new ActionBarPopupWindow(popupLayout, LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT); popupWindow.setAnimationEnabled(false); popupWindow.setAnimationStyle(R.style.PopupContextAnimation); popupWindow.setOutsideTouchable(true); if (popupLayout != null) { popupLayout.setBackgroundColor(Theme.getColor(Theme.key_actionBarDefaultSubmenuBackground)); } } int y = 0; if (selectedView != null) { int lineHeight = -getLineHeight(); int[] coords = offsetToCord(selectionStart); y = (int) (coords[1] + textY + selectedView.getY()) + lineHeight / 2 - AndroidUtilities.dp(4); if (y < 0) y = 0; } popupWindow.showAtLocation(textSelectionOverlay, Gravity.TOP, 0, y - AndroidUtilities.dp(48)); popupWindow.startAnimation(); } } } } protected boolean canShowActions() { return selectedView != null; } //fast way hide floating action mode for long time private final Runnable hideActionsRunnable = new Runnable() { @Override public void run() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (actionMode != null && !actionsIsShowing) { actionMode.hide(Long.MAX_VALUE); AndroidUtilities.runOnUIThread(hideActionsRunnable, 1000); } } } }; private void hideActions() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (actionMode != null && actionsIsShowing) { actionsIsShowing = false; hideActionsRunnable.run(); } actionsIsShowing = false; } if (!isSelectionMode() && actionMode != null) { actionMode.finish(); actionMode = null; } if (popupWindow != null) { popupWindow.dismiss(); } } public TextSelectionOverlay getOverlayView(Context context) { if (textSelectionOverlay == null) { textSelectionOverlay = new TextSelectionOverlay(context); } return textSelectionOverlay; } public boolean isSelected(MessageObject messageObject) { if (messageObject == null) { return false; } return selectedCellId == messageObject.getId(); } public void checkSelectionCancel(MotionEvent e) { if (e.getAction() == MotionEvent.ACTION_UP || e.getAction() == MotionEvent.ACTION_CANCEL) { cancelTextSelectionRunnable(); } } public void cancelTextSelectionRunnable() { AndroidUtilities.cancelRunOnUIThread(startSelectionRunnable); tryCapture = false; } public void clear() { clear(false); } public void clear(boolean instant) { onExitSelectionMode(instant); selectionStart = -1; selectionEnd = -1; hideMagnifier(); hideActions(); invalidate(); selectedView = null; selectedCellId = 0; AndroidUtilities.cancelRunOnUIThread(startSelectionRunnable); tryCapture = false; if (textSelectionOverlay != null) { textSelectionOverlay.setVisibility(View.GONE); } handleViewProgress = 0; if (callback != null) { callback.onStateChanged(false); } capturedX = -1; capturedY = -1; maybeTextX = -1; maybeTextY = -1; movingOffsetX = 0; movingOffsetY = 0; movingHandle = false; } protected void onExitSelectionMode(boolean didAction) { } public void setCallback(Callback listener) { callback = listener; } public boolean isTryingSelect() { return tryCapture; } public void onParentScrolled() { if (isSelectionMode() && textSelectionOverlay != null) { parentIsScrolling = true; textSelectionOverlay.invalidate(); hideActions(); } } public void stopScrolling() { parentIsScrolling = false; showActions(); } public static boolean isInterruptedCharacter(char c) { return Character.isLetter(c) || Character.isDigit(c) || c == '_'; } public void setTopOffset(int topOffset) { this.topOffset = topOffset; } public class TextSelectionOverlay extends View { Paint handleViewPaint = new Paint(Paint.ANTI_ALIAS_FLAG); float pressedX; float pressedY; long pressedTime = 0; Path path = new Path(); public TextSelectionOverlay(Context context) { super(context); handleViewPaint.setStyle(Paint.Style.FILL); } public boolean checkOnTap(MotionEvent event) { if (!isSelectionMode() || movingHandle) return false; switch (event.getAction()) { case MotionEvent.ACTION_DOWN: pressedX = event.getX(); pressedY = event.getY(); pressedTime = System.currentTimeMillis(); break; case MotionEvent.ACTION_UP: if (System.currentTimeMillis() - pressedTime < 200 && distance((int) pressedX, (int) pressedY, (int) event.getX(), (int) event.getY()) < touchSlop) { hideActions(); clear(); return true; } break; } return false; } @Override public boolean onTouchEvent(MotionEvent event) { if (!isSelectionMode()) return false; if (event.getPointerCount() > 1) { return movingHandle; } int dx = (int) (lastX - event.getX()); int dy = (int) (lastY - event.getY()); lastX = (int) event.getX(); lastY = (int) event.getY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: if (movingHandle) { return true; } int x = (int) event.getX(); int y = (int) event.getY(); if (startArea.contains(x, y)) { pickStartView(); if (selectedView == null) { return false; } movingHandle = true; movingHandleStart = true; int[] cords = offsetToCord(selectionStart); float textSizeHalf = getLineHeight() / 2; movingOffsetX = cords[0] + textX + selectedView.getX() - x; movingOffsetY = cords[1] + textY + selectedView.getTop() - y - textSizeHalf; hideActions(); return true; } if (endArea.contains(x, y)) { pickEndView(); if (selectedView == null) { return false; } movingHandle = true; movingHandleStart = false; int[] cords = offsetToCord(selectionEnd); float textSizeHalf = getLineHeight() / 2; movingOffsetX = cords[0] + textX + selectedView.getX() - x; movingOffsetY = cords[1] + textY + selectedView.getTop() - y - textSizeHalf; showMagnifier(lastX); hideActions(); return true; } movingHandle = false; break; case MotionEvent.ACTION_MOVE: if (movingHandle) { if (movingHandleStart) { pickStartView(); } else { pickEndView(); } if (selectedView == null) { return movingHandle; } y = (int) (event.getY() + movingOffsetY); x = (int) (event.getX() + movingOffsetX); boolean viewChanged = selectLayout(x, y); if (selectedView == null) { return true; } if (movingHandleStart) { fillLayoutForOffset(selectionStart, layoutBlock); } else { fillLayoutForOffset(selectionEnd, layoutBlock); } StaticLayout oldTextLayout = layoutBlock.layout; if (oldTextLayout == null) { return true; } float oldYoffset = layoutBlock.yOffset; Cell oldSelectedView = selectedView; y -= selectedView.getTop(); x -= selectedView.getX(); boolean canScrollDown = event.getY() - touchSlop > parentView.getMeasuredHeight() && (multiselect || selectedView.getBottom() > parentView.getMeasuredHeight()); boolean canScrollUp = event.getY() < ((View) parentView.getParent()).getTop() && (multiselect || selectedView.getTop() < 0); if (canScrollDown || canScrollUp) { if (!scrolling) { scrolling = true; AndroidUtilities.runOnUIThread(scrollRunnable); } scrollDown = canScrollDown; if (canScrollDown) { y = (int) (parentView.getMeasuredHeight() - selectedView.getTop() + movingOffsetY); } else { y = (int) (-selectedView.getTop() + movingOffsetY); } } else { if (scrolling) { scrolling = false; AndroidUtilities.cancelRunOnUIThread(scrollRunnable); } } int newSelection = getCharOffsetFromCord(x, y, textX, textY, selectedView, false); if (newSelection >= 0) { if (movingDirectionSettling) { if (viewChanged) { return true; } else if (newSelection < selectionStart) { movingDirectionSettling = false; movingHandleStart = true; hideActions(); } else if (newSelection > selectionEnd) { movingDirectionSettling = false; movingHandleStart = false; hideActions(); } else { return true; } } if (movingHandleStart) { if (selectionStart != newSelection && canSelect(newSelection)) { CharSequence text = getText(selectedView, false); fillLayoutForOffset(newSelection, layoutBlock); StaticLayout layoutOld = layoutBlock.layout; fillLayoutForOffset(selectionStart, layoutBlock); StaticLayout layoutNew = layoutBlock.layout; if (layoutOld == null || layoutNew == null) { return true; } int nextWhitespace = newSelection; while (nextWhitespace - 1 >= 0 && isInterruptedCharacter(text.charAt(nextWhitespace - 1))) { nextWhitespace--; } int nextWhitespaceLine = layoutNew.getLineForOffset(nextWhitespace); int currentLine = layoutNew.getLineForOffset(selectionStart); int newSelectionLine = layoutNew.getLineForOffset(newSelection); if (viewChanged || layoutOld != layoutNew || newSelectionLine != layoutNew.getLineForOffset(selectionStart) && newSelectionLine == nextWhitespaceLine) { jumpToLine(newSelection, nextWhitespace, viewChanged, layoutBlock.yOffset, oldYoffset, oldSelectedView); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { textSelectionOverlay.performHapticFeedback(HapticFeedbackConstants.TEXT_HANDLE_MOVE); } TextSelectionHelper.this.invalidate(); } else if (Layout.DIR_RIGHT_TO_LEFT == layoutNew.getParagraphDirection(layoutNew.getLineForOffset(newSelection)) || layoutNew.isRtlCharAt(newSelection) || nextWhitespaceLine != currentLine || newSelectionLine != nextWhitespaceLine) { selectionStart = newSelection; if (selectionStart > selectionEnd) { int k = selectionEnd; selectionEnd = selectionStart; selectionStart = k; movingHandleStart = false; } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { textSelectionOverlay.performHapticFeedback(HapticFeedbackConstants.TEXT_HANDLE_MOVE); } TextSelectionHelper.this.invalidate(); } else { int previousWhitespace = newSelection; while (previousWhitespace + 1 < text.length() && isInterruptedCharacter(text.charAt(previousWhitespace + 1))) { previousWhitespace++; } int distanseToNextWhitspace = Math.abs(newSelection - nextWhitespace); int distanseToPreviousWhitespace = Math.abs(newSelection - previousWhitespace); if (snap) { snap = dx >= 0; } boolean nextCharIsLitter = newSelection - 1 > 0 && isInterruptedCharacter(text.charAt(newSelection - 1)); char newChar; if (newSelection >= text.length()) { newSelection = text.length(); newChar = '\n'; } else { newChar = text.charAt(newSelection); } char selectionStartChar; if (selectionStart >= text.length()) { selectionStart = text.length(); selectionStartChar = '\n'; } else { selectionStartChar = text.charAt(selectionStart); } if ((newSelection < selectionStart && distanseToNextWhitspace < distanseToPreviousWhitespace) || (newSelection > selectionStart && dx < 0) || !isInterruptedCharacter(newChar) || (isInterruptedCharacter(selectionStartChar) && !snap) || newSelection == 0 || !nextCharIsLitter || selectionStartChar == '\n') { if (snap && newSelection == 1) { return true; } if (newSelection < selectionStart && isInterruptedCharacter(newChar) && !(isInterruptedCharacter(selectionStartChar) && !snap) && selectionStartChar != '\n') { selectionStart = nextWhitespace; snap = true; } else { selectionStart = newSelection; } if (selectionStart > selectionEnd) { int k = selectionEnd; selectionEnd = selectionStart; selectionStart = k; movingHandleStart = false; } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { textSelectionOverlay.performHapticFeedback(HapticFeedbackConstants.TEXT_HANDLE_MOVE); } TextSelectionHelper.this.invalidate(); } } } } else { if (newSelection != selectionEnd && canSelect(newSelection)) { CharSequence text = getText(selectedView, false); int nextWhitespace = newSelection; while (nextWhitespace < text.length() && isInterruptedCharacter(text.charAt(nextWhitespace))) { nextWhitespace++; } fillLayoutForOffset(newSelection, layoutBlock); StaticLayout layoutOld = layoutBlock.layout; fillLayoutForOffset(selectionEnd, layoutBlock); StaticLayout layoutNew = layoutBlock.layout; if (layoutOld == null || layoutNew == null) { return true; } if (newSelection > text.length()) { newSelection = text.length(); } int nextWhitespaceLine = layoutNew.getLineForOffset(nextWhitespace); int currentLine = layoutNew.getLineForOffset(selectionEnd); int newSelectionLine = layoutNew.getLineForOffset(newSelection); if (viewChanged || layoutOld != layoutNew || newSelectionLine != layoutNew.getLineForOffset(selectionEnd) && newSelectionLine == nextWhitespaceLine) { jumpToLine(newSelection, nextWhitespace, viewChanged, layoutBlock.yOffset, oldYoffset, oldSelectedView); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { textSelectionOverlay.performHapticFeedback(HapticFeedbackConstants.TEXT_HANDLE_MOVE); } TextSelectionHelper.this.invalidate(); } else if (Layout.DIR_RIGHT_TO_LEFT == layoutNew.getParagraphDirection(layoutNew.getLineForOffset(newSelection)) || layoutNew.isRtlCharAt(newSelection) || currentLine != nextWhitespaceLine || newSelectionLine != nextWhitespaceLine) { selectionEnd = newSelection; if (selectionStart > selectionEnd) { int k = selectionEnd; selectionEnd = selectionStart; selectionStart = k; movingHandleStart = true; } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { textSelectionOverlay.performHapticFeedback(HapticFeedbackConstants.TEXT_HANDLE_MOVE); } TextSelectionHelper.this.invalidate(); } else { int previousWhitespace = newSelection; while (previousWhitespace - 1 >= 0 && isInterruptedCharacter(text.charAt(previousWhitespace - 1))) { previousWhitespace--; } int distanceToNextWhitespace = Math.abs(newSelection - nextWhitespace); int distanceToPreviousWhitespace = Math.abs(newSelection - previousWhitespace); boolean newIsLetter = newSelection - 1 > 0 && isInterruptedCharacter(text.charAt(newSelection - 1)); if (snap) { snap = dx <= 0; } boolean previousIsLetter = selectionEnd > 0 && isInterruptedCharacter(text.charAt(selectionEnd - 1)); if ((newSelection > selectionEnd && distanceToNextWhitespace <= distanceToPreviousWhitespace) || (newSelection < selectionEnd && dx > 0) || !newIsLetter || (previousIsLetter && !snap)) { if (newSelection > selectionEnd && newIsLetter && !(previousIsLetter && !snap)) { selectionEnd = nextWhitespace; snap = true; } else { selectionEnd = newSelection; } if (selectionStart > selectionEnd) { int k = selectionEnd; selectionEnd = selectionStart; selectionStart = k; movingHandleStart = true; } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { textSelectionOverlay.performHapticFeedback(HapticFeedbackConstants.TEXT_HANDLE_MOVE); } TextSelectionHelper.this.invalidate(); } } } } onOffsetChanged(); } showMagnifier(lastX); } break; case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: hideMagnifier(); movingHandle = false; movingDirectionSettling = false; isOneTouch = false; if (isSelectionMode()) { showActions(); showHandleViews(); } if (scrolling) { scrolling = false; AndroidUtilities.cancelRunOnUIThread(scrollRunnable); } break; } return movingHandle; } @Override protected void onDraw(Canvas canvas) { if (!isSelectionMode()) return; int handleViewSize = AndroidUtilities.dp(22); int count = 0; int top = topOffset; pickEndView(); if (selectedView != null) { canvas.save(); float yOffset = selectedView.getY() + textY; float xOffset = selectedView.getX() + textX; canvas.translate(xOffset, yOffset); handleViewPaint.setColor(Theme.getColor(Theme.key_chat_TextSelectionCursor)); int len = getText(selectedView, false).length(); if (selectionEnd >= 0 && selectionEnd <= len) { fillLayoutForOffset(selectionEnd, layoutBlock); StaticLayout layout = layoutBlock.layout; if (layout != null) { int end = selectionEnd; int textLen = layout.getText().length(); if (end > textLen) { end = textLen; } int line = layout.getLineForOffset(end); float x = layout.getPrimaryHorizontal(end); int y = layout.getLineBottom(line); y += layoutBlock.yOffset; x += layoutBlock.xOffset; if (y + yOffset > top && y + yOffset < parentView.getMeasuredHeight()) { if (!layout.isRtlCharAt(selectionEnd)) { canvas.save(); canvas.translate(x, y); float v = interpolator.getInterpolation(handleViewProgress); canvas.scale(v, v, handleViewSize / 2f, handleViewSize / 2f); path.reset(); path.addCircle(handleViewSize / 2f, handleViewSize / 2f, handleViewSize / 2f, Path.Direction.CCW); path.addRect(0, 0, handleViewSize / 2f, handleViewSize / 2f, Path.Direction.CCW); canvas.drawPath(path, handleViewPaint); canvas.restore(); endArea.set( xOffset + x, yOffset + y - handleViewSize, xOffset + x + handleViewSize, yOffset + y + handleViewSize ); endArea.inset(-AndroidUtilities.dp(8), -AndroidUtilities.dp(8)); count++; } else { canvas.save(); canvas.translate(x - handleViewSize, y); float v = interpolator.getInterpolation(handleViewProgress); canvas.scale(v, v, handleViewSize / 2f, handleViewSize / 2f); path.reset(); path.addCircle(handleViewSize / 2f, handleViewSize / 2f, handleViewSize / 2f, Path.Direction.CCW); path.addRect(handleViewSize / 2f, 0, handleViewSize, handleViewSize / 2f, Path.Direction.CCW); canvas.drawPath(path, handleViewPaint); canvas.restore(); endArea.set( xOffset + x - handleViewSize, yOffset + y - handleViewSize, xOffset + x, yOffset + y + handleViewSize ); endArea.inset(-AndroidUtilities.dp(8), -AndroidUtilities.dp(8)); } } else { endArea.setEmpty(); } } } canvas.restore(); } pickStartView(); if (selectedView != null) { canvas.save(); float yOffset = selectedView.getY() + textY; float xOffset = selectedView.getX() + textX; canvas.translate(xOffset, yOffset); int len = getText(selectedView, false).length(); if (selectionStart >= 0 && selectionStart <= len) { fillLayoutForOffset(selectionStart, layoutBlock); StaticLayout layout = layoutBlock.layout; if (layout != null) { int line = layout.getLineForOffset(selectionStart); float x = layout.getPrimaryHorizontal(selectionStart); int y = layout.getLineBottom(line); y += layoutBlock.yOffset; x += layoutBlock.xOffset; if (y + yOffset > top && y + yOffset < parentView.getMeasuredHeight()) { if (!layout.isRtlCharAt(selectionStart)) { canvas.save(); canvas.translate(x - handleViewSize, y); float v = interpolator.getInterpolation(handleViewProgress); canvas.scale(v, v, handleViewSize / 2f, handleViewSize / 2f); path.reset(); path.addCircle(handleViewSize / 2f, handleViewSize / 2f, handleViewSize / 2f, Path.Direction.CCW); path.addRect(handleViewSize / 2f, 0, handleViewSize, handleViewSize / 2f, Path.Direction.CCW); canvas.drawPath(path, handleViewPaint); canvas.restore(); startArea.set( xOffset + x - handleViewSize, yOffset + y - handleViewSize, xOffset + x, yOffset + y + handleViewSize ); startArea.inset(-AndroidUtilities.dp(8), -AndroidUtilities.dp(8)); count++; } else { canvas.save(); canvas.translate(x, y); float v = interpolator.getInterpolation(handleViewProgress); canvas.scale(v, v, handleViewSize / 2f, handleViewSize / 2f); path.reset(); path.addCircle(handleViewSize / 2f, handleViewSize / 2f, handleViewSize / 2f, Path.Direction.CCW); path.addRect(0, 0, handleViewSize / 2f, handleViewSize / 2f, Path.Direction.CCW); canvas.drawPath(path, handleViewPaint); canvas.restore(); startArea.set( xOffset + x, yOffset + y - handleViewSize, xOffset + x + handleViewSize, yOffset + y + handleViewSize ); startArea.inset(-AndroidUtilities.dp(8), -AndroidUtilities.dp(8)); } } else { if (y + yOffset > 0 && y + yOffset - getLineHeight() < parentView.getMeasuredHeight()) { count++; } startArea.setEmpty(); } } } canvas.restore(); } if (count != 0) { if (movingHandle) { if (!movingHandleStart) { pickEndView(); } showMagnifier(lastX); if (magnifierY != magnifierYanimated) { invalidate(); } } } if (!parentIsScrolling) { showActions(); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && actionMode != null) { actionMode.invalidateContentRect(); if (actionMode != null) { ((FloatingActionMode) actionMode).updateViewLocationInWindow(); } } if (isOneTouch) { invalidate(); } } } protected void jumpToLine(int newSelection, int nextWhitespace, boolean viewChanged, float newYoffset, float oldYoffset, Cell oldSelectedView) { if (movingHandleStart) { selectionStart = nextWhitespace; if (!viewChanged && selectionStart > selectionEnd) { int k = selectionEnd; selectionEnd = selectionStart; selectionStart = k; movingHandleStart = false; } snap = true; } else { selectionEnd = nextWhitespace; if (!viewChanged && selectionStart > selectionEnd) { int k = selectionEnd; selectionEnd = selectionStart; selectionStart = k; movingHandleStart = true; } snap = true; } } protected boolean canSelect(int newSelection) { return newSelection != selectionStart && newSelection != selectionEnd; } protected boolean selectLayout(int x, int y) { return false; } protected void onOffsetChanged() { } protected void pickEndView() { } protected void pickStartView() { } protected boolean isSelectable(View child) { return true; } public void invalidate() { if (selectedView != null) { selectedView.invalidate(); } if (textSelectionOverlay != null) { textSelectionOverlay.invalidate(); } } private ActionMode.Callback createActionCallback() { final ActionMode.Callback callback = new ActionMode.Callback() { @Override public boolean onCreateActionMode(ActionMode mode, Menu menu) { menu.add(Menu.NONE, 0, 0, android.R.string.copy); menu.add(Menu.NONE, 1, 1, android.R.string.selectAll); menu.add(Menu.NONE, 2, 2, R.string.Translate); return true; } @Override public boolean onPrepareActionMode(ActionMode mode, Menu menu) { if (selectedView != null) { CharSequence charSequence = getText(selectedView, false); if (multiselect || selectionStart <= 0 && selectionEnd >= charSequence.length() - 1) { menu.getItem(1).setVisible(false); } else { menu.getItem(1).setVisible(true); } menu.getItem(2).setVisible(selectedView instanceof View); } return true; } @Override public boolean onActionItemClicked(ActionMode mode, MenuItem item) { if (!isSelectionMode()) { return true; } switch (item.getItemId()) { case 0: copyText(); return true; case 1: { CharSequence text = getText(selectedView, false); if (text == null) { return true; } selectionStart = 0; selectionEnd = text.length(); hideActions(); invalidate(); showActions(); return true; } case 2: CharSequence textS = getTextForCopy(); if (textS == null) { return true; } String urlFinal = textS.toString(); Activity activity = ProxyUtil.getOwnerActivity((((View) selectedView).getContext())); TranslateDb db = TranslateDb.currentTarget(); if (db.contains(urlFinal)) { AlertUtil.showCopyAlert(activity, db.query(urlFinal)); } else { AlertDialog pro = AlertUtil.showProgress(activity); pro.show(); Translator.translate(urlFinal, new Translator.Companion.TranslateCallBack() { @Override public void onSuccess(@NotNull String translation) { pro.dismiss(); AlertUtil.showCopyAlert(activity, translation); } @Override public void onFailed(boolean unsupported, @NotNull String message) { pro.dismiss(); AlertUtil.showTransFailedDialog(activity, unsupported, message, () -> { pro.show(); Translator.translate(urlFinal, this); }); } }); } default: clear(); } return true; } @Override public void onDestroyActionMode(ActionMode mode) { if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.M) { clear(); } } }; if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { ActionMode.Callback2 callback2 = new ActionMode.Callback2() { @Override public boolean onCreateActionMode(ActionMode mode, Menu menu) { return callback.onCreateActionMode(mode, menu); } @Override public boolean onPrepareActionMode(ActionMode mode, Menu menu) { return callback.onPrepareActionMode(mode, menu); } @Override public boolean onActionItemClicked(ActionMode mode, MenuItem item) { return callback.onActionItemClicked(mode, item); } @Override public void onDestroyActionMode(ActionMode mode) { callback.onDestroyActionMode(mode); } @Override public void onGetContentRect(ActionMode mode, View view, Rect outRect) { if (!isSelectionMode()) { return; } pickStartView(); int x1 = 0; int y1 = 1; if (selectedView != null) { int lineHeight = -getLineHeight(); int[] coords = offsetToCord(selectionStart); x1 = coords[0] + textX; y1 = (int) (coords[1] + textY + selectedView.getY()) + lineHeight / 2 - AndroidUtilities.dp(4); if (y1 < 1) y1 = 1; } int x2 = parentView.getWidth(); pickEndView(); if (selectedView != null) { int[] coords = offsetToCord(selectionEnd); x2 = coords[0] + textX; } outRect.set( Math.min(x1, x2), y1, Math.max(x1, x2), y1 + 1 ); } }; return callback2; } return callback; } private void copyText() { if (!isSelectionMode()) { return; } CharSequence str = getTextForCopy(); if (str == null) { return; } android.content.ClipboardManager clipboard = (android.content.ClipboardManager) ApplicationLoader.applicationContext.getSystemService(Context.CLIPBOARD_SERVICE); android.content.ClipData clip = android.content.ClipData.newPlainText("label", str); clipboard.setPrimaryClip(clip); hideActions(); clear(true); if (TextSelectionHelper.this.callback != null) { TextSelectionHelper.this.callback.onTextCopied(); } } protected CharSequence getTextForCopy() { CharSequence text = getText(selectedView, false); if (text != null) { return text.subSequence(selectionStart, selectionEnd); } return null; } protected int[] offsetToCord(int offset) { fillLayoutForOffset(offset, layoutBlock); StaticLayout layout = layoutBlock.layout; if (layout == null || offset > layout.getText().length()) { return tmpCoord; } int line = layout.getLineForOffset(offset); tmpCoord[0] = (int) (layout.getPrimaryHorizontal(offset) + layoutBlock.xOffset); tmpCoord[1] = layout.getLineBottom(line); tmpCoord[1] += layoutBlock.yOffset; return tmpCoord; } protected void drawSelection(Canvas canvas, StaticLayout layout, int selectionStart, int selectionEnd) { int startLine = layout.getLineForOffset(selectionStart); int endLine = layout.getLineForOffset(selectionEnd); if (startLine == endLine) { drawLine(canvas, layout, startLine, selectionStart, selectionEnd); } else { int end = layout.getLineEnd(startLine); if (layout.getParagraphDirection(startLine) != StaticLayout.DIR_RIGHT_TO_LEFT && end > 0) { end--; CharSequence text = layout.getText(); int s = (int) layout.getPrimaryHorizontal(end); int e; if (layout.isRtlCharAt(end)) { int endIndex = end; while (layout.isRtlCharAt(endIndex)) { if (endIndex == 0) break; endIndex--; } e = layout.getLineForOffset(endIndex) == layout.getLineForOffset(end) ? (int) layout.getPrimaryHorizontal(endIndex + 1) : (int) layout.getLineLeft(startLine); } else { e = (int) layout.getLineRight(startLine); } int l = Math.min(s, e); int r = Math.max(s, e); if (end > 0 && end < text.length() && !Character.isWhitespace(text.charAt(end - 1))) { canvas.drawRect(l, layout.getLineTop(startLine), r, layout.getLineBottom(startLine), selectionPaint); } } drawLine(canvas, layout, startLine, selectionStart, end); drawLine(canvas, layout, endLine, layout.getLineStart(endLine), selectionEnd); for (int i = startLine + 1; i < endLine; i++) { int s = (int) layout.getLineLeft(i); int e = (int) layout.getLineRight(i); int l = Math.min(s, e); int r = Math.max(s, e); canvas.drawRect(l, layout.getLineTop(i), r, layout.getLineBottom(i), selectionPaint); } } } private void drawLine(Canvas canvas, StaticLayout layout, int line, int start, int end) { layout.getSelectionPath(start, end, path); if (path.lastBottom < layout.getLineBottom(line)) { int lineBottom = layout.getLineBottom(line); int lineTop = layout.getLineTop(line); float lineH = lineBottom - lineTop; float lineHWithoutSpaсing = path.lastBottom - lineTop; canvas.save(); canvas.scale(1f, lineH / lineHWithoutSpaсing, 0, lineTop); canvas.drawPath(path, selectionPaint); canvas.restore(); } else { canvas.drawPath(path, selectionPaint); } } private static class LayoutBlock { StaticLayout layout; float yOffset; float xOffset; } public static class Callback { public void onStateChanged(boolean isSelected) { } ; public void onTextCopied() { } ; } protected void fillLayoutForOffset(int offset, LayoutBlock layoutBlock) { fillLayoutForOffset(offset, layoutBlock, false); } protected abstract CharSequence getText(Cell view, boolean maybe); protected abstract int getCharOffsetFromCord(int x, int y, int offsetX, int offsetY, Cell view, boolean maybe); protected abstract void fillLayoutForOffset(int offset, LayoutBlock layoutBlock, boolean maybe); protected abstract int getLineHeight(); protected abstract void onTextSelected(Cell newView, Cell oldView); public static class ChatListTextSelectionHelper extends TextSelectionHelper { SparseArray animatorSparseArray = new SparseArray<>(); private boolean isDescription; private boolean maybeIsDescription; public static int TYPE_MESSAGE = 0; public static int TYPE_CAPTION = 1; public static int TYPE_DESCRIPTION = 2; @Override protected int getLineHeight() { if (selectedView != null && selectedView.getMessageObject() != null) { MessageObject object = selectedView.getMessageObject(); StaticLayout layout = null; if (isDescription) { layout = selectedView.getDescriptionlayout(); } else if (selectedView.hasCaptionLayout()) { layout = selectedView.getCaptionLayout(); } else if (object.textLayoutBlocks != null) { layout = object.textLayoutBlocks.get(0).textLayout; } if (layout == null) { return 0; } int lineHeight = layout.getLineBottom(0) - layout.getLineTop(0); return lineHeight; } return 0; } public void setMessageObject(ChatMessageCell chatMessageCell) { this.maybeSelectedView = chatMessageCell; MessageObject messageObject = chatMessageCell.getMessageObject(); if (maybeIsDescription && chatMessageCell.getDescriptionlayout() != null) { textArea.set( maybeTextX, maybeTextY, maybeTextX + chatMessageCell.getDescriptionlayout().getWidth(), maybeTextY + chatMessageCell.getDescriptionlayout().getHeight() ); } else if (chatMessageCell.hasCaptionLayout()) { textArea.set(maybeTextX, maybeTextY, maybeTextX + chatMessageCell.getCaptionLayout().getWidth(), maybeTextY + chatMessageCell.getCaptionLayout().getHeight()); } else if (messageObject != null && messageObject.textLayoutBlocks.size() > 0) { MessageObject.TextLayoutBlock block = messageObject.textLayoutBlocks.get(messageObject.textLayoutBlocks.size() - 1); textArea.set( maybeTextX, maybeTextY, maybeTextX + block.textLayout.getWidth(), (int) (maybeTextY + block.textYOffset + block.textLayout.getHeight()) ); } } @Override protected CharSequence getText(ChatMessageCell cell, boolean maybe) { if (cell == null || cell.getMessageObject() == null) { return null; } if (maybe ? maybeIsDescription : isDescription) { return cell.getDescriptionlayout().getText(); } if (cell.hasCaptionLayout()) { return cell.getCaptionLayout().getText(); } return cell.getMessageObject().messageText; } @Override protected void onTextSelected(ChatMessageCell newView, ChatMessageCell oldView) { boolean idChanged = oldView == null || (oldView.getMessageObject() != null && oldView.getMessageObject().getId() != newView.getMessageObject().getId()); selectedCellId = newView.getMessageObject().getId(); enterProgress = 0; isDescription = maybeIsDescription; Animator oldAnimator = animatorSparseArray.get(selectedCellId); if (oldAnimator != null) { oldAnimator.removeAllListeners(); oldAnimator.cancel(); } ValueAnimator animator = ValueAnimator.ofFloat(0, 1f); animator.addUpdateListener(animation -> { enterProgress = (float) animation.getAnimatedValue(); if (textSelectionOverlay != null) { textSelectionOverlay.invalidate(); } if (selectedView != null && selectedView.getCurrentMessagesGroup() == null && idChanged) { selectedView.setSelectedBackgroundProgress(1f - enterProgress); } }); animator.setDuration(250); animator.start(); animatorSparseArray.put(selectedCellId, animator); if (!idChanged) { newView.setSelectedBackgroundProgress(0f); } SharedConfig.removeTextSelectionHint(); } public void draw(MessageObject messageObject, MessageObject.TextLayoutBlock block, Canvas canvas) { if (selectedView == null || selectedView.getMessageObject() == null || isDescription) { return; } MessageObject selectedMessageObject = selectedView.getMessageObject(); if (selectedMessageObject == null || selectedMessageObject.textLayoutBlocks == null) { return; } if (messageObject.getId() == selectedCellId) { int selectionStart = this.selectionStart; int selectionEnd = this.selectionEnd; if (selectedMessageObject.textLayoutBlocks.size() > 1) { if (selectionStart < block.charactersOffset) { selectionStart = block.charactersOffset; } if (selectionStart > block.charactersEnd) { selectionStart = block.charactersEnd; } if (selectionEnd < block.charactersOffset) { selectionEnd = block.charactersOffset; } if (selectionEnd > block.charactersEnd) { selectionEnd = block.charactersEnd; } } if (selectionStart != selectionEnd) { if (selectedMessageObject.isOutOwner()) { selectionPaint.setColor(Theme.getColor(Theme.key_chat_outTextSelectionHighlight)); } else { selectionPaint.setColor(Theme.getColor(key_chat_inTextSelectionHighlight)); } drawSelection(canvas, block.textLayout, selectionStart, selectionEnd); } } } protected int getCharOffsetFromCord(int x, int y, int offsetX, int offsetY, ChatMessageCell cell, boolean maybe) { if (cell == null) { return 0; } int line = -1; x -= offsetX; y -= offsetY; StaticLayout lastLayout; float yOffset = 0; boolean isDescription = maybe ? maybeIsDescription : this.isDescription; if (isDescription) { lastLayout = cell.getDescriptionlayout(); } else if (cell.hasCaptionLayout()) { lastLayout = cell.getCaptionLayout(); } else { MessageObject.TextLayoutBlock lastBlock = cell.getMessageObject().textLayoutBlocks.get(cell.getMessageObject().textLayoutBlocks.size() - 1); lastLayout = lastBlock.textLayout; yOffset = lastBlock.textYOffset; } if (y < 0) { y = 1; } if (y > yOffset + lastLayout.getLineBottom(lastLayout.getLineCount() - 1)) { y = (int) (yOffset + lastLayout.getLineBottom(lastLayout.getLineCount() - 1) - 1); } fillLayoutForCoords(x, y, cell, layoutBlock, maybe); if (layoutBlock.layout == null) { return -1; } StaticLayout layout = layoutBlock.layout; x -= layoutBlock.xOffset; for (int i = 0; i < layout.getLineCount(); i++) { if (y > layoutBlock.yOffset + layout.getLineTop(i) && y < layoutBlock.yOffset + layout.getLineBottom(i)) { line = i; break; } } if (line >= 0) { return layout.getOffsetForHorizontal(line, x); } return -1; } private void fillLayoutForCoords(int x, int y, ChatMessageCell cell, LayoutBlock layoutBlock, boolean maybe) { if (cell == null) { return; } MessageObject messageObject = cell.getMessageObject(); if (maybe ? maybeIsDescription : isDescription) { layoutBlock.layout = cell.getDescriptionlayout(); layoutBlock.yOffset = layoutBlock.xOffset = 0; return; } if (cell.hasCaptionLayout()) { layoutBlock.layout = cell.getCaptionLayout(); layoutBlock.yOffset = layoutBlock.xOffset = 0; return; } for (int i = 0; i < messageObject.textLayoutBlocks.size(); i++) { MessageObject.TextLayoutBlock block = messageObject.textLayoutBlocks.get(i); if (y >= block.textYOffset && y <= block.textYOffset + block.height) { layoutBlock.layout = block.textLayout; layoutBlock.yOffset = block.textYOffset; layoutBlock.xOffset = -(block.isRtl() ? (int) Math.ceil(messageObject.textXOffset) : 0); return; } } } @Override protected void fillLayoutForOffset(int offset, LayoutBlock layoutBlock, boolean maybe) { ChatMessageCell selectedView = maybe ? maybeSelectedView : this.selectedView; if (selectedView == null) { layoutBlock.layout = null; return; } MessageObject messageObject = selectedView.getMessageObject(); if (isDescription) { layoutBlock.layout = selectedView.getDescriptionlayout(); layoutBlock.xOffset = layoutBlock.yOffset = 0; return; } if (selectedView.hasCaptionLayout()) { layoutBlock.layout = selectedView.getCaptionLayout(); layoutBlock.xOffset = layoutBlock.yOffset = 0; return; } if (messageObject.textLayoutBlocks == null) { layoutBlock.layout = null; return; } if (messageObject.textLayoutBlocks.size() == 1) { layoutBlock.layout = messageObject.textLayoutBlocks.get(0).textLayout; layoutBlock.yOffset = 0; layoutBlock.xOffset = -(messageObject.textLayoutBlocks.get(0).isRtl() ? (int) Math.ceil(messageObject.textXOffset) : 0); return; } for (int i = 0; i < messageObject.textLayoutBlocks.size(); i++) { MessageObject.TextLayoutBlock block = messageObject.textLayoutBlocks.get(i); if (offset >= block.charactersOffset && offset <= block.charactersEnd) { layoutBlock.layout = messageObject.textLayoutBlocks.get(i).textLayout; layoutBlock.yOffset = messageObject.textLayoutBlocks.get(i).textYOffset; layoutBlock.xOffset = -(block.isRtl() ? (int) Math.ceil(messageObject.textXOffset) : 0); return; } } layoutBlock.layout = null; } @Override protected void onExitSelectionMode(boolean instant) { if (selectedView != null && selectedView.isDrawingSelectionBackground() && !instant) { final ChatMessageCell cell = selectedView; final int id = selectedView.getMessageObject().getId(); Animator oldAnimator = animatorSparseArray.get(id); if (oldAnimator != null) { oldAnimator.removeAllListeners(); oldAnimator.cancel(); } cell.setSelectedBackgroundProgress(0.01f); ValueAnimator animator = ValueAnimator.ofFloat(0.01f, 1f); animator.addUpdateListener(animation -> { float exit = (float) animation.getAnimatedValue(); if (cell.getMessageObject() != null && cell.getMessageObject().getId() == id) { cell.setSelectedBackgroundProgress(exit); } }); animator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { cell.setSelectedBackgroundProgress(0.0f); } }); animator.setDuration(300); animator.start(); animatorSparseArray.put(id, animator); } } public void onChatMessageCellAttached(ChatMessageCell chatMessageCell) { if (chatMessageCell.getMessageObject() != null && chatMessageCell.getMessageObject().getId() == selectedCellId) { this.selectedView = chatMessageCell; } } public void onChatMessageCellDetached(ChatMessageCell chatMessageCell) { if (chatMessageCell.getMessageObject() != null && chatMessageCell.getMessageObject().getId() == selectedCellId) { this.selectedView = null; } } public void drawCaption(boolean isOut, StaticLayout captionLayout, Canvas canvas) { if (isDescription) { return; } if (isOut) { selectionPaint.setColor(Theme.getColor(Theme.key_chat_outTextSelectionHighlight)); } else { selectionPaint.setColor(Theme.getColor(key_chat_inTextSelectionHighlight)); } drawSelection(canvas, captionLayout, selectionStart, selectionEnd); } public void drawDescription(boolean isOut, StaticLayout layout, Canvas canvas) { if (!isDescription) { return; } if (isOut) { selectionPaint.setColor(Theme.getColor(Theme.key_chat_outTextSelectionHighlight)); } else { selectionPaint.setColor(Theme.getColor(key_chat_inTextSelectionHighlight)); } drawSelection(canvas, layout, selectionStart, selectionEnd); } @Override public void invalidate() { super.invalidate(); if (selectedView != null && selectedView.getCurrentMessagesGroup() != null) { parentView.invalidate(); } } public void cancelAllAnimators() { for (int i = 0; i < animatorSparseArray.size(); i++) { Animator animator = animatorSparseArray.get(animatorSparseArray.keyAt(i)); animator.cancel(); } animatorSparseArray.clear(); } public void setIsDescription(boolean b) { maybeIsDescription = b; } @Override public void clear(boolean instant) { super.clear(instant); isDescription = false; } public int getTextSelectionType(ChatMessageCell cell) { if (isDescription) { return TYPE_DESCRIPTION; } if (cell.hasCaptionLayout()) { return TYPE_CAPTION; } return TYPE_MESSAGE; } public void updateTextPosition(int textX, int textY) { if (this.textX != textX || this.textY != textY) { this.textX = textX; this.textY = textY; invalidate(); } } public void checkDataChanged(MessageObject messageObject) { if (selectedCellId == messageObject.getId()) { clear(true); } } } public static class ArticleTextSelectionHelper extends TextSelectionHelper { int startViewPosition = -1; int startViewChildPosition = -1; int startViewOffset; int endViewPosition = -1; int endViewChildPosition = -1; int endViewOffset; int maybeTextIndex = -1; SparseArray textByPosition = new SparseArray<>(); SparseArray prefixTextByPosition = new SparseArray<>(); SparseIntArray childCountByPosition = new SparseIntArray(); public LinearLayoutManager layoutManager; public ArticleTextSelectionHelper() { multiselect = true; showActionsAsPopupAlways = true; } public ArrayList arrayList = new ArrayList<>(); @Override protected CharSequence getText(ArticleSelectableView view, boolean maybe) { arrayList.clear(); view.fillTextLayoutBlocks(arrayList); int i; if (maybe) { i = maybeTextIndex; } else { i = startPeek ? startViewChildPosition : endViewChildPosition; } if (arrayList.isEmpty() || i < 0) { return ""; } return arrayList.get(i).getLayout().getText(); } @Override protected int getCharOffsetFromCord(int x, int y, int offsetX, int offsetY, ArticleSelectableView view, boolean maybe) { if (view == null) { return -1; } int line = -1; x -= offsetX; y -= offsetY; arrayList.clear(); view.fillTextLayoutBlocks(arrayList); int childIndex; if (maybe) { childIndex = maybeTextIndex; } else { childIndex = startPeek ? startViewChildPosition : endViewChildPosition; } StaticLayout layout = arrayList.get(childIndex).getLayout(); if (x < 0) { x = 1; } if (y < 0) { y = 1; } if (x > layout.getWidth()) { x = layout.getWidth(); } if (y > layout.getLineBottom(layout.getLineCount() - 1)) { y = (int) (layout.getLineBottom(layout.getLineCount() - 1) - 1); } for (int i = 0; i < layout.getLineCount(); i++) { if (y > layout.getLineTop(i) && y < layout.getLineBottom(i)) { line = i; break; } } if (line >= 0) { return layout.getOffsetForHorizontal(line, x); } return -1; } @Override protected void fillLayoutForOffset(int offset, LayoutBlock layoutBlock, boolean maybe) { arrayList.clear(); ArticleSelectableView selectedView = maybe ? maybeSelectedView : this.selectedView; if (selectedView == null) { layoutBlock.layout = null; return; } selectedView.fillTextLayoutBlocks(arrayList); if (maybe) { layoutBlock.layout = arrayList.get(maybeTextIndex).getLayout(); } else { int index = (startPeek ? startViewChildPosition : endViewChildPosition); if (index < 0 || index >= arrayList.size()) { layoutBlock.layout = null; return; } layoutBlock.layout = arrayList.get(index).getLayout(); } layoutBlock.xOffset = layoutBlock.yOffset = 0; } @Override protected int getLineHeight() { if (selectedView == null) { return 0; } else { arrayList.clear(); selectedView.fillTextLayoutBlocks(arrayList); int index = startPeek ? startViewChildPosition : endViewChildPosition; if (index < 0 || index >= arrayList.size()) { return 0; } StaticLayout layout = arrayList.get(index).getLayout(); int min = Integer.MAX_VALUE; for (int i = 0; i < layout.getLineCount(); i++) { int h = layout.getLineBottom(i) - layout.getLineTop(i); if (h < min) min = h; } return min; } } public void trySelect(View view) { if (maybeSelectedView != null) { startSelectionRunnable.run(); } } public void setMaybeView(int x, int y, View parentView) { if (parentView instanceof ArticleSelectableView) { capturedX = x; capturedY = y; maybeSelectedView = (ArticleSelectableView) parentView; maybeTextIndex = findClosestLayoutIndex(x, y, maybeSelectedView); if (maybeTextIndex < 0) { maybeSelectedView = null; } else { maybeTextX = arrayList.get(maybeTextIndex).getX(); maybeTextY = arrayList.get(maybeTextIndex).getY(); } } } private int findClosestLayoutIndex(int x, int y, ArticleSelectableView maybeSelectedView) { if (maybeSelectedView instanceof ViewGroup) { ViewGroup parent = ((ViewGroup) maybeSelectedView); for (int i = 0; i < parent.getChildCount(); i++) { View child = parent.getChildAt(i); if (child instanceof ArticleSelectableView && y > child.getY() && y < child.getY() + child.getHeight()) { return findClosestLayoutIndex((int) (x - child.getX()), (int) (y - child.getY()), (ArticleSelectableView) child); } } } arrayList.clear(); maybeSelectedView.fillTextLayoutBlocks(arrayList); if (arrayList.isEmpty()) { return -1; } else { int minDistance = Integer.MAX_VALUE; int minIndex = -1; for (int i = arrayList.size() - 1; i >= 0; i--) { TextLayoutBlock block = arrayList.get(i); int top = block.getY(); int bottom = top + block.getLayout().getHeight(); if (y >= top && y < bottom) { minDistance = 0; minIndex = i; break; } int d = Math.min(Math.abs(y - top), Math.abs(y - bottom)); if (d < minDistance) { minDistance = d; minIndex = i; } } if (minIndex < 0) { return -1; } int row = arrayList.get(minIndex).getRow(); if (row > 0) { if (minDistance < AndroidUtilities.dp(24)) { int minDistanceX = Integer.MAX_VALUE; int minIndexX = minIndex; for (int i = arrayList.size() - 1; i >= 0; i--) { TextLayoutBlock block = arrayList.get(i); if (block.getRow() == row) { int left = block.getX(); int right = block.getX() + block.getLayout().getWidth(); if (x >= left && x <= right) { return i; } else { int d = Math.min(Math.abs(x - left), Math.abs(x - right)); if (d < minDistanceX) { minDistanceX = d; minIndexX = i; } } } } return minIndexX; } } return minIndex; } } public void draw(Canvas canvas, ArticleSelectableView view, int i) { selectionPaint.setColor(Theme.getColor(key_chat_inTextSelectionHighlight)); int position = getAdapterPosition(view); if (position < 0) { return; } arrayList.clear(); view.fillTextLayoutBlocks(arrayList); if (!arrayList.isEmpty()) { TextLayoutBlock layoutBlock = arrayList.get(i); int endOffset = endViewOffset; int textLen = layoutBlock.getLayout().getText().length(); if (endOffset > textLen) { endOffset = textLen; } if (position == startViewPosition && position == endViewPosition) { if (startViewChildPosition == endViewChildPosition && startViewChildPosition == i) { drawSelection(canvas, layoutBlock.getLayout(), startViewOffset, endOffset); } else if (i == startViewChildPosition) { drawSelection(canvas, layoutBlock.getLayout(), startViewOffset, textLen); } else if (i == endViewChildPosition) { drawSelection(canvas, layoutBlock.getLayout(), 0, endOffset); } else if (i > startViewChildPosition && i < endViewChildPosition) { drawSelection(canvas, layoutBlock.getLayout(), 0, textLen); } } else if (position == startViewPosition && startViewChildPosition == i) { drawSelection(canvas, layoutBlock.getLayout(), startViewOffset, textLen); } else if (position == endViewPosition && endViewChildPosition == i) { drawSelection(canvas, layoutBlock.getLayout(), 0, endOffset); } else if (position > startViewPosition && position < endViewPosition || (position == startViewPosition && i > startViewChildPosition) || (position == endViewPosition && i < endViewChildPosition)) { drawSelection(canvas, layoutBlock.getLayout(), 0, textLen); } } } private int getAdapterPosition(ArticleSelectableView view) { View child = (View) view; ViewParent parent = child.getParent(); while (parent != this.parentView && parent != null) { if (parent instanceof View) { child = (View) parent; parent = child.getParent(); } else { parent = null; break; } } if (parent != null) { if (parentRecyclerView != null) { return parentRecyclerView.getChildAdapterPosition(child); } else { return parentView.indexOfChild(child); } } return -1; } public boolean isSelectable(View child) { if (child instanceof ArticleSelectableView) { arrayList.clear(); ((ArticleSelectableView) child).fillTextLayoutBlocks(arrayList); if (child instanceof ArticleViewer.BlockTableCell) { return true; } else { return !arrayList.isEmpty(); } } return false; } @Override protected void onTextSelected(ArticleSelectableView newView, ArticleSelectableView oldView) { int position = getAdapterPosition(newView); if (position < 0) { return; } startViewPosition = endViewPosition = position; startViewChildPosition = endViewChildPosition = maybeTextIndex; arrayList.clear(); newView.fillTextLayoutBlocks(arrayList); int n = arrayList.size(); childCountByPosition.put(position, n); for (int i = 0; i < n; i++) { textByPosition.put(position + (i << 16), arrayList.get(i).getLayout().getText()); prefixTextByPosition.put(position + (i << 16), arrayList.get(i).getPrefix()); } } protected void onNewViewSelected(ArticleSelectableView oldView, ArticleSelectableView newView, int childPosition) { int position = getAdapterPosition(newView); int oldPosition = -1; if (oldView != null) { oldPosition = getAdapterPosition(oldView); } invalidate(); if (movingDirectionSettling && startViewPosition == endViewPosition) { if (position == startViewPosition) { if (childPosition < startViewChildPosition) { startViewChildPosition = childPosition; pickStartView(); movingHandleStart = true; startViewOffset = selectionEnd; selectionStart = selectionEnd - 1; } else { endViewChildPosition = childPosition; pickEndView(); movingHandleStart = false; endViewOffset = 0; } } else if (position < startViewPosition) { startViewPosition = position; startViewChildPosition = childPosition; pickStartView(); movingHandleStart = true; startViewOffset = selectionEnd; selectionStart = selectionEnd - 1; } else { endViewPosition = position; endViewChildPosition = childPosition; pickEndView(); movingHandleStart = false; endViewOffset = 0; } } else if (movingHandleStart) { if (position == oldPosition) { if (childPosition <= endViewChildPosition || position < endViewPosition) { startViewPosition = position; startViewChildPosition = childPosition; pickStartView(); startViewOffset = selectionEnd; } else { endViewPosition = position; startViewChildPosition = endViewChildPosition; endViewChildPosition = childPosition; startViewOffset = endViewOffset; pickEndView(); endViewOffset = 0; movingHandleStart = false; } } else if (position <= endViewPosition) { startViewPosition = position; startViewChildPosition = childPosition; pickStartView(); startViewOffset = selectionEnd; } else { endViewPosition = position; startViewChildPosition = endViewChildPosition; endViewChildPosition = childPosition; startViewOffset = endViewOffset; pickEndView(); endViewOffset = 0; movingHandleStart = false; } } else { if (position == oldPosition) { if (childPosition >= startViewChildPosition || position > startViewPosition) { endViewPosition = position; endViewChildPosition = childPosition; pickEndView(); endViewOffset = 0; } else { startViewPosition = position; endViewChildPosition = startViewChildPosition; startViewChildPosition = childPosition; endViewOffset = startViewOffset; pickStartView(); movingHandleStart = true; startViewOffset = selectionEnd; } } else if (position >= startViewPosition) { endViewPosition = position; endViewChildPosition = childPosition; pickEndView(); endViewOffset = 0; } else { startViewPosition = position; endViewChildPosition = startViewChildPosition; startViewChildPosition = childPosition; endViewOffset = startViewOffset; pickStartView(); movingHandleStart = true; startViewOffset = selectionEnd; } } arrayList.clear(); newView.fillTextLayoutBlocks(arrayList); int n = arrayList.size(); childCountByPosition.put(position, n); for (int i = 0; i < n; i++) { textByPosition.put(position + (i << 16), arrayList.get(i).getLayout().getText()); prefixTextByPosition.put(position + (i << 16), arrayList.get(i).getPrefix()); } } boolean startPeek; protected void pickEndView() { if (!isSelectionMode()) { return; } startPeek = false; if (endViewPosition >= 0) { ArticleSelectableView view = null; if (layoutManager != null) { view = (ArticleSelectableView) layoutManager.findViewByPosition(endViewPosition); } else if (endViewPosition < parentView.getChildCount()) { view = (ArticleSelectableView) parentView.getChildAt(endViewPosition); } if (view == null) { selectedView = null; return; } selectedView = view; if (startViewPosition != endViewPosition) { selectionStart = 0; } else if (startViewChildPosition != endViewChildPosition) { selectionStart = 0; } else { selectionStart = startViewOffset; } selectionEnd = endViewOffset; CharSequence text = getText(selectedView, false); if (selectionEnd > text.length()) { selectionEnd = text.length(); } arrayList.clear(); selectedView.fillTextLayoutBlocks(arrayList); if (!arrayList.isEmpty()) { textX = arrayList.get(endViewChildPosition).getX(); textY = arrayList.get(endViewChildPosition).getY(); } } } protected void pickStartView() { if (!isSelectionMode()) { return; } startPeek = true; if (startViewPosition >= 0) { ArticleSelectableView view = null; if (layoutManager != null) { view = (ArticleSelectableView) layoutManager.findViewByPosition(startViewPosition); } else if (endViewPosition < parentView.getChildCount()) { view = (ArticleSelectableView) parentView.getChildAt(startViewPosition); } if (view == null) { selectedView = null; return; } selectedView = view; if (startViewPosition != endViewPosition) { selectionEnd = getText(selectedView, false).length(); } else if (startViewChildPosition != endViewChildPosition) { selectionEnd = getText(selectedView, false).length(); } else { selectionEnd = endViewOffset; } selectionStart = startViewOffset; arrayList.clear(); selectedView.fillTextLayoutBlocks(arrayList); if (!arrayList.isEmpty()) { textX = arrayList.get(startViewChildPosition).getX(); textY = arrayList.get(startViewChildPosition).getY(); } } } protected void onOffsetChanged() { int position = getAdapterPosition(selectedView); int childPosition = startPeek ? startViewChildPosition : endViewChildPosition; if (position == startViewPosition && childPosition == startViewChildPosition) { startViewOffset = selectionStart; } if (position == endViewPosition && childPosition == endViewChildPosition) { endViewOffset = selectionEnd; } } public void invalidate() { super.invalidate(); for (int i = 0; i < parentView.getChildCount(); i++) { parentView.getChildAt(i).invalidate(); } } @Override public void clear(boolean instant) { super.clear(instant); startViewPosition = -1; endViewPosition = -1; startViewChildPosition = -1; endViewChildPosition = -1; textByPosition.clear(); childCountByPosition.clear(); } @Override protected CharSequence getTextForCopy() { SpannableStringBuilder stringBuilder = new SpannableStringBuilder(); for (int i = startViewPosition; i <= endViewPosition; i++) { if (i == startViewPosition) { int n = startViewPosition == endViewPosition ? endViewChildPosition : childCountByPosition.get(i) - 1; for (int k = startViewChildPosition; k <= n; k++) { CharSequence text = textByPosition.get(i + (k << 16)); if (text == null) { continue; } if (startViewPosition == endViewPosition && k == endViewChildPosition && k == startViewChildPosition) { int e = endViewOffset; int s = startViewOffset; if (e < s) { int tmp = s; s = e; e = tmp; } if (s < text.length()) { if (e > text.length()) e = text.length(); stringBuilder.append(text.subSequence(s, e)); stringBuilder.append('\n'); } } else if (startViewPosition == endViewPosition && k == endViewChildPosition) { CharSequence prefix = prefixTextByPosition.get(i + (k << 16)); if (prefix != null) { stringBuilder.append(prefix).append(' '); } int e = endViewOffset; if (e > text.length()) e = text.length(); stringBuilder.append(text.subSequence(0, e)); stringBuilder.append('\n'); } else if (k == startViewChildPosition) { int s = startViewOffset; if (s < text.length()) { stringBuilder.append(text.subSequence(s, text.length())); stringBuilder.append('\n'); } } else { CharSequence prefix = prefixTextByPosition.get(i + (k << 16)); if (prefix != null) { stringBuilder.append(prefix).append(' '); } stringBuilder.append(text); stringBuilder.append('\n'); } } } else if (i == endViewPosition) { for (int k = 0; k <= endViewChildPosition; k++) { CharSequence text = textByPosition.get(i + (k << 16)); if (text == null) { continue; } if (startViewPosition == endViewPosition && k == endViewChildPosition && k == startViewChildPosition) { int e = endViewOffset; int s = startViewOffset; if (s < text.length()) { if (e > text.length()) e = text.length(); stringBuilder.append(text.subSequence(s, e)); stringBuilder.append('\n'); } } else if (k == endViewChildPosition) { CharSequence prefix = prefixTextByPosition.get(i + (k << 16)); if (prefix != null) { stringBuilder.append(prefix).append(' '); } int e = endViewOffset; if (e > text.length()) e = text.length(); stringBuilder.append(text.subSequence(0, e)); stringBuilder.append('\n'); } else { CharSequence prefix = prefixTextByPosition.get(i + (k << 16)); if (prefix != null) { stringBuilder.append(prefix).append(' '); } stringBuilder.append(text); stringBuilder.append('\n'); } } } else { int n = childCountByPosition.get(i); for (int k = startViewChildPosition; k < n; k++) { CharSequence prefix = prefixTextByPosition.get(i + (k << 16)); if (prefix != null) { stringBuilder.append(prefix).append(' '); } stringBuilder.append(textByPosition.get(i + (k << 16))); stringBuilder.append('\n'); } } } if (stringBuilder.length() > 0) { IgnoreCopySpannable[] spans = stringBuilder.getSpans(0, stringBuilder.length() - 1, IgnoreCopySpannable.class); for (IgnoreCopySpannable span : spans) { int end = stringBuilder.getSpanEnd(span); int start = stringBuilder.getSpanStart(span); stringBuilder.delete(start, end); } return stringBuilder.subSequence(0, stringBuilder.length() - 1); } else { return null; } } @Override protected boolean selectLayout(int x, int y) { if (!multiselect) { return false; } if (!(y > selectedView.getTop() && y < selectedView.getBottom())) { int n = parentView.getChildCount(); for (int i = 0; i < n; i++) { if (isSelectable(parentView.getChildAt(i))) { ArticleSelectableView child = (ArticleSelectableView) parentView.getChildAt(i); if (y > child.getTop() && y < child.getBottom()) { int index = findClosestLayoutIndex((int) (x - child.getX()), (int) (y - child.getY()), child); if (index >= 0) { onNewViewSelected(selectedView, child, index); selectedView = child; return true; } else { return false; } } } } return false; } else { int currentChildPosition = startPeek ? startViewChildPosition : endViewChildPosition; int k = findClosestLayoutIndex((int) (x - selectedView.getX()), (int) (y - selectedView.getY()), selectedView); if (k != currentChildPosition && k >= 0) { onNewViewSelected(selectedView, selectedView, k); return true; } } return false; } @Override protected boolean canSelect(int newSelection) { if (startViewPosition == endViewPosition && startViewChildPosition == endViewChildPosition) { return super.canSelect(newSelection); } return true; } @Override protected void jumpToLine(int newSelection, int nextWhitespace, boolean viewChanged, float newYoffset, float oldYoffset, ArticleSelectableView oldSelectedView) { if (viewChanged && oldSelectedView == selectedView && oldYoffset == newYoffset) { if (movingHandleStart) { selectionStart = newSelection; } else { selectionEnd = newSelection; } } else { super.jumpToLine(newSelection, nextWhitespace, viewChanged, newYoffset, oldYoffset, oldSelectedView); } } @Override protected boolean canShowActions() { if (layoutManager == null) { return true; } int firstV = layoutManager.findFirstVisibleItemPosition(); int lastV = layoutManager.findLastVisibleItemPosition(); if ((firstV >= startViewPosition && firstV <= endViewPosition) || (lastV >= startViewPosition && lastV <= endViewPosition)) { return true; } if (startViewPosition >= firstV && endViewPosition <= lastV) { return true; } return false; } } public interface ArticleSelectableView extends SelectableView { void fillTextLayoutBlocks(ArrayList blocks); } public interface SelectableView { int getBottom(); int getTop(); float getX(); float getY(); int getMeasuredWidth(); void invalidate(); } public interface TextLayoutBlock { StaticLayout getLayout(); int getX(); int getY(); int getRow(); default CharSequence getPrefix() { return null; } } public static class IgnoreCopySpannable { } private static class PathWithSavedBottom extends Path { float lastBottom = 0; @Override public void reset() { super.reset(); lastBottom = 0; } @Override public void addRect(float left, float top, float right, float bottom, Direction dir) { super.addRect(left, top, right, bottom, dir); if (bottom > lastBottom) { lastBottom = bottom; } } } }