NekoX/TMessagesProj/src/main/java/org/telegram/ui/Cells/TextSelectionHelper.java

2659 lines
112 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<Cell extends TextSelectionHelper.SelectableView> {
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<ChatMessageCell> {
SparseArray<Animator> 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<ArticleSelectableView> {
int startViewPosition = -1;
int startViewChildPosition = -1;
int startViewOffset;
int endViewPosition = -1;
int endViewChildPosition = -1;
int endViewOffset;
int maybeTextIndex = -1;
SparseArray<CharSequence> textByPosition = new SparseArray<>();
SparseArray<CharSequence> prefixTextByPosition = new SparseArray<>();
SparseIntArray childCountByPosition = new SparseIntArray();
public LinearLayoutManager layoutManager;
public ArticleTextSelectionHelper() {
multiselect = true;
showActionsAsPopupAlways = true;
}
public ArrayList<TextLayoutBlock> 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<TextLayoutBlock> 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;
}
}
}
}