FoxiGram/TMessagesProj/src/main/java/tw/nekomimi/nekogram/helpers/ShimmerHeartDrawable.java
instant992 00479f6f98 Show sponsor badge next to names everywhere + update app icon
- Sponsor heart now appears in the chats list (DialogCell), chat header
  (ChatAvatarContainer), message author names (ChatMessageCell) and user
  lists (UserCell), not just on the profile screen
- Added ShimmerHeartDrawable.drawStatic() for cheap static rendering in
  frequently-repainted list cells; header keeps the animated shimmer
- Cells request sponsor status via SponsorHelper batch lookup and redraw
  on sponsorStatusUpdated
- Regenerate launcher icon from updated foxigram4.png
2026-06-10 01:00:16 +04:00

173 lines
5.6 KiB
Java

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;
}
}