Sponsor heart: glossy 3D look with moving specular highlight

Replace the flat linear rainbow sweep with a glossy sticker-style heart:
diagonal purple/blue-to-orange base, soft purple glow, bottom shade, and
a moving glossy highlight orbiting inside plus a fixed top-left specular
dot for an iridescent emoji feel.
This commit is contained in:
instant992 2026-06-09 09:27:11 +04:00
parent 7eeae7fe82
commit 9b81a463a6

View file

@ -3,10 +3,10 @@ package tw.nekomimi.nekogram.helpers;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.LinearGradient;
import android.graphics.Matrix;
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;
@ -16,36 +16,27 @@ import androidx.annotation.NonNull;
import org.telegram.messenger.AndroidUtilities;
/**
* A heart-shaped badge filled with a continuously moving iridescent gradient
* ("shimmer"). It self-invalidates each frame, so when attached to a view via
* {@code setRightDrawable(...)} the host keeps repainting automatically (the
* SimpleTextView drawable callback chain handles invalidation).
* A glossy 3D-looking heart badge with a soft multicolor gradient (purple/blue
* to orange) and a moving glossy highlight ("blik") sweeping across it, like an
* iridescent emoji sticker.
*
* 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 paint = new Paint(Paint.ANTI_ALIAS_FLAG);
private final Paint basePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
private final Paint tintPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
private final Paint highlightPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
private final Paint shadePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
private final Path heart = new Path();
private final Matrix gradientMatrix = new Matrix();
private LinearGradient gradient;
private int lastWidth = -1;
private int gradientWidth;
private int lastHeight = -1;
// Full rainbow spectrum swept across the heart (seamless loop).
private static final int[] COLORS = new int[] {
0xFFFF3B30, // red
0xFFFF9500, // orange
0xFFFFCC00, // yellow
0xFF34C759, // green
0xFF00C7BE, // teal
0xFF007AFF, // blue
0xFF5856D6, // indigo
0xFFAF52DE, // violet
0xFFFF2D55, // pink
0xFFFF3B30, // back to red (seamless loop)
};
private static final long CYCLE_MS = 2200L;
private static final long CYCLE_MS = 3200L;
private final int size;
public ShimmerHeartDrawable() {
@ -54,16 +45,10 @@ public class ShimmerHeartDrawable extends Drawable {
public ShimmerHeartDrawable(int sizePx) {
this.size = sizePx;
paint.setStyle(Paint.Style.FILL);
}
private void buildGradient(int width) {
// Span the whole rainbow across roughly the heart width so multiple
// colors are visible at once, then sweep it for the shimmer.
gradientWidth = Math.max(1, width);
gradient = new LinearGradient(0, 0, gradientWidth, 0, COLORS, null, Shader.TileMode.MIRROR);
paint.setShader(gradient);
lastWidth = width;
basePaint.setStyle(Paint.Style.FILL);
tintPaint.setStyle(Paint.Style.FILL);
highlightPaint.setStyle(Paint.Style.FILL);
shadePaint.setStyle(Paint.Style.FILL);
}
private void buildHeart(Rect b) {
@ -72,7 +57,6 @@ public class ShimmerHeartDrawable extends Drawable {
float h = b.height();
float l = b.left;
float t = b.top;
// Heart path normalized to the bounds.
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);
@ -82,11 +66,40 @@ public class ShimmerHeartDrawable extends Drawable {
heart.close();
}
private void buildShaders(Rect b) {
float w = b.width();
float h = b.height();
float l = b.left;
float t = b.top;
// Base diagonal gradient: deep purple (top-left) -> blue -> warm orange (bottom-right).
basePaint.setShader(new LinearGradient(
l, t, l + w, t + h,
new int[]{0xFF7A3CE0, 0xFF5B6BFF, 0xFF4FA8FF, 0xFFFF9A4D, 0xFFFF7A3C},
new float[]{0f, 0.32f, 0.55f, 0.82f, 1f},
Shader.TileMode.CLAMP));
// A soft purple glow blob on the upper-left lobe for depth.
tintPaint.setShader(new RadialGradient(
l + w * 0.30f, t + h * 0.30f, w * 0.55f,
new int[]{0xCC8A4DFF, 0x00000000},
null, Shader.TileMode.CLAMP));
// Soft inner shade at the bottom tip for a rounded 3D feel.
shadePaint.setShader(new RadialGradient(
l + w * 0.5f, t + h * 0.95f, w * 0.6f,
new int[]{0x66351A6B, 0x00000000},
null, Shader.TileMode.CLAMP));
lastWidth = b.width();
lastHeight = b.height();
}
@Override
protected void onBoundsChange(@NonNull Rect bounds) {
super.onBoundsChange(bounds);
buildHeart(bounds);
buildGradient(bounds.width());
buildShaders(bounds);
}
@Override
@ -95,15 +108,44 @@ public class ShimmerHeartDrawable extends Drawable {
if (b.width() == 0 || b.height() == 0) {
return;
}
if (gradient == null || lastWidth != b.width()) {
buildGradient(b.width());
if (lastWidth != b.width() || lastHeight != b.height()) {
buildHeart(b);
buildShaders(b);
}
float w = b.width();
float h = b.height();
float phase = (System.currentTimeMillis() % CYCLE_MS) / (float) CYCLE_MS;
gradientMatrix.reset();
gradientMatrix.setTranslate(b.left - phase * gradientWidth, 0);
gradient.setLocalMatrix(gradientMatrix);
canvas.drawPath(heart, paint);
double ang = phase * 2 * Math.PI;
int save = canvas.save();
canvas.clipPath(heart);
// Base colors + purple glow + bottom shade.
canvas.drawPath(heart, basePaint);
canvas.drawPath(heart, tintPaint);
canvas.drawPath(heart, shadePaint);
// Moving glossy highlight: a soft bright blob orbiting inside the heart.
float hx = b.left + w * (0.5f + 0.28f * (float) Math.cos(ang));
float hy = b.top + h * (0.42f + 0.24f * (float) Math.sin(ang));
float hr = w * 0.45f;
highlightPaint.setShader(new RadialGradient(
hx, hy, hr,
new int[]{0xCCFFFFFF, 0x33FFFFFF, 0x00FFFFFF},
new float[]{0f, 0.4f, 1f},
Shader.TileMode.CLAMP));
canvas.drawPath(heart, highlightPaint);
// A small fixed top-left specular dot for a glossy sticker look.
highlightPaint.setShader(new RadialGradient(
b.left + w * 0.34f, b.top + h * 0.28f, w * 0.18f,
new int[]{0xE6FFFFFF, 0x00FFFFFF},
null, Shader.TileMode.CLAMP));
canvas.drawPath(heart, highlightPaint);
canvas.restoreToCount(save);
invalidateSelf();
}
@ -119,12 +161,12 @@ public class ShimmerHeartDrawable extends Drawable {
@Override
public void setAlpha(int alpha) {
paint.setAlpha(alpha);
basePaint.setAlpha(alpha);
}
@Override
public void setColorFilter(ColorFilter colorFilter) {
paint.setColorFilter(colorFilter);
basePaint.setColorFilter(colorFilter);
}
@Override