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:
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user