- 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
173 lines
5.6 KiB
Java
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;
|
|
}
|
|
}
|