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:
parent
7eeae7fe82
commit
9b81a463a6
1 changed files with 86 additions and 44 deletions
|
|
@ -3,10 +3,10 @@ package tw.nekomimi.nekogram.helpers;
|
||||||
import android.graphics.Canvas;
|
import android.graphics.Canvas;
|
||||||
import android.graphics.ColorFilter;
|
import android.graphics.ColorFilter;
|
||||||
import android.graphics.LinearGradient;
|
import android.graphics.LinearGradient;
|
||||||
import android.graphics.Matrix;
|
|
||||||
import android.graphics.Paint;
|
import android.graphics.Paint;
|
||||||
import android.graphics.Path;
|
import android.graphics.Path;
|
||||||
import android.graphics.PixelFormat;
|
import android.graphics.PixelFormat;
|
||||||
|
import android.graphics.RadialGradient;
|
||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
||||||
import android.graphics.Shader;
|
import android.graphics.Shader;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
|
|
@ -16,36 +16,27 @@ import androidx.annotation.NonNull;
|
||||||
import org.telegram.messenger.AndroidUtilities;
|
import org.telegram.messenger.AndroidUtilities;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A heart-shaped badge filled with a continuously moving iridescent gradient
|
* A glossy 3D-looking heart badge with a soft multicolor gradient (purple/blue
|
||||||
* ("shimmer"). It self-invalidates each frame, so when attached to a view via
|
* to orange) and a moving glossy highlight ("blik") sweeping across it, like an
|
||||||
* {@code setRightDrawable(...)} the host keeps repainting automatically (the
|
* iridescent emoji sticker.
|
||||||
* SimpleTextView drawable callback chain handles invalidation).
|
*
|
||||||
|
* 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 {
|
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 Path heart = new Path();
|
||||||
private final Matrix gradientMatrix = new Matrix();
|
|
||||||
|
|
||||||
private LinearGradient gradient;
|
|
||||||
private int lastWidth = -1;
|
private int lastWidth = -1;
|
||||||
private int gradientWidth;
|
private int lastHeight = -1;
|
||||||
|
|
||||||
// Full rainbow spectrum swept across the heart (seamless loop).
|
private static final long CYCLE_MS = 3200L;
|
||||||
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 final int size;
|
private final int size;
|
||||||
|
|
||||||
public ShimmerHeartDrawable() {
|
public ShimmerHeartDrawable() {
|
||||||
|
|
@ -54,16 +45,10 @@ public class ShimmerHeartDrawable extends Drawable {
|
||||||
|
|
||||||
public ShimmerHeartDrawable(int sizePx) {
|
public ShimmerHeartDrawable(int sizePx) {
|
||||||
this.size = sizePx;
|
this.size = sizePx;
|
||||||
paint.setStyle(Paint.Style.FILL);
|
basePaint.setStyle(Paint.Style.FILL);
|
||||||
}
|
tintPaint.setStyle(Paint.Style.FILL);
|
||||||
|
highlightPaint.setStyle(Paint.Style.FILL);
|
||||||
private void buildGradient(int width) {
|
shadePaint.setStyle(Paint.Style.FILL);
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void buildHeart(Rect b) {
|
private void buildHeart(Rect b) {
|
||||||
|
|
@ -72,7 +57,6 @@ public class ShimmerHeartDrawable extends Drawable {
|
||||||
float h = b.height();
|
float h = b.height();
|
||||||
float l = b.left;
|
float l = b.left;
|
||||||
float t = b.top;
|
float t = b.top;
|
||||||
// Heart path normalized to the bounds.
|
|
||||||
float cx = l + w / 2f;
|
float cx = l + w / 2f;
|
||||||
heart.moveTo(cx, t + h * 0.30f);
|
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.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();
|
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
|
@Override
|
||||||
protected void onBoundsChange(@NonNull Rect bounds) {
|
protected void onBoundsChange(@NonNull Rect bounds) {
|
||||||
super.onBoundsChange(bounds);
|
super.onBoundsChange(bounds);
|
||||||
buildHeart(bounds);
|
buildHeart(bounds);
|
||||||
buildGradient(bounds.width());
|
buildShaders(bounds);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -95,15 +108,44 @@ public class ShimmerHeartDrawable extends Drawable {
|
||||||
if (b.width() == 0 || b.height() == 0) {
|
if (b.width() == 0 || b.height() == 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (gradient == null || lastWidth != b.width()) {
|
if (lastWidth != b.width() || lastHeight != b.height()) {
|
||||||
buildGradient(b.width());
|
|
||||||
buildHeart(b);
|
buildHeart(b);
|
||||||
|
buildShaders(b);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
float w = b.width();
|
||||||
|
float h = b.height();
|
||||||
float phase = (System.currentTimeMillis() % CYCLE_MS) / (float) CYCLE_MS;
|
float phase = (System.currentTimeMillis() % CYCLE_MS) / (float) CYCLE_MS;
|
||||||
gradientMatrix.reset();
|
double ang = phase * 2 * Math.PI;
|
||||||
gradientMatrix.setTranslate(b.left - phase * gradientWidth, 0);
|
|
||||||
gradient.setLocalMatrix(gradientMatrix);
|
int save = canvas.save();
|
||||||
canvas.drawPath(heart, paint);
|
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();
|
invalidateSelf();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -119,12 +161,12 @@ public class ShimmerHeartDrawable extends Drawable {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setAlpha(int alpha) {
|
public void setAlpha(int alpha) {
|
||||||
paint.setAlpha(alpha);
|
basePaint.setAlpha(alpha);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setColorFilter(ColorFilter colorFilter) {
|
public void setColorFilter(ColorFilter colorFilter) {
|
||||||
paint.setColorFilter(colorFilter);
|
basePaint.setColorFilter(colorFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue