NekoX/TMessagesProj/src/main/java/org/telegram/ui/Components/spoilers/SpoilerEffect.java

827 lines
32 KiB
Java

package org.telegram.ui.Components.spoilers;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.TimeInterpolator;
import android.animation.ValueAnimator;
import android.annotation.SuppressLint;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PixelFormat;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Region;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.text.Layout;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.StaticLayout;
import android.text.TextPaint;
import android.text.style.ForegroundColorSpan;
import android.text.style.ReplacementSpan;
import android.text.style.URLSpan;
import android.view.View;
import android.widget.TextView;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.graphics.ColorUtils;
import androidx.core.math.MathUtils;
import org.telegram.messenger.AndroidUtilities;
import org.telegram.messenger.Emoji;
import org.telegram.messenger.LocaleController;
import org.telegram.messenger.SharedConfig;
import org.telegram.messenger.Utilities;
import org.telegram.ui.ActionBar.Theme;
import org.telegram.ui.Components.Easings;
import org.telegram.ui.Components.TextStyleSpan;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Stack;
import java.util.concurrent.atomic.AtomicReference;
public class SpoilerEffect extends Drawable {
public final static int MAX_PARTICLES_PER_ENTITY = measureMaxParticlesCount();
public final static int PARTICLES_PER_CHARACTER = measureParticlesPerCharacter();
private final static float VERTICAL_PADDING_DP = 2.5f;
private final static int RAND_REPEAT = 14;
private final static float KEYPOINT_DELTA = 5f;
private final static int FPS = 30;
private final static int renderDelayMs = 1000 / FPS + 1;
public final static float[] ALPHAS = {
0.3f, 0.6f, 1.0f
};
private Paint[] particlePaints = new Paint[ALPHAS.length];
private Stack<Particle> particlesPool = new Stack<>();
private int maxParticles;
float[][] particlePoints = new float[ALPHAS.length][MAX_PARTICLES_PER_ENTITY * 2];
private float[] particleRands = new float[RAND_REPEAT];
private int[] renderCount = new int[ALPHAS.length];
private static Path tempPath = new Path();
private RectF visibleRect;
private ArrayList<Particle> particles = new ArrayList<>();
private View mParent;
private long lastDrawTime;
private float rippleX, rippleY;
private float rippleMaxRadius;
private float rippleProgress = -1;
private boolean reverseAnimator;
private boolean shouldInvalidateColor;
private Runnable onRippleEndCallback;
private ValueAnimator rippleAnimator;
private List<RectF> spaces = new ArrayList<>();
private List<Long> keyPoints;
private int mAlpha = 0xFF;
private TimeInterpolator rippleInterpolator = input -> input;
private boolean invalidateParent;
private boolean suppressUpdates;
private boolean isLowDevice;
private boolean enableAlpha;
private int lastColor;
public boolean drawPoints;
private static Paint xRefPaint;
private static int measureParticlesPerCharacter() {
switch (SharedConfig.getDevicePerformanceClass()) {
default:
case SharedConfig.PERFORMANCE_CLASS_LOW:
case SharedConfig.PERFORMANCE_CLASS_AVERAGE:
return 10;
case SharedConfig.PERFORMANCE_CLASS_HIGH:
return 30;
}
}
private static int measureMaxParticlesCount() {
switch (SharedConfig.getDevicePerformanceClass()) {
default:
case SharedConfig.PERFORMANCE_CLASS_LOW:
case SharedConfig.PERFORMANCE_CLASS_AVERAGE:
return 100;
case SharedConfig.PERFORMANCE_CLASS_HIGH:
return 150;
}
}
public SpoilerEffect() {
for (int i = 0; i < ALPHAS.length; i++) {
particlePaints[i] = new Paint();
if (i == 0) {
particlePaints[i].setStrokeWidth(AndroidUtilities.dp(1.4f));
particlePaints[i].setStyle(Paint.Style.STROKE);
particlePaints[i].setStrokeCap(Paint.Cap.ROUND);
} else {
particlePaints[i].setStrokeWidth(AndroidUtilities.dp(1.2f));
particlePaints[i].setStyle(Paint.Style.STROKE);
particlePaints[i].setStrokeCap(Paint.Cap.ROUND);
}
}
isLowDevice = SharedConfig.getDevicePerformanceClass() == SharedConfig.PERFORMANCE_CLASS_LOW;
enableAlpha = true;
setColor(Color.TRANSPARENT);
}
/**
* Sets if we should suppress updates or not
*/
public void setSuppressUpdates(boolean suppressUpdates) {
this.suppressUpdates = suppressUpdates;
invalidateSelf();
}
/**
* Sets if we should invalidate parent instead
*/
public void setInvalidateParent(boolean invalidateParent) {
this.invalidateParent = invalidateParent;
}
/**
* Updates max particles count
*/
public void updateMaxParticles() {
setMaxParticlesCount(MathUtils.clamp((getBounds().width() / AndroidUtilities.dp(6)) * PARTICLES_PER_CHARACTER, PARTICLES_PER_CHARACTER, MAX_PARTICLES_PER_ENTITY));
}
/**
* Sets callback to be run after ripple animation ends
*/
public void setOnRippleEndCallback(@Nullable Runnable onRippleEndCallback) {
this.onRippleEndCallback = onRippleEndCallback;
}
/**
* Starts ripple
*
* @param rX Ripple center x
* @param rY Ripple center y
* @param radMax Max ripple radius
*/
public void startRipple(float rX, float rY, float radMax) {
startRipple(rX, rY, radMax, false);
}
/**
* Starts ripple
*
* @param rX Ripple center x
* @param rY Ripple center y
* @param radMax Max ripple radius
* @param reverse If we should start reverse ripple
*/
public void startRipple(float rX, float rY, float radMax, boolean reverse) {
rippleX = rX;
rippleY = rY;
rippleMaxRadius = radMax;
rippleProgress = reverse ? 1 : 0;
reverseAnimator = reverse;
if (rippleAnimator != null)
rippleAnimator.cancel();
int startAlpha = reverseAnimator ? 0xFF : particlePaints[ALPHAS.length - 1].getAlpha();
rippleAnimator = ValueAnimator.ofFloat(rippleProgress, reverse ? 0 : 1).setDuration((long) MathUtils.clamp(rippleMaxRadius * 0.3f, 250, 550));
rippleAnimator.setInterpolator(rippleInterpolator);
rippleAnimator.addUpdateListener(animation -> {
rippleProgress = (float) animation.getAnimatedValue();
setAlpha((int) (startAlpha * (1f - rippleProgress)));
shouldInvalidateColor = true;
invalidateSelf();
});
rippleAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
Iterator<Particle> it = particles.iterator();
while (it.hasNext()) {
Particle p = it.next();
if (particlesPool.size() < maxParticles) {
particlesPool.push(p);
}
it.remove();
}
if (onRippleEndCallback != null) {
onRippleEndCallback.run();
onRippleEndCallback = null;
}
rippleAnimator = null;
invalidateSelf();
}
});
rippleAnimator.start();
invalidateSelf();
}
/**
* Sets new ripple interpolator
*
* @param rippleInterpolator New interpolator
*/
public void setRippleInterpolator(@NonNull TimeInterpolator rippleInterpolator) {
this.rippleInterpolator = rippleInterpolator;
}
/**
* Sets new keypoints
*
* @param keyPoints New keypoints
*/
public void setKeyPoints(List<Long> keyPoints) {
this.keyPoints = keyPoints;
invalidateSelf();
}
/**
* Gets ripple path
*/
public void getRipplePath(Path path) {
path.addCircle(rippleX, rippleY, rippleMaxRadius * MathUtils.clamp(rippleProgress, 0, 1), Path.Direction.CW);
}
/**
* @return Current ripple progress
*/
public float getRippleProgress() {
return rippleProgress;
}
/**
* @return If we should invalidate color
*/
public boolean shouldInvalidateColor() {
boolean b = shouldInvalidateColor;
shouldInvalidateColor = false;
return b;
}
/**
* Sets new ripple progress
*/
public void setRippleProgress(float rippleProgress) {
this.rippleProgress = rippleProgress;
if (rippleProgress == -1 && rippleAnimator != null) {
rippleAnimator.cancel();
}
shouldInvalidateColor = true;
}
@Override
public void setBounds(int left, int top, int right, int bottom) {
super.setBounds(left, top, right, bottom);
Iterator<Particle> it = particles.iterator();
while (it.hasNext()) {
Particle p = it.next();
if (!getBounds().contains((int) p.x, (int) p.y)) {
it.remove();
}
if (particlesPool.size() < maxParticles) {
particlesPool.push(p);
}
}
}
@Override
public void draw(@NonNull Canvas canvas) {
if (drawPoints) {
long curTime = System.currentTimeMillis();
int dt = (int) Math.min(curTime - lastDrawTime, renderDelayMs);
boolean hasAnimator = false;
lastDrawTime = curTime;
int left = getBounds().left, top = getBounds().top, right = getBounds().right, bottom = getBounds().bottom;
for (int i = 0; i < ALPHAS.length; i++) {
renderCount[i] = 0;
}
for (int i = 0; i < particles.size(); i++) {
Particle particle = particles.get(i);
particle.currentTime = Math.min(particle.currentTime + dt, particle.lifeTime);
if (particle.currentTime >= particle.lifeTime || isOutOfBounds(left, top, right, bottom, particle.x, particle.y)) {
if (particlesPool.size() < maxParticles) {
particlesPool.push(particle);
}
particles.remove(i);
i--;
continue;
}
float hdt = particle.velocity * dt / 500f;
particle.x += particle.vecX * hdt;
particle.y += particle.vecY * hdt;
int alphaIndex = particle.alpha;
particlePoints[alphaIndex][renderCount[alphaIndex] * 2] = particle.x;
particlePoints[alphaIndex][renderCount[alphaIndex] * 2 + 1] = particle.y;
renderCount[alphaIndex]++;
}
if (particles.size() < maxParticles) {
int np = maxParticles - particles.size();
Arrays.fill(particleRands, -1);
for (int i = 0; i < np; i++) {
float rf = particleRands[i % RAND_REPEAT];
if (rf == -1) {
particleRands[i % RAND_REPEAT] = rf = Utilities.fastRandom.nextFloat();
}
Particle newParticle = !particlesPool.isEmpty() ? particlesPool.pop() : new Particle();
int attempts = 0;
do {
generateRandomLocation(newParticle, i);
attempts++;
} while (isOutOfBounds(left, top, right, bottom, newParticle.x, newParticle.y) && attempts < 4);
double angleRad = rf * Math.PI * 2 - Math.PI;
float vx = (float) Math.cos(angleRad);
float vy = (float) Math.sin(angleRad);
newParticle.vecX = vx;
newParticle.vecY = vy;
newParticle.currentTime = 0;
newParticle.lifeTime = 1000 + Math.abs(Utilities.fastRandom.nextInt(2000)); // [1000;3000]
newParticle.velocity = 4 + rf * 6;
newParticle.alpha = Utilities.fastRandom.nextInt(ALPHAS.length);
particles.add(newParticle);
int alphaIndex = newParticle.alpha;
particlePoints[alphaIndex][renderCount[alphaIndex] * 2] = newParticle.x;
particlePoints[alphaIndex][renderCount[alphaIndex] * 2 + 1] = newParticle.y;
renderCount[alphaIndex]++;
}
}
for (int a = enableAlpha ? 0 : ALPHAS.length - 1; a < ALPHAS.length; a++) {
int renderCount = 0;
int off = 0;
for (int i = 0; i < particles.size(); i++) {
Particle p = particles.get(i);
if (visibleRect != null && !visibleRect.contains(p.x, p.y) || p.alpha != a && enableAlpha) {
off++;
continue;
}
particlePoints[a][(i - off) * 2] = p.x;
particlePoints[a][(i - off) * 2 + 1] = p.y;
renderCount += 2;
}
canvas.drawPoints(particlePoints[a], 0, renderCount, particlePaints[a]);
}
} else {
Paint shaderPaint = SpoilerEffectBitmapFactory.getInstance().getPaint();
shaderPaint.setColorFilter(new PorterDuffColorFilter(lastColor, PorterDuff.Mode.SRC_IN));
canvas.drawRect(getBounds().left, getBounds().top, getBounds().right, getBounds().bottom, SpoilerEffectBitmapFactory.getInstance().getPaint());
invalidateSelf();
SpoilerEffectBitmapFactory.getInstance().checkUpdate();
}
}
/**
* Updates visible bounds to update particles
*/
public void setVisibleBounds(float left, float top, float right, float bottom) {
if (visibleRect == null)
visibleRect = new RectF();
visibleRect.left = left;
visibleRect.top = top;
visibleRect.right = right;
visibleRect.bottom = bottom;
invalidateSelf();
}
private boolean isOutOfBounds(int left, int top, int right, int bottom, float x, float y) {
if (x < left || x > right || y < top + AndroidUtilities.dp(VERTICAL_PADDING_DP) ||
y > bottom - AndroidUtilities.dp(VERTICAL_PADDING_DP))
return true;
for (int i = 0; i < spaces.size(); i++) {
if (spaces.get(i).contains(x, y)) {
return true;
}
}
return false;
}
private void generateRandomLocation(Particle newParticle, int i) {
if (keyPoints != null && !keyPoints.isEmpty()) {
float rf = particleRands[i % RAND_REPEAT];
long kp = keyPoints.get(Utilities.fastRandom.nextInt(keyPoints.size()));
newParticle.x = getBounds().left + (kp >> 16) + rf * AndroidUtilities.dp(KEYPOINT_DELTA) - AndroidUtilities.dp(KEYPOINT_DELTA / 2f);
newParticle.y = getBounds().top + (kp & 0xFFFF) + rf * AndroidUtilities.dp(KEYPOINT_DELTA) - AndroidUtilities.dp(KEYPOINT_DELTA / 2f);
} else {
newParticle.x = getBounds().left + Utilities.fastRandom.nextFloat() * getBounds().width();
newParticle.y = getBounds().top + Utilities.fastRandom.nextFloat() * getBounds().height();
}
}
@Override
public void invalidateSelf() {
super.invalidateSelf();
if (mParent != null) {
View v = mParent;
if (v.getParent() != null && invalidateParent) {
((View) v.getParent()).invalidate();
} else {
v.invalidate();
}
}
}
/**
* Attaches to the parent view
*
* @param parentView Parent view
*/
public void setParentView(View parentView) {
this.mParent = parentView;
}
/**
* @return Currently used parent view
*/
public View getParentView() {
return mParent;
}
@Override
public void setAlpha(int alpha) {
mAlpha = alpha;
for (int i = 0; i < ALPHAS.length; i++) {
particlePaints[i].setAlpha((int) (ALPHAS[i] * alpha));
}
}
@Override
public void setColorFilter(@Nullable ColorFilter colorFilter) {
for (Paint p : particlePaints) {
p.setColorFilter(colorFilter);
}
}
/**
* Sets particles color
*
* @param color New color
*/
public void setColor(int color) {
if (lastColor != color) {
for (int i = 0; i < ALPHAS.length; i++) {
particlePaints[i].setColor(ColorUtils.setAlphaComponent(color, (int) (mAlpha * ALPHAS[i])));
}
lastColor = color;
}
}
/**
* @return If effect has color
*/
public boolean hasColor() {
return lastColor != Color.TRANSPARENT;
}
@Override
public int getOpacity() {
return PixelFormat.TRANSPARENT;
}
/**
* @param textLayout Text layout to measure
* @return Measured key points
*/
public static synchronized List<Long> measureKeyPoints(Layout textLayout) {
int w = textLayout.getWidth();
int h = textLayout.getHeight();
if (w == 0 || h == 0)
return Collections.emptyList();
Bitmap measureBitmap = Bitmap.createBitmap(Math.round(w), Math.round(h), Bitmap.Config.ARGB_4444); // We can use 4444 as we don't need accuracy here
Canvas measureCanvas = new Canvas(measureBitmap);
textLayout.draw(measureCanvas);
int[] pixels = new int[measureBitmap.getWidth() * measureBitmap.getHeight()];
measureBitmap.getPixels(pixels, 0, measureBitmap.getWidth(), 0, 0, w, h);
int sX = -1;
ArrayList<Long> keyPoints = new ArrayList<>(pixels.length);
for (int x = 0; x < w; x++) {
for (int y = 0; y < h; y++) {
int clr = pixels[y * measureBitmap.getWidth() + x];
if (Color.alpha(clr) >= 0x80) {
if (sX == -1) {
sX = x;
}
keyPoints.add(((long) (x - sX) << 16) + y);
}
}
}
keyPoints.trimToSize();
measureBitmap.recycle();
return keyPoints;
}
/**
* @return Max particles count
*/
public int getMaxParticlesCount() {
return maxParticles;
}
/**
* Sets new max particles count
*/
public void setMaxParticlesCount(int maxParticles) {
this.maxParticles = maxParticles;
while (particlesPool.size() + particles.size() < maxParticles) {
particlesPool.push(new Particle());
}
}
/**
* Alias for it's big bro
*
* @param tv Text view to use as a parent view
* @param spoilersPool Cached spoilers pool
* @param spoilers Spoilers list to populate
*/
public static void addSpoilers(TextView tv, @Nullable Stack<SpoilerEffect> spoilersPool, List<SpoilerEffect> spoilers) {
addSpoilers(tv, tv.getLayout(), (Spannable) tv.getText(), spoilersPool, spoilers);
}
/**
* Alias for it's big bro
*
* @param v View to use as a parent view
* @param textLayout Text layout to measure
* @param spoilersPool Cached spoilers pool, could be null, but highly recommended
* @param spoilers Spoilers list to populate
*/
public static void addSpoilers(@Nullable View v, Layout textLayout, @Nullable Stack<SpoilerEffect> spoilersPool, List<SpoilerEffect> spoilers) {
if (textLayout.getText() instanceof Spannable){
addSpoilers(v, textLayout, (Spannable) textLayout.getText(), spoilersPool, spoilers);
}
}
/**
* Parses spoilers from spannable
*
* @param v View to use as a parent view
* @param textLayout Text layout to measure
* @param spannable Text to parse
* @param spoilersPool Cached spoilers pool, could be null, but highly recommended
* @param spoilers Spoilers list to populate
*/
public static void addSpoilers(@Nullable View v, Layout textLayout, Spannable spannable, @Nullable Stack<SpoilerEffect> spoilersPool, List<SpoilerEffect> spoilers) {
for (int line = 0; line < textLayout.getLineCount(); line++) {
float l = textLayout.getLineLeft(line), t = textLayout.getLineTop(line), r = textLayout.getLineRight(line), b = textLayout.getLineBottom(line);
int start = textLayout.getLineStart(line), end = textLayout.getLineEnd(line);
for (TextStyleSpan span : spannable.getSpans(start, end, TextStyleSpan.class)) {
if (span.isSpoiler()) {
int ss = spannable.getSpanStart(span), se = spannable.getSpanEnd(span);
int realStart = Math.max(start, ss), realEnd = Math.min(end, se);
int len = realEnd - realStart;
if (len == 0) continue;
addSpoilersInternal(v, spannable, textLayout, start, end, l, t, r, b, realStart, realEnd, spoilersPool, spoilers);
}
}
}
if (v instanceof TextView && spoilersPool != null) {
spoilersPool.clear();
}
}
@SuppressLint("WrongConstant")
private static void addSpoilersInternal(View v, Spannable spannable, Layout textLayout, int lineStart,
int lineEnd, float lineLeft, float lineTop, float lineRight,
float lineBottom, int realStart, int realEnd, Stack<SpoilerEffect> spoilersPool,
List<SpoilerEffect> spoilers) {
SpannableStringBuilder vSpan = SpannableStringBuilder.valueOf(AndroidUtilities.replaceNewLines(new SpannableStringBuilder(spannable, realStart, realEnd)));
for (TextStyleSpan styleSpan : vSpan.getSpans(0, vSpan.length(), TextStyleSpan.class))
vSpan.removeSpan(styleSpan);
for (URLSpan urlSpan : vSpan.getSpans(0, vSpan.length(), URLSpan.class))
vSpan.removeSpan(urlSpan);
int tLen = vSpan.toString().trim().length();
if (tLen == 0) return;
int width = textLayout.getEllipsizedWidth() > 0 ? textLayout.getEllipsizedWidth() : textLayout.getWidth();
TextPaint measurePaint = new TextPaint(textLayout.getPaint());
measurePaint.setColor(Color.BLACK);
StaticLayout newLayout;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
newLayout = StaticLayout.Builder.obtain(vSpan, 0, vSpan.length(), measurePaint, width)
.setBreakStrategy(StaticLayout.BREAK_STRATEGY_HIGH_QUALITY)
.setHyphenationFrequency(StaticLayout.HYPHENATION_FREQUENCY_NONE)
.setAlignment(Layout.Alignment.ALIGN_NORMAL)
.setLineSpacing(textLayout.getSpacingAdd(), textLayout.getSpacingMultiplier())
.build();
} else
newLayout = new StaticLayout(vSpan, measurePaint, width, Layout.Alignment.ALIGN_NORMAL, textLayout.getSpacingMultiplier(), textLayout.getSpacingAdd(), false);
boolean rtlInNonRTL = (LocaleController.isRTLCharacter(vSpan.charAt(0)) || LocaleController.isRTLCharacter(vSpan.charAt(vSpan.length() - 1))) && !LocaleController.isRTL;
SpoilerEffect spoilerEffect = spoilersPool == null || spoilersPool.isEmpty() ? new SpoilerEffect() : spoilersPool.remove(0);
spoilerEffect.setRippleProgress(-1);
float ps = realStart == lineStart ? lineLeft : textLayout.getPrimaryHorizontal(realStart),
pe = realEnd == lineEnd || rtlInNonRTL && realEnd == lineEnd - 1 && spannable.charAt(lineEnd - 1) == '\u2026' ? lineRight : textLayout.getPrimaryHorizontal(realEnd);
spoilerEffect.setBounds((int) Math.min(ps, pe), (int) lineTop, (int) Math.max(ps, pe), (int) lineBottom);
spoilerEffect.setColor(textLayout.getPaint().getColor());
spoilerEffect.setRippleInterpolator(Easings.easeInQuad);
if (!spoilerEffect.isLowDevice)
spoilerEffect.setKeyPoints(SpoilerEffect.measureKeyPoints(newLayout));
spoilerEffect.updateMaxParticles();
if (v != null) {
spoilerEffect.setParentView(v);
}
spoilerEffect.spaces.clear();
for (int i = 0; i < vSpan.length(); i++) {
if (vSpan.charAt(i) == ' ') {
RectF r = new RectF();
int off = realStart + i;
int line = textLayout.getLineForOffset(off);
r.top = textLayout.getLineTop(line);
r.bottom = textLayout.getLineBottom(line);
float lh = textLayout.getPrimaryHorizontal(off), rh = textLayout.getPrimaryHorizontal(off + 1);
r.left = (int) Math.min(lh, rh); // RTL
r.right = (int) Math.max(lh, rh);
if (Math.abs(lh - rh) <= AndroidUtilities.dp(20)) {
spoilerEffect.spaces.add(r);
}
}
}
spoilers.add(spoilerEffect);
}
/**
* Clips out spoilers from canvas
*/
public static void clipOutCanvas(Canvas canvas, List<SpoilerEffect> spoilers) {
tempPath.rewind();
for (SpoilerEffect eff : spoilers) {
Rect b = eff.getBounds();
tempPath.addRect(b.left, b.top, b.right, b.bottom, Path.Direction.CW);
}
canvas.clipPath(tempPath, Region.Op.DIFFERENCE);
}
/**
* Optimized version of text layout double-render
*
* @param v View to use as a parent view
* @param invalidateSpoilersParent Set to invalidate parent or not
* @param spoilersColor Spoilers' color
* @param verticalOffset Additional vertical offset
* @param patchedLayoutRef Patched layout reference
* @param textLayout Layout to render
* @param spoilers Spoilers list to render
* @param canvas Canvas to render
*/
@SuppressLint("WrongConstant")
@MainThread
public static void renderWithRipple(View v, boolean invalidateSpoilersParent, int spoilersColor, int verticalOffset, AtomicReference<Layout> patchedLayoutRef, Layout textLayout, List<SpoilerEffect> spoilers, Canvas canvas) {
if (spoilers.isEmpty()) {
textLayout.draw(canvas);
return;
}
Layout pl = patchedLayoutRef.get();
if (pl == null || !textLayout.getText().toString().equals(pl.getText().toString()) || textLayout.getWidth() != pl.getWidth() || textLayout.getHeight() != pl.getHeight()) {
SpannableStringBuilder sb = new SpannableStringBuilder(textLayout.getText());
Spannable sp = (Spannable) textLayout.getText();
for (TextStyleSpan ss : sp.getSpans(0, sp.length(), TextStyleSpan.class)) {
if (ss.isSpoiler()) {
int start = sp.getSpanStart(ss), end = sp.getSpanEnd(ss);
for (Emoji.EmojiSpan e : sp.getSpans(start, end, Emoji.EmojiSpan.class)) {
sb.setSpan(new ReplacementSpan() {
@Override
public int getSize(@NonNull Paint paint, CharSequence text, int start, int end, @Nullable Paint.FontMetricsInt fm) {
return e.getSize(paint, text, start, end, fm);
}
@Override
public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, @NonNull Paint paint) {
}
}, start, end, sp.getSpanFlags(ss));
sb.removeSpan(e);
}
sb.setSpan(new ForegroundColorSpan(Color.TRANSPARENT), sp.getSpanStart(ss), sp.getSpanEnd(ss), sp.getSpanFlags(ss));
sb.removeSpan(ss);
}
}
Layout layout;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
layout = StaticLayout.Builder.obtain(sb, 0, sb.length(), textLayout.getPaint(), textLayout.getWidth())
.setBreakStrategy(StaticLayout.BREAK_STRATEGY_HIGH_QUALITY)
.setHyphenationFrequency(StaticLayout.HYPHENATION_FREQUENCY_NONE)
.setAlignment(Layout.Alignment.ALIGN_NORMAL)
.setLineSpacing(textLayout.getSpacingAdd(), textLayout.getSpacingMultiplier())
.build();
} else {
layout = new StaticLayout(sb, textLayout.getPaint(), textLayout.getWidth(), textLayout.getAlignment(), textLayout.getSpacingMultiplier(), textLayout.getSpacingAdd(), false);
}
patchedLayoutRef.set(pl = layout);
}
if (!spoilers.isEmpty()) {
canvas.save();
canvas.translate(0, verticalOffset);
pl.draw(canvas);
canvas.restore();
} else {
textLayout.draw(canvas);
}
if (!spoilers.isEmpty()) {
tempPath.rewind();
for (SpoilerEffect eff : spoilers) {
Rect b = eff.getBounds();
tempPath.addRect(b.left, b.top, b.right, b.bottom, Path.Direction.CW);
}
if (!spoilers.isEmpty() && spoilers.get(0).rippleProgress != -1) {
canvas.save();
canvas.clipPath(tempPath);
tempPath.rewind();
if (!spoilers.isEmpty()) {
spoilers.get(0).getRipplePath(tempPath);
}
canvas.clipPath(tempPath);
canvas.translate(0, -v.getPaddingTop());
textLayout.draw(canvas);
canvas.restore();
}
boolean useAlphaLayer = spoilers.get(0).rippleProgress != -1;
if (useAlphaLayer) {
canvas.saveLayer(0, 0, v.getMeasuredWidth(), v.getMeasuredHeight(), null, canvas.ALL_SAVE_FLAG);
} else {
canvas.save();
}
canvas.translate(0, -v.getPaddingTop());
for (SpoilerEffect eff : spoilers) {
eff.setInvalidateParent(invalidateSpoilersParent);
if (eff.getParentView() != v) eff.setParentView(v);
if (eff.shouldInvalidateColor()) {
eff.setColor(ColorUtils.blendARGB(spoilersColor, Theme.chat_msgTextPaint.getColor(), Math.max(0, eff.getRippleProgress())));
} else {
eff.setColor(spoilersColor);
}
eff.draw(canvas);
}
if (useAlphaLayer) {
tempPath.rewind();
spoilers.get(0).getRipplePath(tempPath);
if (xRefPaint == null) {
xRefPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
xRefPaint.setColor(0xff000000);
xRefPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
}
canvas.drawPath(tempPath, xRefPaint);
}
canvas.restore();
}
}
private static class Particle {
private float x, y;
private float vecX, vecY;
private float velocity;
private float lifeTime, currentTime;
private int alpha;
}
}