package tw.nekomimi.nekogram.helpers; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.ColorFilter; import android.graphics.LinearGradient; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.PixelFormat; import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.Shader; import android.graphics.drawable.Drawable; import androidx.annotation.NonNull; import org.telegram.messenger.AndroidUtilities; import org.telegram.messenger.ApplicationLoader; import org.telegram.messenger.R; /** * Sponsor badge that renders the GhostCloud "sponsor.png" heart and overlays a * moving glossy highlight ("shimmer") that is clipped to the artwork's shape * via its alpha channel, so it looks like light sweeping across the heart. * * It self-invalidates each frame, so when attached to a view via * {@code setRightDrawable(...)} / {@code setRightDrawable2(...)} the host keeps * repainting automatically (the SimpleTextView drawable callback chain handles * invalidation). */ public class ShimmerHeartDrawable extends Drawable { private static Bitmap sharedBitmap; private final Paint bitmapPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG); private final Paint shinePaint = new Paint(Paint.ANTI_ALIAS_FLAG); private final Paint maskPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG); private final Matrix shineMatrix = new Matrix(); private final Bitmap bitmap; private LinearGradient shineGradient; private int shineWidth; private static final long CYCLE_MS = 2600L; private final int size; public ShimmerHeartDrawable() { this(AndroidUtilities.dp(18)); } public ShimmerHeartDrawable(int sizePx) { this.size = sizePx; bitmap = loadBitmap(); // The moving highlight is drawn only where the artwork is opaque. maskPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN)); } private static Bitmap loadBitmap() { if (sharedBitmap == null || sharedBitmap.isRecycled()) { try { Context ctx = ApplicationLoader.applicationContext; BitmapFactory.Options opts = new BitmapFactory.Options(); opts.inScaled = false; sharedBitmap = BitmapFactory.decodeResource(ctx.getResources(), R.drawable.foxsponsor_heart, opts); } catch (Throwable ignore) { } } return sharedBitmap; } /** Shared heart artwork, lazily decoded. May be null if decoding failed. */ public static Bitmap getSharedBitmap() { return loadBitmap(); } private static final Paint STATIC_PAINT = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG); /** * Draw the sponsor heart statically (no shimmer) into {@code dst}. Cheap * enough for list cells and headers that repaint frequently. */ public static void drawStatic(@NonNull Canvas canvas, @NonNull RectF dst, int alpha) { Bitmap bmp = loadBitmap(); if (bmp == null) { return; } if (alpha < 255) { STATIC_PAINT.setAlpha(alpha); } else { STATIC_PAINT.setAlpha(255); } canvas.drawBitmap(bmp, null, dst, STATIC_PAINT); } private void buildShine(int w) { // A bright diagonal band, narrow relative to the badge width. shineWidth = Math.max(1, (int) (w * 0.55f)); shineGradient = new LinearGradient( 0, 0, shineWidth, 0, new int[]{0x00FFFFFF, 0x00FFFFFF, 0x99FFFFFF, 0x00FFFFFF, 0x00FFFFFF}, new float[]{0f, 0.35f, 0.5f, 0.65f, 1f}, Shader.TileMode.CLAMP); shinePaint.setShader(shineGradient); } @Override public void draw(@NonNull Canvas canvas) { Rect b = getBounds(); if (b.width() == 0 || b.height() == 0 || bitmap == null) { return; } if (shineGradient == null || shineWidth != (int) (b.width() * 0.55f)) { buildShine(b.width()); } RectF dst = new RectF(b); // Layer so the shimmer can be masked against the artwork alpha. int sc = canvas.saveLayer(dst, null); // 1) the heart artwork canvas.drawBitmap(bitmap, null, dst, bitmapPaint); // 2) moving highlight band, swept diagonally float phase = (System.currentTimeMillis() % CYCLE_MS) / (float) CYCLE_MS; float travel = b.width() + shineWidth; float x = b.left - shineWidth + phase * travel; shineMatrix.reset(); // slight diagonal slant shineMatrix.postRotate(20f, 0, 0); shineMatrix.postTranslate(x, b.top); shineGradient.setLocalMatrix(shineMatrix); int ssc = canvas.saveLayer(dst, null); canvas.drawRect(dst, shinePaint); // keep the highlight only where the artwork is opaque canvas.drawBitmap(bitmap, null, dst, maskPaint); canvas.restoreToCount(ssc); canvas.restoreToCount(sc); invalidateSelf(); } @Override public int getIntrinsicWidth() { return size; } @Override public int getIntrinsicHeight() { return size; } @Override public void setAlpha(int alpha) { bitmapPaint.setAlpha(alpha); } @Override public void setColorFilter(ColorFilter colorFilter) { bitmapPaint.setColorFilter(colorFilter); } @Override public int getOpacity() { return PixelFormat.TRANSLUCENT; } }