/* * This is the source code of Telegram for Android v. 5.x.x. * It is licensed under GNU GPL v. 2 or later. * You should have received a copy of the license in this archive (see LICENSE). * * Copyright Nikolai Kudashov, 2013-2018. */ package org.telegram.ui.Components; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; import android.graphics.drawable.GradientDrawable; import android.graphics.Rect; import android.graphics.drawable.ShapeDrawable; import android.graphics.drawable.shapes.RectShape; import android.os.Build; import android.os.SystemClock; import androidx.annotation.Keep; import androidx.annotation.Nullable; import androidx.core.graphics.ColorUtils; import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; import android.text.Layout; import android.text.StaticLayout; import android.text.TextPaint; import android.text.TextUtils; import android.text.TextWatcher; import android.view.ActionMode; import android.view.Gravity; import android.view.Menu; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import android.view.ViewTreeObserver; import android.view.accessibility.AccessibilityNodeInfo; import android.widget.EditText; import android.widget.TextView; import org.telegram.messenger.AndroidUtilities; import org.telegram.messenger.BuildVars; import org.telegram.messenger.FileLog; import org.telegram.messenger.LocaleController; import org.telegram.messenger.R; import org.telegram.messenger.XiaomiUtilities; import org.telegram.ui.ActionBar.FloatingActionMode; import org.telegram.ui.ActionBar.FloatingToolbar; import org.telegram.ui.ActionBar.Theme; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; public class EditTextBoldCursor extends EditTextEffects { private static Field mEditor; private static Field mShowCursorField; private static Field mScrollYField; private static boolean mScrollYGet; private static Method getVerticalOffsetMethod; private static Class editorClass; private static Field mCursorDrawableResField; private static Method mEditorInvalidateDisplayList; private Drawable mCursorDrawable; private Object editor; private GradientDrawable gradientDrawable; private SubstringLayoutAnimator hintAnimator; private Runnable invalidateRunnable = new Runnable() { @Override public void run() { invalidate(); if (attachedToWindow != null) { AndroidUtilities.runOnUIThread(this, 500); } } }; private Paint linePaint; private Paint activeLinePaint; private TextPaint errorPaint; private int cursorSize; private int ignoreTopCount; private int ignoreBottomCount; private int scrollY; private float lineSpacingExtra; private Rect rect = new Rect(); private StaticLayout hintLayout; private CharSequence hint; private StaticLayout errorLayout; private CharSequence errorText; private int hintColor; private int headerHintColor; private boolean hintVisible = true; private float hintAlpha = 1.0f; private long hintLastUpdateTime; private boolean allowDrawCursor = true; private float cursorWidth = 2.0f; private boolean supportRtlHint; private boolean cursorDrawn; private boolean lineVisible = false; private int lineColor; private int activeLineColor; private int errorLineColor; private float lineY; private boolean lineActive = false; private float lineActiveness = 0; private long lineLastUpdateTime; private float lastLineActiveness = 0; private float activeLineWidth = 0; private boolean nextSetTextAnimated; private boolean transformHintToHeader; private boolean currentDrawHintAsHeader; private AnimatorSet headerTransformAnimation; private float headerAnimationProgress; private boolean fixed; private ViewTreeObserver.OnPreDrawListener listenerFixer; private FloatingToolbar floatingToolbar; public FloatingActionMode floatingActionMode; private ViewTreeObserver.OnPreDrawListener floatingToolbarPreDrawListener; private View windowView; private View attachedToWindow; private int lastSize; int lastOffset = -1; CharSequence lastText; boolean drawInMaim; ShapeDrawable cursorDrawable; private List registeredTextWatchers = new ArrayList<>(); private boolean isTextWatchersSuppressed = false; @TargetApi(23) private class ActionModeCallback2Wrapper extends ActionMode.Callback2 { private final ActionMode.Callback mWrapped; public ActionModeCallback2Wrapper(ActionMode.Callback wrapped) { mWrapped = wrapped; } public boolean onCreateActionMode(ActionMode mode, Menu menu) { return mWrapped.onCreateActionMode(mode, menu); } public boolean onPrepareActionMode(ActionMode mode, Menu menu) { return mWrapped.onPrepareActionMode(mode, menu); } public boolean onActionItemClicked(ActionMode mode, MenuItem item) { return mWrapped.onActionItemClicked(mode, item); } public void onDestroyActionMode(ActionMode mode) { mWrapped.onDestroyActionMode(mode); cleanupFloatingActionModeViews(); floatingActionMode = null; } @Override public void onGetContentRect(ActionMode mode, View view, Rect outRect) { if (mWrapped instanceof ActionMode.Callback2) { ((ActionMode.Callback2) mWrapped).onGetContentRect(mode, view, outRect); } else { super.onGetContentRect(mode, view, outRect); } } } public EditTextBoldCursor(Context context) { super(context); if (Build.VERSION.SDK_INT >= 26) { setImportantForAutofill(View.IMPORTANT_FOR_AUTOFILL_NO); } init(); } @Override public void addTextChangedListener(TextWatcher watcher) { registeredTextWatchers.add(watcher); if (isTextWatchersSuppressed) { return; } super.addTextChangedListener(watcher); } @Override public void removeTextChangedListener(TextWatcher watcher) { registeredTextWatchers.remove(watcher); if (isTextWatchersSuppressed) { return; } super.removeTextChangedListener(watcher); } /** * Dispatches text changed event to all text watchers */ public void dispatchTextWatchersTextChanged() { for (TextWatcher w : registeredTextWatchers) { w.beforeTextChanged("", 0, length(), length()); w.onTextChanged(getText(), 0, length(), length()); w.afterTextChanged(getText()); } } /** * Sets text watchers suppress state * * @param textWatchersSuppressed Suppress flag * @param dispatchChanged If we should notify watchers about text changed. Works only if textWatchersSuppressed = false */ public void setTextWatchersSuppressed(boolean textWatchersSuppressed, boolean dispatchChanged) { if (isTextWatchersSuppressed == textWatchersSuppressed) return; isTextWatchersSuppressed = textWatchersSuppressed; if (textWatchersSuppressed) { for (TextWatcher w : registeredTextWatchers) { super.removeTextChangedListener(w); } } else { for (TextWatcher w : registeredTextWatchers) { super.addTextChangedListener(w); if (dispatchChanged) { w.beforeTextChanged("", 0, length(), length()); w.onTextChanged(getText(), 0, length(), length()); w.afterTextChanged(getText()); } } } } /** * @return If text watchers are suppressed (Not listening to events) */ public boolean isTextWatchersSuppressed() { return isTextWatchersSuppressed; } @Nullable @Override public Drawable getTextCursorDrawable() { if (cursorDrawable != null) { return super.getTextCursorDrawable(); } ShapeDrawable shapeDrawable = new ShapeDrawable(new RectShape()) { @Override public void draw(Canvas canvas) { super.draw(canvas); cursorDrawn = true; } }; shapeDrawable.getPaint().setColor(0); return shapeDrawable; } @TargetApi(Build.VERSION_CODES.O) @Override public int getAutofillType() { return AUTOFILL_TYPE_NONE; } @SuppressLint("PrivateApi") private void init() { linePaint = new Paint(); activeLinePaint = new Paint(); errorPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); errorPaint.setTextSize(AndroidUtilities.dp(11)); if (Build.VERSION.SDK_INT >= 26) { setImportantForAutofill(View.IMPORTANT_FOR_AUTOFILL_NO); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { cursorDrawable = new ShapeDrawable() { @Override public void draw(Canvas canvas) { if (drawInMaim) { cursorDrawn = true; } else { super.draw(canvas); } } @Override public int getIntrinsicHeight() { return AndroidUtilities.dp(cursorSize + 20); } @Override public int getIntrinsicWidth() { return AndroidUtilities.dp(cursorWidth); } }; cursorDrawable.setShape(new RectShape()); gradientDrawable = new GradientDrawable(GradientDrawable.Orientation.TOP_BOTTOM, new int[]{0xff54a1db, 0xff54a1db}); setTextCursorDrawable(cursorDrawable); } try { if (!mScrollYGet && mScrollYField == null) { mScrollYGet = true; mScrollYField = View.class.getDeclaredField("mScrollY"); mScrollYField.setAccessible(true); } } catch (Throwable ignore) { } try { if (editorClass == null) { mEditor = TextView.class.getDeclaredField("mEditor"); mEditor.setAccessible(true); editorClass = Class.forName("android.widget.Editor"); try { mShowCursorField = editorClass.getDeclaredField("mShowCursor"); mShowCursorField.setAccessible(true); } catch (Exception ignore) {} try { mEditorInvalidateDisplayList = editorClass.getDeclaredMethod("invalidateTextDisplayList"); mEditorInvalidateDisplayList.setAccessible(true); } catch (Exception ignore) {} getVerticalOffsetMethod = TextView.class.getDeclaredMethod("getVerticalOffset", boolean.class); getVerticalOffsetMethod.setAccessible(true); } } catch (Throwable e) { FileLog.e(e); } if (cursorDrawable == null) { try { gradientDrawable = new GradientDrawable(GradientDrawable.Orientation.TOP_BOTTOM, new int[]{0xff54a1db, 0xff54a1db}); if (Build.VERSION.SDK_INT >= 29) { setTextCursorDrawable(gradientDrawable); } editor = mEditor.get(this); } catch (Throwable ignore) { } try { if (mCursorDrawableResField == null) { mCursorDrawableResField = TextView.class.getDeclaredField("mCursorDrawableRes"); mCursorDrawableResField.setAccessible(true); } if (mCursorDrawableResField != null) { mCursorDrawableResField.set(this, R.drawable.field_carret_empty); } } catch (Throwable ignore) { } } cursorSize = AndroidUtilities.dp(24); } @SuppressLint("PrivateApi") public void fixHandleView(boolean reset) { if (reset) { fixed = false; } else if (!fixed) { try { if (editorClass == null) { editorClass = Class.forName("android.widget.Editor"); mEditor = TextView.class.getDeclaredField("mEditor"); mEditor.setAccessible(true); editor = mEditor.get(this); } if (listenerFixer == null) { Method initDrawablesMethod = editorClass.getDeclaredMethod("getPositionListener"); initDrawablesMethod.setAccessible(true); listenerFixer = (ViewTreeObserver.OnPreDrawListener) initDrawablesMethod.invoke(editor); } AndroidUtilities.runOnUIThread(listenerFixer::onPreDraw, 500); } catch (Throwable ignore) { } fixed = true; } } public void setTransformHintToHeader(boolean value) { if (transformHintToHeader == value) { return; } transformHintToHeader = value; if (headerTransformAnimation != null) { headerTransformAnimation.cancel(); headerTransformAnimation = null; } } public void setAllowDrawCursor(boolean value) { allowDrawCursor = value; invalidate(); } public void setCursorWidth(float width) { cursorWidth = width; } public void setCursorColor(int color) { if (cursorDrawable != null) { cursorDrawable.getPaint().setColor(color); } if (gradientDrawable != null) { gradientDrawable.setColor(color); } invalidate(); } public void setCursorSize(int value) { cursorSize = value; } public void setErrorLineColor(int error) { errorLineColor = error; errorPaint.setColor(errorLineColor); invalidate(); } private Rect padding = new Rect(); public void setLineColors(int color, int active, int error) { lineVisible = true; getContext().getResources().getDrawable(R.drawable.search_dark).getPadding(padding); setPadding(padding.left, padding.top, padding.right, padding.bottom); lineColor = color; activeLineColor = active; activeLinePaint.setColor(activeLineColor); errorLineColor = error; errorPaint.setColor(errorLineColor); invalidate(); } public void setHintVisible(boolean value) { if (hintVisible == value) { return; } hintLastUpdateTime = System.currentTimeMillis(); hintVisible = value; invalidate(); } public void setHintColor(int value) { hintColor = value; invalidate(); } public void setHeaderHintColor(int value) { headerHintColor = value; invalidate(); } public void setNextSetTextAnimated(boolean value) { nextSetTextAnimated = value; } public void setErrorText(CharSequence text) { if (TextUtils.equals(text, errorText)) { return; } errorText = text; requestLayout(); } public boolean hasErrorText() { return !TextUtils.isEmpty(errorText); } public StaticLayout getErrorLayout(int width) { if (TextUtils.isEmpty(errorText)) { return null; } else { return new StaticLayout(errorText, errorPaint, width, Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false); } } public float getLineY() { return lineY; } public void setSupportRtlHint(boolean value) { supportRtlHint = value; } @Override protected void onScrollChanged(int horiz, int vert, int oldHoriz, int oldVert) { super.onScrollChanged(horiz, vert, oldHoriz, oldVert); if (horiz != oldHoriz) { getParent().requestDisallowInterceptTouchEvent(true); } } @Override public void setText(CharSequence text, BufferType type) { super.setText(text, type); checkHeaderVisibility(nextSetTextAnimated); nextSetTextAnimated = false; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int currentSize = getMeasuredHeight() + (getMeasuredWidth() << 16); if (hintLayout != null) { if (lastSize != currentSize) { setHintText(hint); } lineY = (getMeasuredHeight() - hintLayout.getHeight()) / 2.0f + hintLayout.getHeight() + AndroidUtilities.dp(6); } else { lineY = getMeasuredHeight() - AndroidUtilities.dp(2); } lastSize = currentSize; } public void setHintText(CharSequence text) { setHintText(text, false); } public void setHintText(CharSequence text, boolean animated) { if (text == null) { text = ""; } if (getMeasuredWidth() == 0) { animated = false; } if (animated) { if (hintAnimator == null) { hintAnimator = new SubstringLayoutAnimator(this); } hintAnimator.create(hintLayout, hint, text, getPaint()); } else { if (hintAnimator != null) { hintAnimator.cancel(); } } hint = text; if (getMeasuredWidth() != 0) { text = TextUtils.ellipsize(text, getPaint(), getMeasuredWidth(), TextUtils.TruncateAt.END); if (hintLayout != null && TextUtils.equals(hintLayout.getText(), text)) { return; } } hintLayout = new StaticLayout(text, getPaint(), AndroidUtilities.dp(1000), Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false); } public Layout getHintLayoutEx() { return hintLayout; } @Override protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { try { super.onFocusChanged(focused, direction, previouslyFocusedRect); } catch (Exception e) { FileLog.e(e); } checkHeaderVisibility(true); } private void checkHeaderVisibility(boolean animated) { boolean newHintHeader = transformHintToHeader && (isFocused() || getText().length() > 0); if (currentDrawHintAsHeader != newHintHeader) { if (headerTransformAnimation != null) { headerTransformAnimation.cancel(); headerTransformAnimation = null; } currentDrawHintAsHeader = newHintHeader; if (animated) { headerTransformAnimation = new AnimatorSet(); headerTransformAnimation.playTogether(ObjectAnimator.ofFloat(this, "headerAnimationProgress", newHintHeader ? 1.0f : 0.0f)); headerTransformAnimation.setDuration(200); headerTransformAnimation.setInterpolator(CubicBezierInterpolator.EASE_OUT_QUINT); headerTransformAnimation.start(); } else { headerAnimationProgress = newHintHeader ? 1.0f : 0.0f; } invalidate(); } } @Keep public void setHeaderAnimationProgress(float value) { headerAnimationProgress = value; invalidate(); } @Keep public float getHeaderAnimationProgress() { return headerAnimationProgress; } @Override public void setLineSpacing(float add, float mult) { super.setLineSpacing(add, mult); lineSpacingExtra = add; } @Override public int getExtendedPaddingTop() { if (ignoreTopCount != 0) { ignoreTopCount--; return 0; } return super.getExtendedPaddingTop(); } @Override public int getExtendedPaddingBottom() { if (ignoreBottomCount != 0) { ignoreBottomCount--; return scrollY != Integer.MAX_VALUE ? -scrollY : 0; } return super.getExtendedPaddingBottom(); } private int lastTouchX = -1; @Override public boolean onTouchEvent(MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_DOWN) { lastTouchX = (int) event.getX(); } return super.onTouchEvent(event); } public void invalidateForce() { invalidate(); if (!isHardwareAccelerated()) { return; } try { // on hardware accelerated edittext to invalidate imagespan display list must be invalidated if (mEditorInvalidateDisplayList != null) { if (editor == null) { editor = mEditor.get(this); } if (editor != null) { mEditorInvalidateDisplayList.invoke(editor); } } } catch (Exception ignore) {}; } @Override protected void onDraw(Canvas canvas) { if ((length() == 0 || transformHintToHeader) && hintLayout != null && (hintVisible || hintAlpha != 0)) { if (hintVisible && hintAlpha != 1.0f || !hintVisible && hintAlpha != 0.0f) { long newTime = System.currentTimeMillis(); long dt = newTime - hintLastUpdateTime; if (dt < 0 || dt > 17) { dt = 17; } hintLastUpdateTime = newTime; if (hintVisible) { hintAlpha += dt / 150.0f; if (hintAlpha > 1.0f) { hintAlpha = 1.0f; } } else { hintAlpha -= dt / 150.0f; if (hintAlpha < 0.0f) { hintAlpha = 0.0f; } } invalidate(); } int oldColor = getPaint().getColor(); canvas.save(); int left = 0; float lineLeft = hintLayout.getLineLeft(0); float hintWidth = hintLayout.getLineWidth(0); if (lineLeft != 0) { left -= lineLeft; } if (supportRtlHint && LocaleController.isRTL) { float offset = getMeasuredWidth() - hintWidth; canvas.translate(left + getScrollX() + offset, lineY - hintLayout.getHeight() - AndroidUtilities.dp(7)); } else { canvas.translate(left + getScrollX(), lineY - hintLayout.getHeight() - AndroidUtilities.dp2(7)); } if (transformHintToHeader) { float scale = 1.0f - 0.3f * headerAnimationProgress; if (supportRtlHint && LocaleController.isRTL) { canvas.translate((hintWidth + lineLeft) - (hintWidth + lineLeft) * scale, 0); } else if (lineLeft != 0) { canvas.translate(lineLeft * (1.0f - scale), 0); } canvas.scale(scale, scale); canvas.translate(0, -AndroidUtilities.dp(22) * headerAnimationProgress); getPaint().setColor(ColorUtils.blendARGB(hintColor, headerHintColor, headerAnimationProgress)); } else { getPaint().setColor(hintColor); getPaint().setAlpha((int) (255 * hintAlpha * (Color.alpha(hintColor) / 255.0f))); } if (hintAnimator != null && hintAnimator.animateTextChange) { canvas.save(); canvas.clipRect(0, 0, getMeasuredWidth(), getMeasuredHeight()); hintAnimator.draw(canvas, getPaint()); canvas.restore(); } else { hintLayout.draw(canvas); } getPaint().setColor(oldColor); canvas.restore(); } int topPadding = getExtendedPaddingTop(); scrollY = Integer.MAX_VALUE; try { if (mScrollYField != null) { scrollY = mScrollYField.getInt(this); mScrollYField.set(this, 0); } else { scrollY = getScrollX(); } } catch (Exception e) { if (BuildVars.DEBUG_PRIVATE_VERSION) { throw new RuntimeException(e); } } ignoreTopCount = 1; ignoreBottomCount = 1; canvas.save(); canvas.translate(0, topPadding); try { drawInMaim = true; super.onDraw(canvas); drawInMaim = false; } catch (Exception e) { if (BuildVars.DEBUG_PRIVATE_VERSION) { throw new RuntimeException(e); } } if (mScrollYField != null && scrollY != Integer.MAX_VALUE) { try { mScrollYField.set(this, scrollY); } catch (Exception e) { if (BuildVars.DEBUG_PRIVATE_VERSION) { throw new RuntimeException(e); } } } canvas.restore(); if (cursorDrawable == null) { try { boolean showCursor; if (mShowCursorField != null && editor != null) { long mShowCursor = mShowCursorField.getLong(editor); showCursor = (SystemClock.uptimeMillis() - mShowCursor) % (2 * 500) < 500 && isFocused(); } else { showCursor = cursorDrawn; cursorDrawn = false; } if (allowDrawCursor && showCursor) { canvas.save(); int voffsetCursor = 0; if (getVerticalOffsetMethod != null) { if ((getGravity() & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) { voffsetCursor = (int) getVerticalOffsetMethod.invoke(this, true); } } else { if ((getGravity() & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) { voffsetCursor = getTotalPaddingTop() - getExtendedPaddingTop(); } } canvas.translate(getPaddingLeft(), getExtendedPaddingTop() + voffsetCursor); Layout layout = getLayout(); int line = layout.getLineForOffset(getSelectionStart()); int lineCount = layout.getLineCount(); updateCursorPosition(); Rect bounds = gradientDrawable.getBounds(); rect.left = bounds.left; rect.right = bounds.left + AndroidUtilities.dp(cursorWidth); rect.bottom = bounds.bottom; rect.top = bounds.top; if (lineSpacingExtra != 0 && line < lineCount - 1) { rect.bottom -= lineSpacingExtra; } rect.top = rect.centerY() - cursorSize / 2; rect.bottom = rect.top + cursorSize; gradientDrawable.setBounds(rect); gradientDrawable.draw(canvas); canvas.restore(); } } catch (Throwable exception) { if (BuildVars.DEBUG_PRIVATE_VERSION) { throw new RuntimeException(exception); } } } else { if (cursorDrawn) { try { canvas.save(); int voffsetCursor = 0; if (getVerticalOffsetMethod != null) { if ((getGravity() & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) { voffsetCursor = (int) getVerticalOffsetMethod.invoke(this, true); } } else { if ((getGravity() & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) { voffsetCursor = getTotalPaddingTop() - getExtendedPaddingTop(); } } canvas.translate(getPaddingLeft(), getExtendedPaddingTop() + voffsetCursor); Layout layout = getLayout(); int line = layout.getLineForOffset(getSelectionStart()); int lineCount = layout.getLineCount(); updateCursorPosition(); Rect bounds = gradientDrawable.getBounds(); rect.left = bounds.left; rect.right = bounds.left + AndroidUtilities.dp(cursorWidth); rect.bottom = bounds.bottom; rect.top = bounds.top; if (lineSpacingExtra != 0 && line < lineCount - 1) { rect.bottom -= lineSpacingExtra; } rect.top = rect.centerY() - cursorSize / 2; rect.bottom = rect.top + cursorSize; gradientDrawable.setBounds(rect); gradientDrawable.draw(canvas); canvas.restore(); cursorDrawn = false; } catch (Throwable exception) { if (BuildVars.DEBUG_PRIVATE_VERSION) { throw new RuntimeException(exception); } } } } if (lineVisible && lineColor != 0) { int lineWidth = AndroidUtilities.dp(1); boolean wasLineActive = lineActive; if (!TextUtils.isEmpty(errorText)) { linePaint.setColor(errorLineColor); lineWidth = AndroidUtilities.dp(2); lineActive = false; } else if (isFocused()) { lineActive = true; } else { linePaint.setColor(lineColor); lineActive = false; } if (lineActive != wasLineActive) { lineLastUpdateTime = SystemClock.elapsedRealtime(); lastLineActiveness = lineActiveness; } float t = (SystemClock.elapsedRealtime() - lineLastUpdateTime) / 150.0f; if (t < 1f || lineActive && lineActiveness != 1.0f || !lineActive && lineActiveness != 0.0f) { lineActiveness = AndroidUtilities.lerp(lastLineActiveness, lineActive ? 1 : 0, Math.max(0, Math.min(1, t))); if (t < 1f) { invalidate(); } } int scrollHeight = (getLayout() == null ? 0 : getLayout().getHeight()) - getMeasuredHeight() + getPaddingBottom() + getPaddingTop(); int bottom = (int) lineY + getScrollY() + Math.min(Math.max(0, scrollHeight - getScrollY()), AndroidUtilities.dp(2)); int centerX = lastTouchX < 0 ? getMeasuredWidth() / 2 : lastTouchX, maxWidth = Math.max(centerX, getMeasuredWidth() - centerX) * 2; if (lineActiveness < 1f) { canvas.drawRect(getScrollX(), bottom - lineWidth, getScrollX() + getMeasuredWidth(), bottom, linePaint); } if (lineActiveness > 0f) { float lineActivenessT = CubicBezierInterpolator.EASE_BOTH.getInterpolation(lineActiveness); if (lineActive) { activeLineWidth = maxWidth * lineActivenessT; } int lineThickness = (int) ((lineActive ? 1 : lineActivenessT) * AndroidUtilities.dp(2)); canvas.drawRect( getScrollX() + Math.max(0, centerX - activeLineWidth / 2), bottom - lineThickness, getScrollX() + Math.min(centerX + activeLineWidth / 2, getMeasuredWidth()), bottom, activeLinePaint ); } } /*if (errorLayout != null) { canvas.save(); canvas.translate(getScrollX(), lineY + AndroidUtilities.dp(3)); errorLayout.draw(canvas); canvas.restore(); }*/ } public void setWindowView(View view) { windowView = view; } private boolean updateCursorPosition() { final Layout layout = getLayout(); final int offset = getSelectionStart(); final int line = layout.getLineForOffset(offset); final int top = layout.getLineTop(line); final int bottom = layout.getLineTop(line + 1); updateCursorPosition(top, bottom, layout.getPrimaryHorizontal(offset)); lastText = layout.getText(); lastOffset = offset; return true; } private Rect mTempRect; private int clampHorizontalPosition(final Drawable drawable, float horizontal) { horizontal = Math.max(0.5f, horizontal - 0.5f); if (mTempRect == null) { mTempRect = new Rect(); } int drawableWidth = 0; if (drawable != null) { drawable.getPadding(mTempRect); drawableWidth = drawable.getIntrinsicWidth(); } else { mTempRect.setEmpty(); } int scrollX = getScrollX(); float horizontalDiff = horizontal - scrollX; int viewClippedWidth = getWidth() - getCompoundPaddingLeft() - getCompoundPaddingRight(); final int left; if (horizontalDiff >= (viewClippedWidth - 1f)) { left = viewClippedWidth + scrollX - (drawableWidth - mTempRect.right); } else if (Math.abs(horizontalDiff) <= 1f || (TextUtils.isEmpty(getText()) && (1024 * 1024 - scrollX) <= (viewClippedWidth + 1f) && horizontal <= 1f)) { left = scrollX - mTempRect.left; } else { left = (int) horizontal - mTempRect.left; } return left; } private void updateCursorPosition(int top, int bottom, float horizontal) { final int left = clampHorizontalPosition(gradientDrawable, horizontal); final int width = AndroidUtilities.dp(cursorWidth); gradientDrawable.setBounds(left, top - mTempRect.top, left + width, bottom + mTempRect.bottom); } @Override public float getLineSpacingExtra() { return super.getLineSpacingExtra(); } private void cleanupFloatingActionModeViews() { if (floatingToolbar != null) { floatingToolbar.dismiss(); floatingToolbar = null; } if (floatingToolbarPreDrawListener != null) { getViewTreeObserver().removeOnPreDrawListener(floatingToolbarPreDrawListener); floatingToolbarPreDrawListener = null; } } @Override protected void onAttachedToWindow() { try { super.onAttachedToWindow(); } catch (Exception e) { FileLog.e(e); } attachedToWindow = getRootView(); AndroidUtilities.runOnUIThread(invalidateRunnable); } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); attachedToWindow = null; AndroidUtilities.cancelRunOnUIThread(invalidateRunnable); } @Override public ActionMode startActionMode(ActionMode.Callback callback) { if (Build.VERSION.SDK_INT >= 23 && (windowView != null || attachedToWindow != null)) { if (floatingActionMode != null) { floatingActionMode.finish(); } cleanupFloatingActionModeViews(); floatingToolbar = new FloatingToolbar(getContext(), windowView != null ? windowView : attachedToWindow, getActionModeStyle(), getResourcesProvider()); floatingActionMode = new FloatingActionMode(getContext(), new ActionModeCallback2Wrapper(callback), this, floatingToolbar); floatingToolbarPreDrawListener = () -> { if (floatingActionMode != null) { floatingActionMode.updateViewLocationInWindow(); } return true; }; callback.onCreateActionMode(floatingActionMode, floatingActionMode.getMenu()); extendActionMode(floatingActionMode, floatingActionMode.getMenu()); floatingActionMode.invalidate(); getViewTreeObserver().addOnPreDrawListener(floatingToolbarPreDrawListener); invalidate(); return floatingActionMode; } else { return super.startActionMode(callback); } } @Override public ActionMode startActionMode(ActionMode.Callback callback, int type) { if (Build.VERSION.SDK_INT >= 23 && (windowView != null || attachedToWindow != null)) { return startActionMode(callback); } else { return super.startActionMode(callback, type); } } protected void extendActionMode(ActionMode actionMode, Menu menu) { } protected int getActionModeStyle() { return FloatingToolbar.STYLE_THEME; } public void hideActionMode() { cleanupFloatingActionModeViews(); } @Override public void setSelection(int start, int stop) { try { super.setSelection(start, stop); } catch (Exception e) { FileLog.e(e); } } @Override public void setSelection(int index) { try { super.setSelection(index); } catch (Exception e) { FileLog.e(e); } } @Override public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(info); info.setClassName("android.widget.EditText"); if (hintLayout != null) { if (getText().length() <= 0) { info.setText(hintLayout.getText()); } else { AccessibilityNodeInfoCompat.wrap(info).setHintText(hintLayout.getText()); } } } protected Theme.ResourcesProvider getResourcesProvider() { return null; } public void setHandlesColor(int color) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q || XiaomiUtilities.isMIUI()) { return; } try { Drawable left = getTextSelectHandleLeft(); left.setColorFilter(color, PorterDuff.Mode.SRC_IN); setTextSelectHandleLeft(left); Drawable middle = getTextSelectHandle(); middle.setColorFilter(color, PorterDuff.Mode.SRC_IN); setTextSelectHandle(middle); Drawable right = getTextSelectHandleRight(); right.setColorFilter(color, PorterDuff.Mode.SRC_IN); setTextSelectHandleRight(right); } catch (Exception ignore) {} } }