Add new drawable resources for icons and themes

- Created `archive_filled.xml` for filled archive icon.
- Added `bookmark_outlined.xml` for outlined bookmark icon.
- Introduced `day_theme_filled.xml` for day theme icon.
- Added `folder_outlined.xml` for outlined folder icon.
- Created `gear_outlined.xml` for outlined gear icon.
- Introduced `night_mode.xml` for night mode icon.
This commit is contained in:
2026-02-13 17:37:03 +05:00
parent e17b03c1c5
commit 93ce53d3d5
30 changed files with 1269 additions and 147 deletions

View File

@@ -13,7 +13,8 @@ import kotlin.math.min
*
* Used by ProfileMetaballOverlayCpu for devices without RenderEffect (API < 31).
*/
internal fun stackBlurBitmap(source: Bitmap, radius: Int): Bitmap {
@JvmOverloads
fun stackBlurBitmap(source: Bitmap, radius: Int): Bitmap {
if (radius < 1) return source
val bitmap = source.copy(source.config, true)
stackBlurBitmapInPlace(bitmap, radius)
@@ -23,7 +24,7 @@ internal fun stackBlurBitmap(source: Bitmap, radius: Int): Bitmap {
/**
* In-place stack blur on a mutable bitmap.
*/
internal fun stackBlurBitmapInPlace(bitmap: Bitmap, radius: Int) {
fun stackBlurBitmapInPlace(bitmap: Bitmap, radius: Int) {
if (radius < 1) return
val w = bitmap.width

View File

@@ -0,0 +1,136 @@
package com.rosetta.messenger.ui.components.metaball
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.drawable.BitmapDrawable
import android.view.Gravity
import android.view.View
import android.widget.FrameLayout
import android.widget.ImageView
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color as ComposeColor
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
/**
* Compose wrapper around Java [ProfileGooeyView] — the 1:1 Telegram port.
*
* Uses AndroidView to embed the View-based gooey effect inside Compose layout.
* The avatar content is rendered via a nested ComposeView inside ProfileGooeyView.
*
* @param collapseProgress 0 = fully expanded, 1 = fully collapsed
* @param expansionProgress 0 = normal, 1 = pulled down (overscroll)
* @param statusBarHeight status bar height in dp
* @param headerHeight current header height in dp
* @param hasAvatar whether user has an avatar image
* @param avatarColor fallback background color for avatar
* @param modifier modifier for the container
* @param avatarContent composable content to draw inside the gooey avatar
*/
@Composable
fun ProfileGooeyEffect(
collapseProgress: Float,
expansionProgress: Float,
statusBarHeight: Dp,
headerHeight: Dp,
hasAvatar: Boolean,
avatarColor: ComposeColor,
modifier: Modifier = Modifier,
avatarContent: @Composable BoxScope.() -> Unit = {},
) {
val density = LocalDensity.current
// Convert Dp to px for the View layer
val statusBarPx = with(density) { statusBarHeight.toPx() }
val headerPx = with(density) { headerHeight.toPx() }
val avatarSizeDp = 100f // matches AVATAR_SIZE_DP in ProfileGooeyView
val avatarSizePx = with(density) { avatarSizeDp.dp.toPx() }.toInt()
// Compute avatar position/scale based on collapse progress
val expandedHeaderDp = 300f // should match EXPANDED_HEADER_HEIGHT from ProfileScreen
val collapsedHeaderDp = 56f // should match COLLAPSED_HEADER_HEIGHT
val contentAreaDp = expandedHeaderDp - 70f
val avatarExpandedYDp = statusBarHeight + ((contentAreaDp - avatarSizeDp).dp / 2)
val avatarCollapsedYDp = statusBarHeight + ((collapsedHeaderDp - 30f).dp / 2) // collapsed avatar ~30dp
val avatarYDp = androidx.compose.ui.unit.lerp(avatarExpandedYDp, avatarCollapsedYDp, collapseProgress)
val avatarYPx = with(density) { avatarYDp.toPx() }.toInt()
// Avatar scale: 1.0 at expanded, ~0.3 at collapsed
val avatarScale = androidx.compose.ui.unit.lerp(1f.dp, 0.3f.dp, collapseProgress)
val avatarScaleFloat = avatarScale.value
AndroidView(
modifier = modifier,
factory = { context ->
ProfileGooeyView(context).apply {
// Add a child ComposeView that renders the avatar content
val composeChild = ComposeView(context).apply {
layoutParams = FrameLayout.LayoutParams(
avatarSizePx,
avatarSizePx,
Gravity.CENTER_HORIZONTAL or Gravity.TOP
).apply {
topMargin = avatarYPx
}
}
addView(composeChild)
setGooeyEnabled(true)
setIntensity(15f)
}
},
update = { gooeyView ->
// Update gooey parameters each recomposition
val blurIntensity = collapseProgress.coerceIn(0f, 1f)
gooeyView.setBlurIntensity(blurIntensity)
gooeyView.setPullProgress(expansionProgress)
// Update child position and scale
val child = gooeyView.getChildAt(0)
if (child != null) {
val lp = child.layoutParams as FrameLayout.LayoutParams
lp.topMargin = avatarYPx
lp.width = avatarSizePx
lp.height = avatarSizePx
child.layoutParams = lp
child.scaleX = avatarScaleFloat
child.scaleY = avatarScaleFloat
child.pivotX = avatarSizePx / 2f
child.pivotY = avatarSizePx / 2f
// Update ComposeView content
if (child is ComposeView) {
child.setContent {
Box(
modifier = Modifier
.fillMaxSize()
.background(avatarColor),
contentAlignment = Alignment.Center,
content = avatarContent
)
}
}
}
gooeyView.invalidate()
}
)
}

View File

@@ -0,0 +1,544 @@
package com.rosetta.messenger.ui.components.metaball;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BlendMode;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorMatrixColorFilter;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.RectF;
import android.graphics.RenderEffect;
import android.graphics.RenderNode;
import android.graphics.Shader;
import android.os.Build;
import android.view.Gravity;
import android.view.View;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.core.math.MathUtils;
/**
* Port of Telegram's ProfileGooeyView.java — gooey/metaball effect for profile avatars.
* Adapted to work with rosetta-android's Kotlin utility classes
* (NotchInfoUtils, DevicePerformanceClass, CpuBlurUtils) instead of Telegram internals.
*/
public class ProfileGooeyView extends FrameLayout {
private static final float AVATAR_SIZE_DP = 100;
private static final float BLACK_KING_BAR = 32;
private final Paint blackPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
private final Path path = new Path();
private final Impl impl;
private float intensity;
private float pullProgress;
private float blurIntensity;
private boolean enabled;
@Nullable
public NotchInfoUtils.NotchInfo notchInfo;
// ───── Utility helpers (replacing Telegram's AndroidUtilities) ─────
private static float sDensity = -1f;
private static float getDensity() {
if (sDensity < 0) {
sDensity = Resources.getSystem().getDisplayMetrics().density;
}
return sDensity;
}
/** dp → px, matching Telegram's AndroidUtilities.dp */
private static int dp(float value) {
if (value == 0) return 0;
return (int) Math.ceil(getDensity() * value);
}
/** Linear interpolation */
private static float lerp(float a, float b, float f) {
return a + f * (b - a);
}
/** Clamped range lerp: interpolates a→b as f goes from c1→c2 */
private static float lerp(float a, float b, float c1, float c2, float f) {
return lerp(a, b, MathUtils.clamp((f - c1) / (c2 - c1), 0f, 1f));
}
/** Inverse lerp: returns where x falls between a and b (0..1 unclamped) */
private static float ilerp(float x, float a, float b) {
return (x - a) / (b - a);
}
/** Get status bar height in pixels */
private static int getStatusBarHeight(Context context) {
int result = 0;
int resourceId = context.getResources().getIdentifier("status_bar_height", "dimen", "android");
if (resourceId > 0) {
result = context.getResources().getDimensionPixelSize(resourceId);
}
return result;
}
/** In-place stack blur — delegates to CpuBlurUtils.kt */
private static void stackBlurBitmap(Bitmap bitmap, int radius) {
CpuBlurUtilsKt.stackBlurBitmapInPlace(bitmap, radius);
}
// ───── Constructor ─────
public ProfileGooeyView(Context context) {
super(context);
blackPaint.setColor(Color.BLACK);
PerformanceClass perf = DevicePerformanceClass.INSTANCE.get(context);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && perf.ordinal() >= PerformanceClass.AVERAGE.ordinal()) {
impl = new GPUImpl(perf == PerformanceClass.HIGH ? 1f : 1.5f);
} else {
impl = new CPUImpl();
}
setIntensity(15f);
setBlurIntensity(0f);
setWillNotDraw(false);
}
// ───── Public API ─────
public float getEndOffset(boolean occupyStatusBar, float avatarScale) {
if (notchInfo != null) {
return -(dp(16) + (notchInfo.isLikelyCircle()
? notchInfo.getBounds().width() + notchInfo.getBounds().width() * getAvatarEndScale()
: notchInfo.getBounds().height() - notchInfo.getBounds().top));
}
return -((occupyStatusBar ? getStatusBarHeight(getContext()) : 0) + dp(16) + dp(AVATAR_SIZE_DP));
}
public float getAvatarEndScale() {
if (notchInfo != null) {
float f;
if (notchInfo.isLikelyCircle()) {
f = (notchInfo.getBounds().width() - dp(2)) / dp(AVATAR_SIZE_DP);
} else {
f = Math.min(notchInfo.getBounds().width(), notchInfo.getBounds().height()) / dp(AVATAR_SIZE_DP);
}
return Math.min(0.8f, f);
}
return 0.8f;
}
public boolean hasNotchInfo() {
return notchInfo != null;
}
public void setIntensity(float intensity) {
this.intensity = intensity;
impl.setIntensity(intensity);
invalidate();
}
public void setPullProgress(float pullProgress) {
this.pullProgress = pullProgress;
invalidate();
}
public void setBlurIntensity(float intensity) {
this.blurIntensity = intensity;
impl.setBlurIntensity(intensity);
invalidate();
}
public void setGooeyEnabled(boolean enabled) {
if (this.enabled == enabled) {
return;
}
this.enabled = enabled;
invalidate();
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
notchInfo = NotchInfoUtils.INSTANCE.getInfo(getContext());
if (notchInfo != null && (notchInfo.getGravity() != Gravity.CENTER) || getWidth() > getHeight()) {
notchInfo = null;
}
impl.onSizeChanged(w, h);
}
@Override
public void draw(@NonNull Canvas canvas) {
if (!enabled) {
super.draw(canvas);
return;
}
impl.draw(c -> {
c.save();
c.translate(0, dp(BLACK_KING_BAR));
super.draw(c);
c.restore();
}, canvas);
}
// ───── CPU implementation (all Android versions) ─────
private final class CPUImpl implements Impl {
private Bitmap bitmap;
private Canvas bitmapCanvas;
private final Paint bitmapPaint = new Paint();
private final Paint bitmapPaint2 = new Paint();
private int optimizedH;
private int optimizedW;
private int bitmapOrigW, bitmapOrigH;
private final float scaleConst = 6f;
{
bitmapPaint.setFlags(Paint.FILTER_BITMAP_FLAG | Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
bitmapPaint.setFilterBitmap(true);
bitmapPaint2.setFlags(Paint.FILTER_BITMAP_FLAG | Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
bitmapPaint2.setFilterBitmap(true);
bitmapPaint2.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_ATOP));
bitmapPaint.setColorFilter(new ColorMatrixColorFilter(new float[]{
0f, 0f, 0f, 0f, 0f,
0f, 0f, 0f, 0f, 0f,
0f, 0f, 0f, 0f, 0f,
0f, 0f, 0f, 60, -7500
}));
}
@Override
public void onSizeChanged(int w, int h) {
if (bitmap != null) {
bitmap.recycle();
}
optimizedW = Math.min(dp(120), w);
optimizedH = Math.min(dp(220), h);
bitmapOrigW = optimizedW;
bitmapOrigH = optimizedH + dp(BLACK_KING_BAR);
bitmap = Bitmap.createBitmap((int) (bitmapOrigW / scaleConst), (int) (bitmapOrigH / scaleConst), Bitmap.Config.ARGB_8888);
bitmapCanvas = new Canvas(bitmap);
}
@Override
public void draw(Drawer drawer, Canvas canvas) {
if (bitmap == null) return;
final float v = (MathUtils.clamp(blurIntensity, 0.2f, 0.3f) - 0.2f) / (0.3f - 0.2f);
final int alpha = (int) ((1f - v) * 0xFF);
final float optimizedOffsetX = (getWidth() - optimizedW) / 2f;
// Offset everything for black bar
canvas.save();
canvas.translate(0, -dp(BLACK_KING_BAR));
if (alpha != 255) {
bitmap.eraseColor(0);
bitmapCanvas.save();
bitmapCanvas.scale((float) bitmap.getWidth() / bitmapOrigW, (float) bitmap.getHeight() / bitmapOrigH);
bitmapCanvas.translate(-optimizedOffsetX, 0);
drawer.draw(bitmapCanvas);
bitmapCanvas.restore();
bitmapCanvas.save();
bitmapCanvas.scale((float) bitmap.getWidth() / bitmapOrigW, (float) bitmap.getHeight() / bitmapOrigH);
if (notchInfo != null) {
bitmapCanvas.save();
bitmapCanvas.translate(-optimizedOffsetX, dp(BLACK_KING_BAR));
if (notchInfo.isLikelyCircle()) {
float rad = Math.min(notchInfo.getBounds().width(), notchInfo.getBounds().height()) / 2f;
bitmapCanvas.drawCircle(notchInfo.getBounds().centerX(), notchInfo.getBounds().bottom - notchInfo.getBounds().width() / 2f, rad, blackPaint);
} else if (notchInfo.isAccurate()) {
bitmapCanvas.drawPath(notchInfo.getPath(), blackPaint);
} else {
float rad = Math.max(notchInfo.getBounds().width(), notchInfo.getBounds().height()) / 2f;
bitmapCanvas.drawRoundRect(notchInfo.getBounds(), rad, rad, blackPaint);
}
bitmapCanvas.restore();
} else {
bitmapCanvas.drawRect(0, 0, optimizedW, dp(BLACK_KING_BAR), blackPaint);
}
bitmapCanvas.restore();
// Blur buffer
stackBlurBitmap(bitmap, (int) (intensity * 2 / scaleConst));
// Filter alpha + fade, then draw
canvas.save();
canvas.translate(optimizedOffsetX, 0);
canvas.saveLayer(0, 0, bitmapOrigW, bitmapOrigH, null);
canvas.scale((float) bitmapOrigW / bitmap.getWidth(), (float) bitmapOrigH / bitmap.getHeight());
canvas.drawBitmap(bitmap, 0, 0, bitmapPaint);
canvas.drawBitmap(bitmap, 0, 0, bitmapPaint2);
canvas.restore();
canvas.restore();
}
// Fade, draw blurred
if (alpha != 0) {
if (alpha != 255) {
canvas.saveLayerAlpha(optimizedOffsetX, 0, optimizedOffsetX + optimizedW, optimizedH, alpha);
}
drawer.draw(canvas);
if (alpha != 255) {
canvas.restore();
}
}
canvas.restore();
}
}
// ───── GPU implementation (Android 12+ / API 31+) ─────
@RequiresApi(api = Build.VERSION_CODES.S)
private final class GPUImpl implements Impl {
private final Paint filter = new Paint(Paint.ANTI_ALIAS_FLAG);
private final RenderNode node = new RenderNode("render");
private final RenderNode effectNotchNode = new RenderNode("effectNotch");
private final RenderNode effectNode = new RenderNode("effect");
private final RenderNode blurNode = new RenderNode("blur");
private final float factorMult;
private final RectF whole = new RectF();
private final RectF temp = new RectF();
private final Paint blackNodePaint = new Paint();
private GPUImpl(float factorMult) {
this.factorMult = factorMult;
blackNodePaint.setColor(Color.BLACK);
blackNodePaint.setBlendMode(BlendMode.SRC_IN);
}
@Override
public void setIntensity(float intensity) {
effectNode.setRenderEffect(RenderEffect.createBlurEffect(intensity, intensity, Shader.TileMode.CLAMP));
effectNotchNode.setRenderEffect(RenderEffect.createBlurEffect(intensity, intensity, Shader.TileMode.CLAMP));
filter.setColorFilter(new ColorMatrixColorFilter(new float[]{
1f, 0f, 0f, 0f, 0f,
0f, 1f, 0f, 0f, 0f,
0f, 0f, 1f, 0f, 0f,
0f, 0f, 0f, 51, 51 * -125
}));
}
@Override
public void setBlurIntensity(float blurIntensity) {
if (blurIntensity == 0) {
blurNode.setRenderEffect(null);
return;
}
blurNode.setRenderEffect(RenderEffect.createBlurEffect(blurIntensity * intensity / factorMult, blurIntensity * intensity / factorMult, Shader.TileMode.DECAL));
}
private final RectF wholeOptimized = new RectF();
@Override
public void draw(Drawer drawer, @NonNull Canvas canvas) {
if (!canvas.isHardwareAccelerated()) {
return;
}
Canvas c;
whole.set(0, 0, getWidth(), getHeight());
if (getChildCount() > 0) {
final View child = getChildAt(0);
final float w = child.getWidth() * child.getScaleX();
final float h = child.getHeight() * child.getScaleY();
final float l = child.getX();
final float t = child.getY();
wholeOptimized.set(l, t, l + w, t + h);
if (notchInfo != null) {
wholeOptimized.union(notchInfo.getBounds());
}
wholeOptimized.inset(-dp(20), -dp(20));
wholeOptimized.intersect(whole);
wholeOptimized.top = 0;
} else {
wholeOptimized.set(whole);
}
wholeOptimized.bottom += dp(BLACK_KING_BAR);
final int width = (int) Math.ceil(wholeOptimized.width());
final int height = (int) Math.ceil(wholeOptimized.height());
final float left = wholeOptimized.left;
final float top = wholeOptimized.top;
node.setPosition(0, 0, width, height);
blurNode.setPosition(0, 0, width, height);
effectNode.setPosition(0, 0, width, height);
effectNotchNode.setPosition(0, 0, width, height);
wholeOptimized.set(0, 0, width, height);
// Record everything into buffer
c = node.beginRecording();
c.translate(-left, -top);
final int imageAlphaNoClamp = (int) ((1f - ilerp(pullProgress, 0.5f, 1.0f)) * 255);
final int imageAlpha = MathUtils.clamp(imageAlphaNoClamp, 0, 255);
drawer.draw(c);
node.endRecording();
// Blur only buffer
float blurScaleFactor = factorMult / 4f + 1f + blurIntensity * 0.5f * factorMult + (factorMult - 1f) * 2f;
c = blurNode.beginRecording();
c.scale(1f / blurScaleFactor, 1f / blurScaleFactor, 0, 0);
c.drawRenderNode(node);
blurNode.endRecording();
// Blur + filter buffer
float gooScaleFactor = 2f + factorMult;
c = effectNode.beginRecording();
c.scale(1f / gooScaleFactor, 1f / gooScaleFactor, 0, 0);
if (imageAlpha < 255) {
c.saveLayer(wholeOptimized, null);
c.drawRenderNode(node);
c.drawRect(wholeOptimized, blackNodePaint);
c.restore();
}
final float h = lerp(0, dp(7) * gooScaleFactor, 0, 0.5f, pullProgress);
if (getChildCount() > 0) {
final View child = getChildAt(0);
final float cx = child.getX() + child.getWidth() * child.getScaleX() / 2.0f - left;
final float cy = child.getY() + child.getHeight() * child.getScaleY() / 2.0f + dp(BLACK_KING_BAR) - top;
final float r = child.getWidth() / 2.0f * child.getScaleX();
path.rewind();
path.moveTo(cx - r, cy - (float) Math.cos(Math.PI / 4) * r);
path.lineTo(cx, cy - r - h * 0.25f);
path.lineTo(cx + r, cy - (float) Math.cos(Math.PI / 4) * r);
path.close();
c.drawPath(path, blackPaint);
}
if (imageAlpha > 0) {
if (imageAlpha != 255) {
c.saveLayerAlpha(wholeOptimized, imageAlpha);
}
c.drawRenderNode(node);
if (imageAlpha != 255) {
c.restore();
}
}
effectNode.endRecording();
c = effectNotchNode.beginRecording();
c.scale(1f / gooScaleFactor, 1f / gooScaleFactor, 0, 0);
if (notchInfo != null) {
c.translate(-left, -top);
c.translate(0, dp(BLACK_KING_BAR));
if (notchInfo.isLikelyCircle()) {
float rad = Math.min(notchInfo.getBounds().width(), notchInfo.getBounds().height()) / 2f;
final float cy = notchInfo.getBounds().bottom - notchInfo.getBounds().width() / 2f;
c.drawCircle(notchInfo.getBounds().centerX(), cy, rad, blackPaint);
path.rewind();
path.moveTo(notchInfo.getBounds().centerX() - h / 2f, cy);
path.lineTo(notchInfo.getBounds().centerX(), cy + rad + h);
path.lineTo(notchInfo.getBounds().centerX() + h / 2f, cy);
path.close();
c.drawPath(path, blackPaint);
} else if (notchInfo.isAccurate()) {
c.drawPath(notchInfo.getPath(), blackPaint);
} else {
float rad = Math.max(notchInfo.getBounds().width(), notchInfo.getBounds().height()) / 2f;
temp.set(notchInfo.getBounds());
c.drawRoundRect(temp, rad, rad, blackPaint);
path.rewind();
path.moveTo(temp.centerX() - h / 2f, temp.bottom);
path.lineTo(temp.centerX(), temp.bottom + h);
path.lineTo(temp.centerX() + h / 2f, temp.bottom);
path.close();
c.drawPath(path, blackPaint);
}
} else {
c.drawRect(0, 0, width, dp(BLACK_KING_BAR), blackPaint);
path.rewind();
path.moveTo((width - h) / 2f, dp(BLACK_KING_BAR));
path.lineTo((width) / 2f, dp(BLACK_KING_BAR) + h);
path.lineTo((width + h) / 2f, dp(BLACK_KING_BAR));
path.close();
c.drawPath(path, blackPaint);
}
effectNotchNode.endRecording();
// Offset everything for black bar
canvas.save();
canvas.translate(left, top - dp(BLACK_KING_BAR));
if (notchInfo != null) {
canvas.clipRect(0, notchInfo.getBounds().top, width, height);
}
// Filter alpha + fade, then draw
canvas.saveLayer(wholeOptimized, filter);
canvas.scale(gooScaleFactor, gooScaleFactor);
canvas.drawRenderNode(effectNotchNode);
canvas.drawRenderNode(effectNode);
canvas.restore();
// Fade, draw blurred
final int blurImageAlpha = MathUtils.clamp(imageAlphaNoClamp * 3 / 4, 0, 255);
if (blurImageAlpha < 255) {
canvas.saveLayer(wholeOptimized, null);
if (blurIntensity != 0) {
canvas.saveLayer(wholeOptimized, filter);
canvas.scale(blurScaleFactor, blurScaleFactor);
canvas.drawRenderNode(blurNode);
canvas.restore();
} else {
canvas.drawRenderNode(node);
}
canvas.drawRect(wholeOptimized, blackNodePaint);
canvas.restore();
}
if (blurImageAlpha > 0) {
if (blurImageAlpha != 255) {
canvas.saveLayerAlpha(wholeOptimized, blurImageAlpha);
}
if (blurIntensity != 0) {
canvas.saveLayer(wholeOptimized, filter);
canvas.scale(blurScaleFactor, blurScaleFactor);
canvas.drawRenderNode(blurNode);
canvas.restore();
} else {
canvas.drawRenderNode(node);
}
if (blurImageAlpha != 255) {
canvas.restore();
}
}
canvas.restore();
}
}
// ───── Impl interface ─────
private interface Impl {
default void setIntensity(float intensity) {}
default void setBlurIntensity(float intensity) {}
default void onSizeChanged(int w, int h) {}
void draw(Drawer drawer, Canvas canvas);
}
private interface Drawer {
void draw(Canvas canvas);
}
}