package tw.nekomimi.nekogram.helpers; import android.graphics.Canvas; import android.graphics.ColorFilter; import android.graphics.Paint; import android.graphics.Path; import android.graphics.PixelFormat; import android.graphics.RadialGradient; import android.graphics.Rect; import android.graphics.Shader; import android.graphics.drawable.Drawable; import androidx.annotation.NonNull; import org.telegram.messenger.AndroidUtilities; /** * A soft, airbrushed 3D heart badge matching the GhostCloud "sponsor.png": * violet in the upper-left, blue through the centre, warm orange/coral on the * right and bottom, all blended smoothly with a gentle glossy highlight. The * colour blobs drift slightly so the badge subtly shimmers. * * 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 final Paint basePaint = new Paint(Paint.ANTI_ALIAS_FLAG); private final Paint blobPaint = new Paint(Paint.ANTI_ALIAS_FLAG); private final Paint highlightPaint = new Paint(Paint.ANTI_ALIAS_FLAG); private final Path heart = new Path(); private int lastWidth = -1; private int lastHeight = -1; private static final long CYCLE_MS = 5200L; private final int size; public ShimmerHeartDrawable() { this(AndroidUtilities.dp(18)); } public ShimmerHeartDrawable(int sizePx) { this.size = sizePx; basePaint.setStyle(Paint.Style.FILL); blobPaint.setStyle(Paint.Style.FILL); highlightPaint.setStyle(Paint.Style.FILL); } private void buildHeart(Rect b) { heart.reset(); float w = b.width(); float h = b.height(); float l = b.left; float t = b.top; float cx = l + w / 2f; heart.moveTo(cx, t + h * 0.30f); heart.cubicTo(l + w * 0.40f, t + h * 0.05f, l + w * 0.02f, t + h * 0.18f, l + w * 0.10f, t + h * 0.45f); heart.cubicTo(l + w * 0.17f, t + h * 0.66f, l + w * 0.40f, t + h * 0.80f, cx, t + h * 0.97f); heart.cubicTo(l + w * 0.60f, t + h * 0.80f, l + w * 0.83f, t + h * 0.66f, l + w * 0.90f, t + h * 0.45f); heart.cubicTo(l + w * 0.98f, t + h * 0.18f, l + w * 0.60f, t + h * 0.05f, cx, t + h * 0.30f); heart.close(); } /** A soft circular blob of one colour fading to transparent. */ private void drawBlob(Canvas canvas, float cx, float cy, float r, int color) { blobPaint.setShader(new RadialGradient( cx, cy, Math.max(1f, r), new int[]{color, color & 0x00FFFFFF}, new float[]{0f, 1f}, Shader.TileMode.CLAMP)); canvas.drawRect(getBounds(), blobPaint); } @Override protected void onBoundsChange(@NonNull Rect bounds) { super.onBoundsChange(bounds); buildHeart(bounds); lastWidth = bounds.width(); lastHeight = bounds.height(); } @Override public void draw(@NonNull Canvas canvas) { Rect b = getBounds(); if (b.width() == 0 || b.height() == 0) { return; } if (lastWidth != b.width() || lastHeight != b.height()) { buildHeart(b); lastWidth = b.width(); lastHeight = b.height(); } float w = b.width(); float h = b.height(); float l = b.left; float t = b.top; // Subtle drift so the colours softly shimmer. float phase = (System.currentTimeMillis() % CYCLE_MS) / (float) CYCLE_MS; double a = phase * 2 * Math.PI; float dx = (float) Math.cos(a) * w * 0.05f; float dy = (float) Math.sin(a) * h * 0.05f; int save = canvas.save(); canvas.clipPath(heart); // Light lavender base so blends stay airy. basePaint.setShader(null); basePaint.setColor(0xFFB9A8F0); canvas.drawRect(b, basePaint); // Soft overlapping colour blobs (airbrushed look). // Violet — upper-left. drawBlob(canvas, l + w * (0.30f) + dx, t + h * (0.28f) + dy, w * 0.62f, 0xFF7A2FE0); // Blue — centre-left. drawBlob(canvas, l + w * (0.40f) - dx, t + h * (0.55f) + dy, w * 0.55f, 0xFF4F6BFF); // Cyan/blue glow — centre. drawBlob(canvas, l + w * (0.52f) + dy, t + h * (0.45f) - dx, w * 0.42f, 0xCC59B7FF); // Warm orange — right. drawBlob(canvas, l + w * (0.82f) - dx, t + h * (0.40f) - dy, w * 0.60f, 0xFFFF9A3D); // Coral/pink — bottom-right. drawBlob(canvas, l + w * (0.70f) + dx, t + h * (0.80f) + dy, w * 0.55f, 0xFFFF7E5A); // Deep violet — bottom-left corner for contrast. drawBlob(canvas, l + w * (0.22f) + dx, t + h * (0.82f) - dy, w * 0.45f, 0xCC6A2BC8); // Gentle glossy highlight, upper-centre. highlightPaint.setShader(new RadialGradient( l + w * 0.45f + dx, t + h * 0.30f + dy, w * 0.40f, new int[]{0x80FFFFFF, 0x00FFFFFF}, new float[]{0f, 1f}, Shader.TileMode.CLAMP)); canvas.drawRect(b, highlightPaint); canvas.restoreToCount(save); invalidateSelf(); } @Override public int getIntrinsicWidth() { return size; } @Override public int getIntrinsicHeight() { return size; } @Override public void setAlpha(int alpha) { basePaint.setAlpha(alpha); } @Override public void setColorFilter(ColorFilter colorFilter) { basePaint.setColorFilter(colorFilter); } @Override public int getOpacity() { return PixelFormat.TRANSLUCENT; } }