Sponsor heart: render sponsor.png with a moving shimmer highlight

- Bundle the GhostCloud sponsor.png as drawable-nodpi/foxsponsor_heart.png
- Draw the artwork and sweep a glossy highlight band across it, masked to
  the heart shape via the bitmap alpha (DST_IN), so it shimmers again
This commit is contained in:
instant992 2026-06-09 13:17:52 +04:00
parent e2f2ceb26d
commit 76093b407e
2 changed files with 72 additions and 90 deletions

View file

@ -1,24 +1,31 @@
package tw.nekomimi.nekogram.helpers; package tw.nekomimi.nekogram.helpers;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas; import android.graphics.Canvas;
import android.graphics.ColorFilter; import android.graphics.ColorFilter;
import android.graphics.LinearGradient;
import android.graphics.Matrix;
import android.graphics.Paint; import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PixelFormat; import android.graphics.PixelFormat;
import android.graphics.RadialGradient; import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect; import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Shader; import android.graphics.Shader;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import org.telegram.messenger.AndroidUtilities; import org.telegram.messenger.AndroidUtilities;
import org.telegram.messenger.ApplicationLoader;
import org.telegram.messenger.R;
/** /**
* A soft, airbrushed 3D heart badge matching the GhostCloud "sponsor.png": * Sponsor badge that renders the GhostCloud "sponsor.png" heart and overlays a
* violet in the upper-left, blue through the centre, warm orange/coral on the * moving glossy highlight ("shimmer") that is clipped to the artwork's shape
* right and bottom, all blended smoothly with a gentle glossy highlight. The * via its alpha channel, so it looks like light sweeping across the heart.
* colour blobs drift slightly so the badge subtly shimmers.
* *
* It self-invalidates each frame, so when attached to a view via * It self-invalidates each frame, so when attached to a view via
* {@code setRightDrawable(...)} / {@code setRightDrawable2(...)} the host keeps * {@code setRightDrawable(...)} / {@code setRightDrawable2(...)} the host keeps
@ -27,15 +34,18 @@ import org.telegram.messenger.AndroidUtilities;
*/ */
public class ShimmerHeartDrawable extends Drawable { public class ShimmerHeartDrawable extends Drawable {
private final Paint basePaint = new Paint(Paint.ANTI_ALIAS_FLAG); private static Bitmap sharedBitmap;
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 final Paint bitmapPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
private int lastHeight = -1; 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 static final long CYCLE_MS = 5200L; private final Bitmap bitmap;
private LinearGradient shineGradient;
private int shineWidth;
private static final long CYCLE_MS = 2600L;
private final int size; private final int size;
public ShimmerHeartDrawable() { public ShimmerHeartDrawable() {
@ -44,98 +54,70 @@ public class ShimmerHeartDrawable extends Drawable {
public ShimmerHeartDrawable(int sizePx) { public ShimmerHeartDrawable(int sizePx) {
this.size = sizePx; this.size = sizePx;
basePaint.setStyle(Paint.Style.FILL); bitmap = loadBitmap();
blobPaint.setStyle(Paint.Style.FILL); // The moving highlight is drawn only where the artwork is opaque.
highlightPaint.setStyle(Paint.Style.FILL); maskPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
} }
private void buildHeart(Rect b) { private static Bitmap loadBitmap() {
heart.reset(); if (sharedBitmap == null || sharedBitmap.isRecycled()) {
float w = b.width(); try {
float h = b.height(); Context ctx = ApplicationLoader.applicationContext;
float l = b.left; BitmapFactory.Options opts = new BitmapFactory.Options();
float t = b.top; opts.inScaled = false;
float cx = l + w / 2f; sharedBitmap = BitmapFactory.decodeResource(ctx.getResources(), R.drawable.foxsponsor_heart, opts);
heart.moveTo(cx, t + h * 0.30f); } catch (Throwable ignore) {
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); return sharedBitmap;
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 buildShine(int w) {
private void drawBlob(Canvas canvas, float cx, float cy, float r, int color) { // A bright diagonal band, narrow relative to the badge width.
blobPaint.setShader(new RadialGradient( shineWidth = Math.max(1, (int) (w * 0.55f));
cx, cy, Math.max(1f, r), shineGradient = new LinearGradient(
new int[]{color, color & 0x00FFFFFF}, 0, 0, shineWidth, 0,
new float[]{0f, 1f}, new int[]{0x00FFFFFF, 0x00FFFFFF, 0x99FFFFFF, 0x00FFFFFF, 0x00FFFFFF},
Shader.TileMode.CLAMP)); new float[]{0f, 0.35f, 0.5f, 0.65f, 1f},
canvas.drawRect(getBounds(), blobPaint); Shader.TileMode.CLAMP);
} shinePaint.setShader(shineGradient);
@Override
protected void onBoundsChange(@NonNull Rect bounds) {
super.onBoundsChange(bounds);
buildHeart(bounds);
lastWidth = bounds.width();
lastHeight = bounds.height();
} }
@Override @Override
public void draw(@NonNull Canvas canvas) { public void draw(@NonNull Canvas canvas) {
Rect b = getBounds(); Rect b = getBounds();
if (b.width() == 0 || b.height() == 0) { if (b.width() == 0 || b.height() == 0 || bitmap == null) {
return; return;
} }
if (lastWidth != b.width() || lastHeight != b.height()) { if (shineGradient == null || shineWidth != (int) (b.width() * 0.55f)) {
buildHeart(b); buildShine(b.width());
lastWidth = b.width();
lastHeight = b.height();
} }
float w = b.width(); RectF dst = new RectF(b);
float h = b.height();
float l = b.left;
float t = b.top;
// Subtle drift so the colours softly shimmer. // 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 phase = (System.currentTimeMillis() % CYCLE_MS) / (float) CYCLE_MS;
double a = phase * 2 * Math.PI; float travel = b.width() + shineWidth;
float dx = (float) Math.cos(a) * w * 0.05f; float x = b.left - shineWidth + phase * travel;
float dy = (float) Math.sin(a) * h * 0.05f; shineMatrix.reset();
// slight diagonal slant
shineMatrix.postRotate(20f, 0, 0);
shineMatrix.postTranslate(x, b.top);
shineGradient.setLocalMatrix(shineMatrix);
int save = canvas.save(); int ssc = canvas.saveLayer(dst, null);
canvas.clipPath(heart); canvas.drawRect(dst, shinePaint);
// keep the highlight only where the artwork is opaque
canvas.drawBitmap(bitmap, null, dst, maskPaint);
canvas.restoreToCount(ssc);
// Light lavender base so blends stay airy. canvas.restoreToCount(sc);
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(); invalidateSelf();
} }
@ -152,12 +134,12 @@ public class ShimmerHeartDrawable extends Drawable {
@Override @Override
public void setAlpha(int alpha) { public void setAlpha(int alpha) {
basePaint.setAlpha(alpha); bitmapPaint.setAlpha(alpha);
} }
@Override @Override
public void setColorFilter(ColorFilter colorFilter) { public void setColorFilter(ColorFilter colorFilter) {
basePaint.setColorFilter(colorFilter); bitmapPaint.setColorFilter(colorFilter);
} }
@Override @Override

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB