- Add BlurredTextSpan: draws the real glyphs through a BlurMaskFilter into a cached offscreen bitmap, so the blur works on both software and hardware-accelerated views - formatOwnPhone() now returns this span in Blur mode instead of replacing digits with bullets - SettingsActivity header builds the subtitle with a SpannableStringBuilder so the span is preserved
109 lines
3.6 KiB
Java
109 lines
3.6 KiB
Java
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);
|
|
}
|
|
}
|