NekoX/TMessagesProj/src/main/java/org/telegram/ui/Components/EditTextEffects.java

274 lines
8.9 KiB
Java

package org.telegram.ui.Components;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Path;
import android.graphics.Rect;
import android.graphics.Region;
import android.text.Editable;
import android.text.Layout;
import android.text.Spannable;
import android.view.MotionEvent;
import android.widget.EditText;
import org.telegram.ui.Components.spoilers.SpoilerEffect;
import org.telegram.ui.Components.spoilers.SpoilersClickDetector;
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;
public class EditTextEffects extends EditText {
private final static int SPOILER_TIMEOUT = 10000;
private List<SpoilerEffect> spoilers = new ArrayList<>();
private Stack<SpoilerEffect> spoilersPool = new Stack<>();
private boolean isSpoilersRevealed;
private boolean shouldRevealSpoilersByTouch = true;
private SpoilersClickDetector clickDetector;
private boolean suppressOnTextChanged;
private Path path = new Path();
private int selStart, selEnd;
private float lastRippleX, lastRippleY;
private boolean postedSpoilerTimeout;
private Runnable spoilerTimeout = () -> {
postedSpoilerTimeout = false;
isSpoilersRevealed = false;
invalidateSpoilers();
if (spoilers.isEmpty())
return;
spoilers.get(0).setOnRippleEndCallback(() -> post(() -> setSpoilersRevealed(false, true)));
float rad = (float) Math.sqrt(Math.pow(getWidth(), 2) + Math.pow(getHeight(), 2));
for (SpoilerEffect eff : spoilers) {
eff.startRipple(lastRippleX, lastRippleY, rad, true);
}
};
private Rect rect = new Rect();
public EditTextEffects(Context context) {
super(context);
clickDetector = new SpoilersClickDetector(this, spoilers, this::onSpoilerClicked);
}
private void onSpoilerClicked(SpoilerEffect eff, float x, float y) {
if (isSpoilersRevealed) return;
lastRippleX = x;
lastRippleY = y;
postedSpoilerTimeout = false;
removeCallbacks(spoilerTimeout);
setSpoilersRevealed(true, false);
eff.setOnRippleEndCallback(() -> post(() -> {
invalidateSpoilers();
checkSpoilerTimeout();
}));
float rad = (float) Math.sqrt(Math.pow(getWidth(), 2) + Math.pow(getHeight(), 2));
for (SpoilerEffect ef : spoilers)
ef.startRipple(x, y, rad);
}
@Override
protected void onSelectionChanged(int selStart, int selEnd) {
super.onSelectionChanged(selStart, selEnd);
if (suppressOnTextChanged)
return;
this.selStart = selStart;
this.selEnd = selEnd;
checkSpoilerTimeout();
}
/**
* Checks for spoiler timeout to be posted
*/
private void checkSpoilerTimeout() {
boolean onSpoiler = false;
CharSequence cs = getLayout() != null ? getLayout().getText() : null;
if (cs instanceof Spannable) {
Spannable e = (Spannable) cs;
TextStyleSpan[] spans = e.getSpans(0, e.length(), TextStyleSpan.class);
for (TextStyleSpan span : spans) {
int ss = e.getSpanStart(span), se = e.getSpanEnd(span);
if (span.isSpoiler()) {
if (ss > selStart && se < selEnd || selStart > ss && selStart < se || selEnd > ss && selEnd < se) {
onSpoiler = true;
removeCallbacks(spoilerTimeout);
postedSpoilerTimeout = false;
break;
}
}
}
}
if (isSpoilersRevealed && !onSpoiler && !postedSpoilerTimeout) {
postedSpoilerTimeout = true;
postDelayed(spoilerTimeout, SPOILER_TIMEOUT);
}
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
removeCallbacks(spoilerTimeout);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
invalidateEffects();
}
@Override
protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
super.onTextChanged(text, start, lengthBefore, lengthAfter);
if (!suppressOnTextChanged) {
invalidateEffects();
Layout layout = getLayout();
if (text instanceof Spannable && layout != null) {
int line = layout.getLineForOffset(start);
int x = (int) layout.getPrimaryHorizontal(start);
int y = (int) ((layout.getLineTop(line) + layout.getLineBottom(line)) / 2f);
for (SpoilerEffect eff : spoilers) {
if (eff.getBounds().contains(x, y)) {
int selOffset = lengthAfter - lengthBefore;
selStart += selOffset;
selEnd += selOffset;
onSpoilerClicked(eff, x, y);
break;
}
}
}
}
}
@Override
public void setText(CharSequence text, BufferType type) {
if (!suppressOnTextChanged) {
isSpoilersRevealed = false;
if (spoilersPool != null) // Constructor check
spoilersPool.clear();
}
super.setText(text, type);
}
/**
* Sets if spoilers should be revealed by touch or not
*/
public void setShouldRevealSpoilersByTouch(boolean shouldRevealSpoilersByTouch) {
this.shouldRevealSpoilersByTouch = shouldRevealSpoilersByTouch;
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
boolean detector = false;
if (shouldRevealSpoilersByTouch && clickDetector.onTouchEvent(event)) {
int act = event.getActionMasked();
if (act == MotionEvent.ACTION_UP) {
MotionEvent c = MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0, 0, 0);
super.dispatchTouchEvent(c);
c.recycle();
}
detector = true;
}
return super.dispatchTouchEvent(event) || detector;
}
/**
* Sets if spoiler are already revealed or not
*/
public void setSpoilersRevealed(boolean spoilersRevealed, boolean notifyEffects) {
isSpoilersRevealed = spoilersRevealed;
Spannable text = getText();
if (text != null) {
TextStyleSpan[] spans = text.getSpans(0, text.length(), TextStyleSpan.class);
for (TextStyleSpan span : spans) {
if (span.isSpoiler()) {
span.setSpoilerRevealed(spoilersRevealed);
}
}
}
suppressOnTextChanged = true;
setText(text, BufferType.EDITABLE);
setSelection(selStart, selEnd);
suppressOnTextChanged = false;
if (notifyEffects) {
invalidateSpoilers();
}
}
@Override
protected void onDraw(Canvas canvas) {
canvas.save();
path.rewind();
for (SpoilerEffect eff : spoilers) {
Rect bounds = eff.getBounds();
path.addRect(bounds.left, bounds.top, bounds.right, bounds.bottom, Path.Direction.CW);
}
canvas.clipPath(path, Region.Op.DIFFERENCE);
super.onDraw(canvas);
canvas.restore();
canvas.save();
canvas.clipPath(path);
path.rewind();
if (!spoilers.isEmpty())
spoilers.get(0).getRipplePath(path);
canvas.clipPath(path);
canvas.translate(0, -getPaddingTop());
super.onDraw(canvas);
canvas.restore();
rect.set(0, getScrollY(), getWidth(), getScrollY() + getHeight() - getPaddingBottom());
canvas.save();
canvas.clipRect(rect);
for (SpoilerEffect eff : spoilers) {
Rect b = eff.getBounds();
if (rect.top <= b.bottom && rect.bottom >= b.top || b.top <= rect.bottom && b.bottom >= rect.top) {
eff.setColor(getPaint().getColor());
eff.draw(canvas);
}
}
canvas.restore();
}
public void invalidateEffects() {
Editable text = getText();
if (text != null) {
for (TextStyleSpan span : text.getSpans(0, text.length(), TextStyleSpan.class)) {
if (span.isSpoiler()) {
span.setSpoilerRevealed(isSpoilersRevealed);
}
}
}
invalidateSpoilers();
}
private void invalidateSpoilers() {
if (spoilers == null) return; // A null-check for super constructor, because it calls onTextChanged
spoilersPool.addAll(spoilers);
spoilers.clear();
if (isSpoilersRevealed) {
invalidate();
return;
}
Layout layout = getLayout();
if (layout != null && layout.getText() instanceof Spannable) {
SpoilerEffect.addSpoilers(this, spoilersPool, spoilers);
}
invalidate();
}
}