package tw.nekomimi.nekogram.helpers; import android.graphics.Bitmap; import android.graphics.BlurMaskFilter; import android.graphics.Canvas; import android.graphics.Paint; import android.text.style.ReplacementSpan; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.telegram.messenger.AndroidUtilities; /** * Renders a piece of text genuinely blurred (Gaussian-style), instead of just * masking the characters. The real glyphs are drawn into an offscreen software * bitmap with a {@link BlurMaskFilter} applied and then blitted onto the target * canvas, so it works the same on both software and hardware-accelerated views. */ public class BlurredTextSpan extends ReplacementSpan { private final String text; private final float blurRadius; private Bitmap cache; private int cacheWidth; private int cacheHeight; private float cacheTextSize; private int cacheColor; private float cacheBaseline; private int cachePad; public BlurredTextSpan(String text) { this(text, AndroidUtilities.dp(3.5f)); } public BlurredTextSpan(String text, float blurRadius) { this.text = text == null ? "" : text; this.blurRadius = Math.max(0.5f, blurRadius); } @Override public int getSize(@NonNull Paint paint, CharSequence charSequence, int start, int end, @Nullable Paint.FontMetricsInt fm) { if (fm != null) { Paint.FontMetricsInt pfm = paint.getFontMetricsInt(); fm.ascent = pfm.ascent; fm.descent = pfm.descent; fm.top = pfm.top; fm.bottom = pfm.bottom; fm.leading = pfm.leading; } return (int) Math.ceil(paint.measureText(text)); } @Override public void draw(@NonNull Canvas canvas, CharSequence charSequence, int start, int end, float x, int top, int y, int bottom, @NonNull Paint paint) { ensureCache(paint); if (cache == null) { return; } canvas.drawBitmap(cache, x - cachePad, y - cacheBaseline, null); } private void ensureCache(Paint paint) { final float textSize = paint.getTextSize(); final int color = paint.getColor(); final int width = (int) Math.ceil(paint.measureText(text)); if (width <= 0) { cache = null; return; } // padding so the blur isn't clipped at the edges final int pad = (int) Math.ceil(blurRadius * 2.5f); Paint.FontMetricsInt fm = paint.getFontMetricsInt(); final int textHeight = fm.descent - fm.ascent; final int w = width + pad * 2; final int h = textHeight + pad * 2; if (cache != null && cacheWidth == w && cacheHeight == h && cacheTextSize == textSize && cacheColor == color) { return; } if (cache != null) { cache.recycle(); } try { cache = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888); } catch (Throwable e) { cache = null; return; } cacheWidth = w; cacheHeight = h; cacheTextSize = textSize; cacheColor = color; cachePad = pad; // baseline offset inside the cache bitmap, relative to the text top cacheBaseline = pad - fm.ascent; Canvas c = new Canvas(cache); Paint p = new Paint(Paint.ANTI_ALIAS_FLAG); p.setTextSize(textSize); p.setColor(color); p.setTypeface(paint.getTypeface()); p.setMaskFilter(new BlurMaskFilter(blurRadius, BlurMaskFilter.Blur.NORMAL)); c.drawText(text, pad, cacheBaseline, p); } }