diff --git a/AndroidLiquidGlassView/src/main/java/com/qmdeve/liquidglass/Config.java b/AndroidLiquidGlassView/src/main/java/com/qmdeve/liquidglass/Config.java index e5e37d9..6793704 100644 --- a/AndroidLiquidGlassView/src/main/java/com/qmdeve/liquidglass/Config.java +++ b/AndroidLiquidGlassView/src/main/java/com/qmdeve/liquidglass/Config.java @@ -34,13 +34,14 @@ public class Config { public volatile float CHROMA_MULTIPLIER; public volatile float BLUR_RADIUS; public float TINT_ALPHA, TINT_COLOR_RED, TINT_COLOR_GREEN, TINT_COLOR_BLUE; + public float BORDER_WIDTH = 0.0f; public void configure(@Nullable Overrides overrides) { if (overrides != null) overrides.apply(this); } public static final class Overrides { - Float cornerRadius, refractionHeight, refractionOffset, contrast, whitePoint, chromaMultiplier, blurRadius, tintAlpha, tintColorRed, tintColorGreen, tintColorBlue, dispersion; + Float cornerRadius, refractionHeight, refractionOffset, contrast, whitePoint, chromaMultiplier, blurRadius, tintAlpha, tintColorRed, tintColorGreen, tintColorBlue, dispersion, borderWidth; Integer width, height; public Overrides tintAlpha(float v) { @@ -114,6 +115,11 @@ public Overrides dispersion(float v) { return this; } + public Overrides borderWidth(float v) { + borderWidth = v; + return this; + } + public Overrides size(int w, int h) { width = w; height = h; @@ -135,6 +141,7 @@ void apply(Config c) { if (tintColorGreen != null) c.TINT_COLOR_GREEN = tintColorGreen; if (tintColorBlue != null) c.TINT_COLOR_BLUE = tintColorBlue; if (dispersion != null) c.DISPERSION = dispersion; + if (borderWidth != null) c.BORDER_WIDTH = borderWidth; } } } \ No newline at end of file diff --git a/AndroidLiquidGlassView/src/main/java/com/qmdeve/liquidglass/LiquidGlass.java b/AndroidLiquidGlassView/src/main/java/com/qmdeve/liquidglass/LiquidGlass.java index 29f9580..5f4ca00 100644 --- a/AndroidLiquidGlassView/src/main/java/com/qmdeve/liquidglass/LiquidGlass.java +++ b/AndroidLiquidGlassView/src/main/java/com/qmdeve/liquidglass/LiquidGlass.java @@ -34,6 +34,7 @@ import androidx.annotation.NonNull; import com.qmdeve.liquidglass.impl.Impl; +import com.qmdeve.liquidglass.impl.LiquidGlassLegacyImpl; import com.qmdeve.liquidglass.impl.LiquidGlassimpl; import java.lang.ref.WeakReference; @@ -44,9 +45,12 @@ public class LiquidGlass extends FrameLayout { private ViewGroup target; private boolean listenerAdded = false; private final Config config; + private boolean isPaused = false; private static class PreDrawListener implements ViewTreeObserver.OnPreDrawListener { private final WeakReference liquidGlassRef; + private int frameSkipCounter = 0; + private static final int FRAME_SKIP_INTERVAL = 1; public PreDrawListener(LiquidGlass liquidGlass) { this.liquidGlassRef = new WeakReference<>(liquidGlass); @@ -55,7 +59,15 @@ public PreDrawListener(LiquidGlass liquidGlass) { @Override public boolean onPreDraw() { LiquidGlass liquidGlass = liquidGlassRef.get(); - if (liquidGlass != null && liquidGlass.impl != null) { + if (liquidGlass == null || liquidGlass.impl == null) { + return true; + } + if (liquidGlass.getVisibility() != View.VISIBLE || liquidGlass.getAlpha() < 0.01f) { + return true; + } + frameSkipCounter++; + if (frameSkipCounter >= FRAME_SKIP_INTERVAL) { + frameSkipCounter = 0; liquidGlass.impl.onPreDraw(); } return true; @@ -94,6 +106,11 @@ public void init(ViewGroup target) { addPreDrawListener(); requestLayout(); invalidate(); + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + impl = new LiquidGlassLegacyImpl(this, target, config); + addPreDrawListener(); + requestLayout(); + invalidate(); } else { removePreDrawListener(); } @@ -102,12 +119,16 @@ public void init(ViewGroup target) { private void init() { setWillNotDraw(false); setLayerType(LAYER_TYPE_HARDWARE, null); + setClipToPadding(false); + setClipChildren(false); updateOutlineProvider(); } @Override protected void onDraw(@NonNull Canvas canvas) { - if (impl != null) impl.draw(canvas); + if (impl != null && !isPaused && getVisibility() == View.VISIBLE) { + impl.draw(canvas); + } } public void updateParameters() { @@ -138,16 +159,24 @@ protected void onSizeChanged(int w, int h, int oldw, int oldh) { @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); + isPaused = false; addPreDrawListener(); } @Override protected void onDetachedFromWindow() { + isPaused = true; removePreDrawListener(); if (impl != null) impl.dispose(); super.onDetachedFromWindow(); } + @Override + protected void onVisibilityChanged(@NonNull View changedView, int visibility) { + super.onVisibilityChanged(changedView, visibility); + isPaused = (visibility != View.VISIBLE); + } + private void addPreDrawListener() { if (target != null && !listenerAdded) { target.getViewTreeObserver().addOnPreDrawListener(preDrawListener); diff --git a/AndroidLiquidGlassView/src/main/java/com/qmdeve/liquidglass/impl/LiquidGlassLegacyImpl.java b/AndroidLiquidGlassView/src/main/java/com/qmdeve/liquidglass/impl/LiquidGlassLegacyImpl.java new file mode 100644 index 0000000..a75014b --- /dev/null +++ b/AndroidLiquidGlassView/src/main/java/com/qmdeve/liquidglass/impl/LiquidGlassLegacyImpl.java @@ -0,0 +1,306 @@ +package com.qmdeve.liquidglass.impl; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; +import android.graphics.RenderEffect; +import android.graphics.RenderNode; +import android.graphics.Shader; +import android.os.Build; +import android.view.View; + +import androidx.annotation.NonNull; + +import com.qmdeve.liquidglass.Config; + +public final class LiquidGlassLegacyImpl implements Impl { + private final View host, target; + private final Config config; + private final Paint blurPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + private final Paint tintPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + private final int[] targetPos = new int[2]; + private final int[] hostPos = new int[2]; + private Bitmap buffer; + private Bitmap blurredBuffer; + private RenderNode renderNode; + private long lastCaptureTime = 0; + private static final long MIN_CAPTURE_INTERVAL_MS = 16; + + public LiquidGlassLegacyImpl(@NonNull View host, @NonNull View target, @NonNull Config config) { + this.host = host; + this.target = target; + this.config = config; + tintPaint.setStyle(Paint.Style.FILL); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + renderNode = new RenderNode("AndroidLiquidGlassViewLegacy"); + } + } + + @Override + public void onSizeChanged(int w, int h) { + recreateBuffers(w, h); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && renderNode != null) { + renderNode.setPosition(0, 0, w, h); + } + } + + @Override + public void onPreDraw() { + long now = System.currentTimeMillis(); + if (now - lastCaptureTime >= MIN_CAPTURE_INTERVAL_MS) { + captureTarget(); + lastCaptureTime = now; + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { + applyCpuBlur(); + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + updateRenderNode(); + } + } + } + + @Override + public void draw(Canvas canvas) { + if (buffer == null) return; + float borderWidth = config.BORDER_WIDTH; + if (borderWidth > 0.0f) { + drawWithBorderMask(canvas); + } else { + drawFull(canvas); + } + } + + private void drawFull(Canvas canvas) { + if (!canvas.isHardwareAccelerated() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + canvas.drawBitmap(blurredBuffer != null ? blurredBuffer : buffer, 0, 0, null); + return; + } + float tintAlpha = config.TINT_ALPHA; + float tintRed = config.TINT_COLOR_RED; + float tintGreen = config.TINT_COLOR_GREEN; + float tintBlue = config.TINT_COLOR_BLUE; + tintPaint.setColor(Color.argb(clampColor(tintAlpha), clampColor(tintRed), clampColor(tintGreen), clampColor(tintBlue))); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && renderNode != null) { + canvas.drawRenderNode(renderNode); + } else { + canvas.drawBitmap(blurredBuffer != null ? blurredBuffer : buffer, 0, 0, null); + } + if (tintAlpha > 0.001f) canvas.drawRect(0, 0, buffer.getWidth(), buffer.getHeight(), tintPaint); + } + + private void drawWithBorderMask(Canvas canvas) { + int width = buffer.getWidth(); + int height = buffer.getHeight(); + if (width == 0 || height == 0) return; + float borderWidth = config.BORDER_WIDTH; + android.graphics.Paint maskPaint = new android.graphics.Paint(Paint.ANTI_ALIAS_FLAG); + maskPaint.setXfermode(new android.graphics.PorterDuffXfermode(android.graphics.PorterDuff.Mode.DST_OUT)); + int savedLayer = canvas.saveLayer(0, 0, width, height, null); + drawFull(canvas); + float cornerRadius = config.CORNER_RADIUS_PX; + android.graphics.Path path = new android.graphics.Path(); + float innerWidth = Math.max(0, width - borderWidth * 2); + float innerHeight = Math.max(0, height - borderWidth * 2); + float innerCornerRadius = Math.max(0, cornerRadius - borderWidth); + android.graphics.RectF innerRect = new android.graphics.RectF( + borderWidth, borderWidth, + borderWidth + innerWidth, borderWidth + innerHeight + ); + path.addRoundRect(innerRect, innerCornerRadius, innerCornerRadius, android.graphics.Path.Direction.CW); + canvas.drawPath(path, maskPaint); + canvas.restoreToCount(savedLayer); + } + + @Override + public void dispose() { + recycleBuffer(buffer); + recycleBuffer(blurredBuffer); + } + + private void recreateBuffers(int w, int h) { + recycleBuffer(buffer); + recycleBuffer(blurredBuffer); + if (w > 0 && h > 0) { + buffer = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888); + blurredBuffer = Build.VERSION.SDK_INT < Build.VERSION_CODES.S ? Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888) : null; + } + } + + private void captureTarget() { + if (buffer == null) return; + if (target.getVisibility() != View.VISIBLE || host.getVisibility() != View.VISIBLE) { + return; + } + int width = host.getWidth(); + int height = host.getHeight(); + if (width == 0 || height == 0) return; + target.getLocationInWindow(targetPos); + host.getLocationInWindow(hostPos); + int dx = hostPos[0] - targetPos[0]; + int dy = hostPos[1] - targetPos[1]; + int hostVisibility = host.getVisibility(); + if (hostVisibility == View.VISIBLE) { + host.setVisibility(View.INVISIBLE); + } + try { + Canvas rec = new Canvas(buffer); + rec.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); + rec.save(); + rec.translate(-dx, -dy); + target.draw(rec); + rec.restore(); + } finally { + if (hostVisibility == View.VISIBLE) { + host.setVisibility(hostVisibility); + } + } + } + + private void applyCpuBlur() { + if (blurredBuffer == null || buffer == null) return; + copyBitmap(buffer, blurredBuffer); + int radius = Math.min(25, Math.max(0, Math.round(config.BLUR_RADIUS))); + if (radius == 0) return; + boxBlur(blurredBuffer, radius); + } + + private void updateRenderNode() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S || renderNode == null || buffer == null) return; + int w = buffer.getWidth(); + int h = buffer.getHeight(); + if (w == 0 || h == 0) return; + Canvas rec = renderNode.beginRecording(w, h); + if (rec != null) { + rec.drawBitmap(buffer, 0, 0, blurPaint); + } + renderNode.endRecording(); + float sigma = Math.max(0f, config.BLUR_RADIUS); + RenderEffect effect = sigma > 0.01f ? RenderEffect.createBlurEffect(sigma, sigma, Shader.TileMode.CLAMP) : null; + renderNode.setRenderEffect(effect); + } + + private void boxBlur(Bitmap bmp, int radius) { + if (radius <= 0) return; + int w = bmp.getWidth(); + int h = bmp.getHeight(); + int[] pixels = new int[w * h]; + bmp.getPixels(pixels, 0, w, 0, 0, w, h); + int[] temp = new int[w * h]; + boxBlurHorizontal(pixels, temp, w, h, radius); + boxBlurVertical(temp, pixels, w, h, radius); + bmp.setPixels(pixels, 0, w, 0, 0, w, h); + } + + private void boxBlurHorizontal(int[] src, int[] dst, int w, int h, int radius) { + for (int y = 0; y < h; y++) { + int idx = y * w; + int sumR = 0, sumG = 0, sumB = 0, sumA = 0; + int count = 0; + for (int i = -radius; i <= radius; i++) { + int px = clamp(i, 0, w - 1); + int pix = src[idx + px]; + sumA += (pix >>> 24) & 0xFF; + sumR += (pix >> 16) & 0xFF; + sumG += (pix >> 8) & 0xFF; + sumB += pix & 0xFF; + count++; + } + for (int x = 0; x < w; x++) { + dst[idx + x] = ((sumA / count) << 24) | ((sumR / count) << 16) | ((sumG / count) << 8) | (sumB / count); + int left = x - radius; + int right = x + radius + 1; + if (left >= 0) { + int pix = src[idx + left]; + sumA -= (pix >>> 24) & 0xFF; + sumR -= (pix >> 16) & 0xFF; + sumG -= (pix >> 8) & 0xFF; + sumB -= pix & 0xFF; + count--; + } + if (right < w) { + int pix = src[idx + right]; + sumA += (pix >>> 24) & 0xFF; + sumR += (pix >> 16) & 0xFF; + sumG += (pix >> 8) & 0xFF; + sumB += pix & 0xFF; + count++; + } else if (left < 0) { + int pix = src[idx + clamp(x, 0, w - 1)]; + sumA += (pix >>> 24) & 0xFF; + sumR += (pix >> 16) & 0xFF; + sumG += (pix >> 8) & 0xFF; + sumB += pix & 0xFF; + count++; + } + } + } + } + + private void boxBlurVertical(int[] src, int[] dst, int w, int h, int radius) { + for (int x = 0; x < w; x++) { + int sumR = 0, sumG = 0, sumB = 0, sumA = 0; + int count = 0; + for (int i = -radius; i <= radius; i++) { + int py = clamp(i, 0, h - 1); + int pix = src[py * w + x]; + sumA += (pix >>> 24) & 0xFF; + sumR += (pix >> 16) & 0xFF; + sumG += (pix >> 8) & 0xFF; + sumB += pix & 0xFF; + count++; + } + for (int y = 0; y < h; y++) { + dst[y * w + x] = ((sumA / count) << 24) | ((sumR / count) << 16) | ((sumG / count) << 8) | (sumB / count); + int top = y - radius; + int bottom = y + radius + 1; + if (top >= 0) { + int pix = src[top * w + x]; + sumA -= (pix >>> 24) & 0xFF; + sumR -= (pix >> 16) & 0xFF; + sumG -= (pix >> 8) & 0xFF; + sumB -= pix & 0xFF; + count--; + } + if (bottom < h) { + int pix = src[bottom * w + x]; + sumA += (pix >>> 24) & 0xFF; + sumR += (pix >> 16) & 0xFF; + sumG += (pix >> 8) & 0xFF; + sumB += pix & 0xFF; + count++; + } else if (top < 0) { + int pix = src[clamp(y, 0, h - 1) * w + x]; + sumA += (pix >>> 24) & 0xFF; + sumR += (pix >> 16) & 0xFF; + sumG += (pix >> 8) & 0xFF; + sumB += pix & 0xFF; + count++; + } + } + } + } + + private int clamp(int v, int min, int max) { + return Math.max(min, Math.min(max, v)); + } + + private int clampColor(float c) { + return Math.max(0, Math.min(255, Math.round(c * 255f))); + } + + private void recycleBuffer(Bitmap bmp) { + if (bmp != null && !bmp.isRecycled()) bmp.recycle(); + } + + private void copyBitmap(Bitmap src, Bitmap dst) { + if (src == null || dst == null) return; + Canvas c = new Canvas(dst); + c.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); + Paint p = new Paint(Paint.ANTI_ALIAS_FLAG); + p.setColorFilter(new PorterDuffColorFilter(Color.TRANSPARENT, PorterDuff.Mode.SRC)); + c.drawBitmap(src, 0, 0, null); + } +} + diff --git a/AndroidLiquidGlassView/src/main/java/com/qmdeve/liquidglass/impl/LiquidGlassimpl.java b/AndroidLiquidGlassView/src/main/java/com/qmdeve/liquidglass/impl/LiquidGlassimpl.java index 784549c..d7b2a6b 100644 --- a/AndroidLiquidGlassView/src/main/java/com/qmdeve/liquidglass/impl/LiquidGlassimpl.java +++ b/AndroidLiquidGlassView/src/main/java/com/qmdeve/liquidglass/impl/LiquidGlassimpl.java @@ -53,10 +53,13 @@ public final class LiquidGlassimpl implements Impl { private float lastCornerRadius, lastEccentricFactor, lastRefractionHeight, lastRefractionAmount, lastContrast, lastWhitePoint, lastChromaMultiplier, lastSigma, lastChromaticAberration, lastDepthEffect, lastBlurLevel, - lastTintRed, lastTintGreen, lastTintBlue, lastTintAlpha; + lastTintRed, lastTintGreen, lastTintBlue, lastTintAlpha, lastBorderWidth; private boolean needsUpdate = true; private long lastBlurUpdateTime = 0; + private long lastRecordTime = 0; + private static final long MIN_RECORD_INTERVAL_MS = 16; + private static final long MIN_BLUR_UPDATE_INTERVAL_MS = 100; private final Config config; public LiquidGlassimpl(View host, View target, Config config) { @@ -81,6 +84,7 @@ public LiquidGlassimpl(View host, View target, Config config) { lastTintGreen = Float.NaN; lastTintBlue = Float.NaN; lastTintAlpha = Float.NaN; + lastBorderWidth = Float.NaN; host.post(this::applyRenderEffect); } @@ -94,7 +98,11 @@ public void onSizeChanged(int w, int h) { @Override public void onPreDraw() { - record(); + long now = System.currentTimeMillis(); + if (now - lastRecordTime >= MIN_RECORD_INTERVAL_MS) { + record(); + lastRecordTime = now; + } float cornerRadius = config.CORNER_RADIUS_PX; float eccentricFactor = config.ECCENTRIC_FACTOR; @@ -110,23 +118,26 @@ public void onPreDraw() { float tintGreen = config.TINT_COLOR_GREEN; float tintBlue = config.TINT_COLOR_BLUE; float tintAlpha = config.TINT_ALPHA; + float borderWidth = config.BORDER_WIDTH; - boolean paramsChanged = - lastCornerRadius != cornerRadius || - lastEccentricFactor != eccentricFactor || - lastRefractionHeight != refractionHeight || - lastRefractionAmount != refractionAmount || - lastContrast != contrast || - lastWhitePoint != whitePoint || - lastChromaMultiplier != chromaMultiplier || - lastBlurLevel != blurLevel || - lastChromaticAberration != chromaticAberration || - lastDepthEffect != depthEffect || - lastTintRed != tintRed || - lastTintGreen != tintGreen || - lastTintBlue != tintBlue || - lastTintAlpha != tintAlpha || - needsUpdate; + boolean paramsChanged = needsUpdate; + if (!paramsChanged) { + paramsChanged = (Math.abs(lastCornerRadius - cornerRadius) > 0.1f) || + (Math.abs(lastEccentricFactor - eccentricFactor) > 0.001f) || + (Math.abs(lastRefractionHeight - refractionHeight) > 0.1f) || + (Math.abs(lastRefractionAmount - refractionAmount) > 0.1f) || + (Math.abs(lastContrast - contrast) > 0.001f) || + (Math.abs(lastWhitePoint - whitePoint) > 0.001f) || + (Math.abs(lastChromaMultiplier - chromaMultiplier) > 0.001f) || + (Math.abs(lastBlurLevel - blurLevel) > 0.01f) || + (Math.abs(lastChromaticAberration - chromaticAberration) > 0.001f) || + (Math.abs(lastDepthEffect - depthEffect) > 0.001f) || + (Math.abs(lastTintRed - tintRed) > 0.001f) || + (Math.abs(lastTintGreen - tintGreen) > 0.001f) || + (Math.abs(lastTintBlue - tintBlue) > 0.001f) || + (Math.abs(lastTintAlpha - tintAlpha) > 0.001f) || + (Math.abs(lastBorderWidth - borderWidth) > 0.1f); + } if (paramsChanged) { lastCornerRadius = cornerRadius; @@ -143,27 +154,47 @@ public void onPreDraw() { lastTintGreen = tintGreen; lastTintBlue = tintBlue; lastTintAlpha = tintAlpha; + lastBorderWidth = borderWidth; needsUpdate = false; applyRenderEffect(); } } private void record() { + if (target.getVisibility() != View.VISIBLE || host.getVisibility() != View.VISIBLE) { + return; + } int w = target.getWidth(), h = target.getHeight(); if (w == 0 || h == 0) return; - - Canvas rec = node.beginRecording(w, h); + target.getLocationInWindow(tp); host.getLocationInWindow(hp); - rec.translate(-(hp[0] - tp[0]), -(hp[1] - tp[1])); - target.draw(rec); + int dx = hp[0] - tp[0]; + int dy = hp[1] - tp[1]; + + if (dx == 0 && dy == 0 && w == host.getWidth() && h == host.getHeight()) { + Canvas rec = node.beginRecording(w, h); + if (rec != null) { + target.draw(rec); + } + } else { + Canvas rec = node.beginRecording(w, h); + if (rec != null) { + rec.translate(-dx, -dy); + target.draw(rec); + } + } node.endRecording(); } @Override public void draw(Canvas canvas) { - if (!canvas.isHardwareAccelerated()) return; - canvas.drawRenderNode(node); + if (!canvas.isHardwareAccelerated()) { + return; + } + if (node.hasDisplayList()) { + canvas.drawRenderNode(node); + } } private void applyRenderEffect() { @@ -193,7 +224,7 @@ private void applyRenderEffect() { RenderEffect contentEffect = null; if (blurLevel > 0.01f) { long now = System.currentTimeMillis(); - if (cachedBlurEffect == null || Math.abs(blurLevel - lastSigma) > 0.3f || now - lastBlurUpdateTime > 120) { + if (cachedBlurEffect == null || Math.abs(blurLevel - lastSigma) > 0.3f || now - lastBlurUpdateTime > MIN_BLUR_UPDATE_INTERVAL_MS) { try { contentEffect = RenderEffect.createBlurEffect(blurLevel, blurLevel, Shader.TileMode.CLAMP); cachedBlurEffect = contentEffect; @@ -219,6 +250,7 @@ private void applyRenderEffect() { liquidShader.setFloatUniform("chromaMultiplier", chromaMultiplier); liquidShader.setFloatUniform("tintColor", new float[]{tintRed, tintGreen, tintBlue}); liquidShader.setFloatUniform("tintAlpha", tintAlpha); + liquidShader.setFloatUniform("borderWidth", config.BORDER_WIDTH); RenderEffect shaderEffect = RenderEffect.createRuntimeShaderEffect(liquidShader, "content"); RenderEffect finalEffect = (contentEffect != null) diff --git a/AndroidLiquidGlassView/src/main/java/com/qmdeve/liquidglass/widget/LiquidGlassView.java b/AndroidLiquidGlassView/src/main/java/com/qmdeve/liquidglass/widget/LiquidGlassView.java index e3cb916..2a843cf 100644 --- a/AndroidLiquidGlassView/src/main/java/com/qmdeve/liquidglass/widget/LiquidGlassView.java +++ b/AndroidLiquidGlassView/src/main/java/com/qmdeve/liquidglass/widget/LiquidGlassView.java @@ -46,7 +46,7 @@ public class LiquidGlassView extends ViewGroup { private LiquidGlass glass; private ViewGroup customSource; private final Context context; - private float cornerRadius = Utils.dp2px(getResources(), 40), refractionHeight = Utils.dp2px(getResources(), 20), refractionOffset = -Utils.dp2px(getResources(), 70), tintAlpha = 0.0f, tintColorRed = 1.0f, tintColorGreen = 1.0f, tintColorBlue = 1.0f, blurRadius = 0.01f, dispersion = 0.5f, downX, downY, startTx, startTy; + private float cornerRadius = Utils.dp2px(getResources(), 40), refractionHeight = Utils.dp2px(getResources(), 20), refractionOffset = -Utils.dp2px(getResources(), 70), tintAlpha = 0.0f, tintColorRed = 1.0f, tintColorGreen = 1.0f, tintColorBlue = 1.0f, blurRadius = 0.01f, dispersion = 0.5f, borderWidth = 0.0f, downX, downY, startTx, startTy; private boolean draggableEnabled = false; private boolean elasticEnabled = false; private boolean touchEffectEnabled = false; @@ -82,6 +82,7 @@ public LiquidGlassView(Context context, @Nullable AttributeSet attrs, int defSty private void init() { setClipToPadding(false); setClipChildren(false); + setWillNotDraw(false); liquidTracker = new LiquidTracker(this); glowPaint = new Paint(Paint.ANTI_ALIAS_FLAG); @@ -172,7 +173,7 @@ public void setRefractionHeight(float px) { /** * Set the refraction offset px - * Positive value will be converted to negative + * Positive values will be converted to negative * * @param px float */ @@ -275,6 +276,16 @@ public void setTouchEffectEnabled(boolean enabled) { this.touchEffectEnabled = enabled; } + /** + * Set the border width in pixels. Only the border area will show liquid effect, center will be transparent. + * Set to 0 to show effect on entire view. + * @param px float border width in pixels + */ + public void setBorderWidth(float px) { + this.borderWidth = Math.max(0f, px); + updateConfig(); + } + private void updateConfig() { if (glass == null) { rebuild(); @@ -297,6 +308,7 @@ private void updateConfig() { config.TINT_COLOR_BLUE = tintColorBlue; config.TINT_COLOR_GREEN = tintColorGreen; config.TINT_COLOR_RED = tintColorRed; + config.BORDER_WIDTH = borderWidth; glass.post(() -> glass.updateParameters()); } @@ -355,6 +367,7 @@ private void ensureGlass() { .tintColorGreen(tintColorGreen) .tintColorBlue(tintColorBlue) .dispersion(dispersion) + .borderWidth(borderWidth) .size(w, h) ); diff --git a/AndroidLiquidGlassView/src/main/res/raw/liquidglass_effect.agsl b/AndroidLiquidGlassView/src/main/res/raw/liquidglass_effect.agsl index 5e767a1..f73ad2f 100644 --- a/AndroidLiquidGlassView/src/main/res/raw/liquidglass_effect.agsl +++ b/AndroidLiquidGlassView/src/main/res/raw/liquidglass_effect.agsl @@ -33,6 +33,7 @@ uniform float chromaMultiplier; uniform float3 tintColor; uniform float tintAlpha; +uniform float borderWidth; const half3 rgbToY = half3(0.2126, 0.7152, 0.0722); @@ -55,8 +56,13 @@ float sdRoundedRect(float2 coord, float2 halfSize, float radius) { float2 gradSdRoundedRect(float2 coord, float2 halfSize, float radius) { float2 cornerCoord = abs(coord) - (halfSize - float2(radius)); + float2 maxCorner = max(cornerCoord, 0.0); if (cornerCoord.x >= 0.0 || cornerCoord.y >= 0.0) { - return sign(coord) * normalize(max(cornerCoord, 0.0)); + float len = length(maxCorner); + if (len > 0.0) { + return sign(coord) * (maxCorner / len); + } + return sign(coord); } else { float gradX = step(cornerCoord.y, cornerCoord.x); return sign(coord) * float2(gradX, 1.0 - gradX); @@ -68,6 +74,9 @@ float circleMap(float x) { } half4 saturateColor(half4 color, float amount) { + if (abs(amount - 1.0) < 0.001) { + return color; + } half3 lin = toLinearSrgb(color.rgb); float y = dot(lin, rgbToY); half3 gray = half3(y); @@ -81,16 +90,26 @@ half4 main(float2 coord) { float radius = radiusAt(centeredCoord, cornerRadii); float sd = sdRoundedRect(centeredCoord, halfSize, radius); + float distanceFromEdge = -sd; + + half borderAlpha = 1.0; + if (borderWidth > 0.0) { + borderAlpha = 1.0 - smoothstep(0.0, borderWidth, distanceFromEdge); + if (borderAlpha <= 0.0) { + return half4(0.0); + } + } + if (-sd >= refractionHeight) { half4 baseColor = content.eval(coord); baseColor = saturateColor(baseColor, chromaMultiplier); - float3 target = (whitePoint > 0.0) ? float3(1.0) : float3(0.0); + half3 target = (whitePoint > 0.0) ? half3(1.0) : half3(0.0); baseColor.rgb = mix(baseColor.rgb, target, abs(whitePoint)); baseColor.rgb = (baseColor.rgb - 0.5) * (1.0 + contrast) + 0.5; - half3 tintedRGB = mix(baseColor.rgb, tintColor, tintAlpha); - return half4(tintedRGB, baseColor.a); + half3 tintedRGB = mix(baseColor.rgb, half3(tintColor), tintAlpha); + return half4(tintedRGB, baseColor.a * borderAlpha); } sd = min(sd, 0.0); @@ -99,50 +118,61 @@ half4 main(float2 coord) { float gradRadius = min(smoothRadius, min(halfSize.x, halfSize.y)); float2 grad = normalize(gradSdRoundedRect(centeredCoord, halfSize, gradRadius) + depthEffect * normalize(centeredCoord)); - float2 refractedCoord = coord + d * grad; - float dispersionIntensity = chromaticAberration * ((centeredCoord.x * centeredCoord.y) / (halfSize.x * halfSize.y)); - float2 dispersedCoord = d * grad * dispersionIntensity; - + half4 color = half4(0.0); - - half4 red = content.eval(refractedCoord + dispersedCoord); - color.r += red.r / 3.5; - color.a += red.a / 7.0; - - half4 orange = content.eval(refractedCoord + dispersedCoord * (2.0 / 3.0)); - color.r += orange.r / 3.5; - color.g += orange.g / 7.0; - color.a += orange.a / 7.0; - - half4 yellow = content.eval(refractedCoord + dispersedCoord * (1.0 / 3.0)); - color.r += yellow.r / 3.5; - color.g += yellow.g / 3.5; - color.a += yellow.a / 7.0; - - half4 green = content.eval(refractedCoord); - color.g += green.g / 3.5; - color.a += green.a / 7.0; - - half4 cyan = content.eval(refractedCoord - dispersedCoord * (1.0 / 3.0)); - color.g += cyan.g / 3.5; - color.b += cyan.b / 3.0; - color.a += cyan.a / 7.0; - - half4 blue = content.eval(refractedCoord - dispersedCoord * (2.0 / 3.0)); - color.b += blue.b / 3.0; - color.a += blue.a / 7.0; - - half4 purple = content.eval(refractedCoord - dispersedCoord); - color.r += purple.r / 7.0; - color.b += purple.b / 3.0; - color.a += purple.a / 7.0; + + if (chromaticAberration > 0.01) { + float dispersionIntensity = chromaticAberration * ((centeredCoord.x * centeredCoord.y) / (halfSize.x * halfSize.y)); + float2 dispersedCoord = d * grad * dispersionIntensity; + + half inv35 = 1.0 / 3.5; + half inv7 = 1.0 / 7.0; + half inv3 = 1.0 / 3.0; + half twoThirds = 2.0 / 3.0; + half oneThird = 1.0 / 3.0; + + half4 red = content.eval(refractedCoord + dispersedCoord); + color.r += red.r * inv35; + color.a += red.a * inv7; + + half4 orange = content.eval(refractedCoord + dispersedCoord * twoThirds); + color.r += orange.r * inv35; + color.g += orange.g * inv7; + color.a += orange.a * inv7; + + half4 yellow = content.eval(refractedCoord + dispersedCoord * oneThird); + color.r += yellow.r * inv35; + color.g += yellow.g * inv35; + color.a += yellow.a * inv7; + + half4 green = content.eval(refractedCoord); + color.g += green.g * inv35; + color.a += green.a * inv7; + + half4 cyan = content.eval(refractedCoord - dispersedCoord * oneThird); + color.g += cyan.g * inv35; + color.b += cyan.b * inv3; + color.a += cyan.a * inv7; + + half4 blue = content.eval(refractedCoord - dispersedCoord * twoThirds); + color.b += blue.b * inv3; + color.a += blue.a * inv7; + + half4 purple = content.eval(refractedCoord - dispersedCoord); + color.r += purple.r * inv7; + color.b += purple.b * inv3; + color.a += purple.a * inv7; + } else { + half4 baseColor = content.eval(refractedCoord); + color = baseColor; + } color = saturateColor(color, chromaMultiplier); - float3 target = (whitePoint > 0.0) ? float3(1.0) : float3(0.0); + half3 target = (whitePoint > 0.0) ? half3(1.0) : half3(0.0); color.rgb = mix(color.rgb, target, abs(whitePoint)); color.rgb = (color.rgb - 0.5) * (1.0 + contrast) + 0.5; - half3 tintedRGB = mix(color.rgb, tintColor, tintAlpha); - return half4(tintedRGB, color.a); + half3 tintedRGB = mix(color.rgb, half3(tintColor), tintAlpha); + return half4(tintedRGB, color.a * borderAlpha); } \ No newline at end of file diff --git a/AndroidLiquidGlassView/src/main/res/values-vi/strings.xml b/AndroidLiquidGlassView/src/main/res/values-vi/strings.xml new file mode 100644 index 0000000..e3257e4 --- /dev/null +++ b/AndroidLiquidGlassView/src/main/res/values-vi/strings.xml @@ -0,0 +1,245 @@ + + +Vui lòng chọn ngôn ngữ +Bắt đầu +Tiếp theo +Tiếp tục +"Quyền ghi âm là quyền cần thiết để ứng dụng hoạt động" +Để sử dụng tính năng này, bạn cần có quyền +Cảm ơn bạn đã sử dụng +Chúng tôi làm việc rất chăm chỉ để cải thiện ứng dụng cho bạn, và rất muốn biết: bạn sẽ đánh giá ứng dụng của chúng tôi như thế nào +Đánh giá ngay +Cảm ơn bạn đã đánh giá +Quyền truy cập +Học Piano +Bài hát của tôi +Cài đặt +Hướng dẫn +Trang chủ +Piano +Chơi +Trống +Guitar +Saxophone +Chính sách +Đánh giá ứng dụng này +Chia sẻ +Để tôi giới thiệu… +Chia sẻ đến +Quay lại +Phong cách +Nhạc cụ +Ghi âm +Chơi đôi +2 Phím +Equalizer +Tốc độ +Hiện ghi chú +Âm lượng +Chạm vào các phím piano để bắt đầu. Bạn có thể chọn bài hát để chơi piano và ghi lại video +Danh sách phát +Bạn cần bật quyền +Đi đến cài đặt +Lưu ghi âm +Bạn có muốn lưu tệp ghi âm này không? +Lưu +Xóa tệp +Bạn có muốn xóa tệp này vào thùng rác không? +Thành công! +Chơi Piano như một chuyên gia +Khám phá thêm âm thanh +Chơi cùng với các bài hát +Cảm nhận nhịp điệu +Học và thực hành giai điệu với một cây piano thực tế. +Thử piano, trống và bàn phím trong một nơi. +Theo dõi nốt nhạc và chơi các bài hát yêu thích của bạn. +Chạm và tạo nhịp với âm thanh trống thực. +Bỏ qua +Xóa +Chỉnh sửa ghi âm +Thay đổi tên ghi âm +Vui lòng nhập tên tệp +Quyền ghi âm +Để sử dụng các tính năng này, bạn cần cho phép quyền truy cập ghi âm +Mở cài đặt +Tạm dừng +Có vẻ như bạn chưa có tệp nào +Mở Piano +Phong cách Piano +Thay đổi thành công +"Hơn nữa, bạn có thể chọn nhiều chế độ để tùy chỉnh các phím piano." +Bạn có thể thay đổi phong cách piano và chọn nhiều chế độ để chơi. Bạn có thể tùy chỉnh trong cài đặt. +Nhấn vào dây guitar để bắt đầu. Bạn có thể chọn hợp âm trước khi chơi guitar. Hơn nữa, bạn có thể ghi lại video và thay đổi phong cách guitar. +Nhấn nút trên saxophone để bắt đầu. Bạn có thể thử chơi với nhiều âm thanh. Hơn nữa, bạn có thể ghi lại video và tùy chỉnh phong cách saxophone. +Chạm vào trống hoặc đĩa để bắt đầu. Bạn có thể chơi trống với âm thanh như kick, snare, crash, tom... Hơn nữa, bạn có thể ghi lại video và tùy chỉnh phong cách trống. +Phong cách Guitar +Phong cách Saxophone +Phong cách Trống +Tên tệp quá dài, giới hạn 30 ký tự +Tệp đã tồn tại, vui lòng thử lại +Cho phép quyền! +Cho phép quyền ghi âm & ghi vào bộ nhớ ngoài? +Bạn có thích ứng dụng của chúng tôi không? +Nếu bạn thích ứng dụng này, vui lòng đánh giá ứng dụng của chúng tôi 5 sao trên cửa hàng! +Vui lòng phản hồi! +Hủy +Sau +Học với những bài hát bạn yêu thích! +Chơi Piano +Nhập +Ẩn ghi chú +Danh sách ghi âm +Bạn chưa có ghi âm nào +%1$s • %2$s +Cần quyền truy cập bộ nhớ. Đi đến cài đặt Android, nhấn vào quyền và nhấn cho phép! +Bỏ qua +Đi đến cài đặt +Đổi tên +Xóa +Tên ghi âm... +Bạn có muốn xóa ghi âm này không? +Trống +Chạm vào các phím piano để bắt đầu. Bạn có thể chọn bài hát để chơi piano và ghi lại video. +Hơn nữa, bạn có thể chọn nhiều chế độ để tùy chỉnh các phím piano. +Bạn có thể thay đổi phong cách piano và chọn nhiều chế độ để chơi. Bạn có thể tùy chỉnh trong cài đặt. +Nhấn vào dây guitar để bắt đầu. Bạn có thể chọn hợp âm trước khi chơi guitar. Hơn nữa, bạn có thể ghi lại video và thay đổi phong cách guitar. +Chạm vào trống hoặc đĩa để bắt đầu. Bạn có thể chơi trống với âm thanh như kick, snare, crash, tom... Hơn nữa, bạn có thể ghi lại video và tùy chỉnh phong cách trống. +Nhấn nút trên saxophone để bắt đầu. Bạn có thể thử chơi với nhiều âm thanh. Hơn nữa, bạn có thể ghi lại video và tùy chỉnh phong cách saxophone. +Hoàn tất +Chúc mừng, bạn đã thay đổi chủ đề thành công +Đang tải... +Bạn có chắc chắn muốn quay lại không? +Bạn có chắc chắn muốn thoát không? +Thoát +Chúng tôi đã làm tốt nhất có thể +Cảm ơn bạn +Cảm ơn bạn đã phản hồi! +Vuốt để khám phá +Pad trống +Bạn có muốn tiêu tốn %s xu cho bài hát này không? +Mua bài hát +Mua +Xin lỗi bạn! +Số xu của bạn không đủ. Chỉ cần\nchơi game để thu thập thêm xu +OK +Đi nào +Đang áp dụng ngôn ngữ đã chọn... +Xin chào phần trống +Chơi những bài hát yêu thích của bạn ngay bây giờ! +Ngôn ngữ +Chủ đề +Nhập tên ghi âm của bạn ở đây +Tệp đã tồn tại, vui lòng thử lại! +Chủ đề đã được áp dụng thành công! +Chơi nhạc +Chọn phong cách piano của bạn +Tiếp tục +Bạn thích chơi thể loại nhạc nào trên Piano? +Danh sách ghi âm của tôi +Không có ghi âm nào ở đây +Vui lòng chọn ít nhất một thể loại +Xóa? +Thích học piano không? +Chạm vào một ngôi sao để đánh giá trên Google Play. +Gửi +Piano \nBàn phím +Trò chơi Piano +Tất cả bài hát +Mất kết nối +Vui lòng kết nối internet để hiển thị vị trí của bạn +Tại sao lại gỡ ứng dụng khi nó giữ cho loa của bạn nghe như mới? +Thử lại +Vẫn muốn gỡ cài đặt +Vấn đề gặp phải trong quá trình sử dụng +Tính năng không hoạt động +Quá nhiều quảng cáo +Tôi không cần nó +Khác +Hơn 90% người dùng đã thành công trong việc làm sạch loa của họ với ứng dụng này. +Gỡ cài đặt +Bàn phím Piano +Trống +Chủ đề +Bàn phím +Trống +Chủ đề +Ghi âm của tôi +Người mới bắt đầu +Có kỹ năng +Chuyên gia +Tất cả thể loại +Tất cả cấp độ +Người mới bắt đầu: +Cách chơi +Cấp độ thành thạo +Hệ thống tính điểm +Điểm +Có tổng cộng ba cấp độ cho bài học Piano: Người mới bắt đầu, Có kỹ năngChuyên gia, sắp xếp từ dễ nhất đến khó nhất. +Người mới bắt đầu được thiết kế cho những người mới, Có kỹ năng dành cho những người có một chút kinh nghiệm, và Chuyên gia dành cho những người chơi nâng cao +Hãy để chúng tôi giới thiệu hệ thống tính điểm của chúng tôi. Có bốn trạng thái thời gian khi chơi piano: Hoàn hảo, Tốt, Sớm và Bỏ lỡ. Điểm cuối cùng của bạn do đó dựa trên số lượng của mỗi trạng thái mà bạn đạt được. +Hoàn hảo +Tốt +Sớm +Bỏ lỡ +Điểm +Thời gian và độ chính xác tuyệt vời — sẵn sàng để thử một bài hát khó hơn hoặc chế độ mới? +Công việc tuyệt vời trong việc làm chủ cấp độ này—hãy thử thách bản thân với những bài hát khó hơn. +Bạn đã làm chủ những điều cơ bản—hãy kiểm tra kỹ năng của bạn ở các cấp độ khó hơn. +Học gần như hoàn hảo! Hãy xem xét việc khám phá kiến thức sâu hơn để mở rộng kỹ năng của bạn. +Nỗ lực và độ chính xác thực sự ấn tượng. Hãy tiếp tục mở rộng ranh giới học tập của bạn. +Bạn đã rõ ràng làm chủ bài học này. Hãy thử các kỹ thuật mới để mở rộng phạm vi của bạn. +Bạn đã làm điều này trông dễ dàng. Hãy thử các etude nâng cao để tinh chỉnh hơn nữa. +Kết quả của bạn chứng minh rằng nỗ lực liên tục dẫn đến thành công xuất sắc. +Hãy tiếp tục dẫn đầu với màn trình diễn đáng chú ý của bạn. +Kỹ năng của bạn thật ấn tượng—hãy thử thách bản thân hơn nữa bằng cách tham gia vào các cấp độ trò chơi khó hơn. +Một thành tựu hiếm có cho thấy tiềm năng thực sự của bạn. +Bạn hiểu tài liệu rất tốt. Một chút thực hành nữa sẽ đưa bạn lên đỉnh. +Bạn đang thể hiện sự hiểu biết mạnh mẽ. Một chút ôn tập nữa sẽ làm cho nó hoàn hảo. +Tiến bộ liên tục! Hãy tiếp tục thực hành và bạn sẽ sớm làm chủ bài học. +Bạn đã xây dựng một nền tảng vững chắc—hãy tiếp tục hướng tới sự xuất sắc. +Với một chút nỗ lực nữa, bạn sẽ đạt đến đỉnh cao. +Hãy tiếp tục tinh chỉnh kiến thức của bạn—bạn đang gần đến sự vĩ đại. +Bạn đang gần đến sự xuất sắc — chỉ cần một vài điều chỉnh là cần thiết. +Bạn đã nắm bắt được cơ chế rất tốt—hãy tinh chỉnh thực hành của bạn để đạt được hiệu suất hàng đầu. +Bạn đã nắm bắt được những điều cơ bản một cách rõ ràng. Hãy tinh chỉnh độ chính xác của bạn để vươn cao hơn. +Bạn đang có tiến bộ ổn định. Hãy tiếp tục thực hành hàng ngày để mở khóa tiềm năng đầy đủ của bạn. +Bạn đã xây dựng được những điều cơ bản. Hãy tiếp tục thực hành thường xuyên để củng cố kỹ năng của bạn. +Nền tảng đã có. Việc lặp lại cẩn thận hơn sẽ giúp bạn cải thiện nhanh chóng. +Bạn đang tiến lên từng bước một. Thực hành hàng ngày sẽ mang lại sự phát triển ổn định. +Sai lầm là điều bình thường. Mỗi lần thử giúp bạn kiểm soát tốt hơn. +Bạn đã thể hiện nỗ lực. Nhiều kiên nhẫn và thực hành sẽ đưa bạn lên cao hơn. +Bạn đang xây dựng một nền tảng vững chắc. Hãy củng cố nó bằng các gam và bài tập. +Bạn đang xây dựng một nền tảng vững chắc. Hãy tiếp tục thực hành hàng ngày để cải thiện. +Thực hành thường xuyên sẽ biến nỗ lực của bạn thành sự thành thạo thực sự. +Bạn đang học rất tốt. Hãy tập trung vào việc sửa chữa những sai lầm nhỏ để có dòng chảy tốt hơn. +Tiến bộ của bạn là rõ ràng. Hãy giữ vững và kết quả sẽ cải thiện. +Tiến bộ của bạn có thể cảm thấy chậm, nhưng sự cống hiến sẽ được đền đáp. +Mỗi bước đều quan trọng. Hãy tiếp tục thực hành chậm rãi và kỹ năng của bạn sẽ phát triển. +Sai lầm là một phần của hành trình. Mỗi lần thử đưa bạn gần hơn đến sự thành thạo. +Các kỹ năng cơ bản cần được củng cố, hãy dành nhiều thời gian hơn để thực hành những điều cơ bản. +Học piano cần thời gian. Hãy tập trung vào các phần ngắn và xây dựng độ chính xác từ từ. +Mỗi bài tập đưa bạn gần hơn đến mục tiêu của mình. +Thực hành chậm rãi và cẩn thận sẽ xây dựng kỹ năng mạnh mẽ hơn theo thời gian. +Tiến bộ có thể chậm, nhưng nỗ lực ổn định sẽ đưa bạn lên cao hơn. +Lặp lại giúp củng cố trí nhớ và kỹ thuật của bạn. Hãy tiếp tục luyện tập! +Tập trung vào độ chính xác và nhịp điệu. Sự cải thiện sẽ đến. +Mỗi nghệ sĩ piano đều bắt đầu từ đây. Hãy luyện tập những điều cơ bản và bạn sẽ tiến bộ từng bước một. +Đây chỉ là khởi đầu. Hãy giữ động lực và sự tiến bộ sẽ đến theo thời gian. +Đây không phải là thất bại, mà là bước đầu tiên trong con đường học tập của bạn. +Điểm số này chỉ là một điểm dừng. Hãy tiếp tục tiến về phía trước với sự kiên nhẫn. +Ai cũng mắc lỗi lúc đầu. Sự cống hiến sẽ giúp bạn phát triển. +Luyện tập hàng ngày đơn giản là chìa khóa để xây dựng kỹ năng vững mạnh hơn. +Điểm số thấp không có nghĩa là thất bại. Với sự luyện tập đều đặn, bạn sẽ cải thiện. +Điểm số này không phải là kết thúc, mà là cơ hội để bắt đầu lại mạnh mẽ hơn. +Sự thành thạo piano cần thời gian. Hãy giữ bình tĩnh và tiếp tục luyện tập. +Củng cố nền tảng của bạn với các gam và bài tập đơn giản. +Đang tải dữ liệu… +Không có bài hát nào ở đây +Tên không hợp lệ! +Thử lại +Tiếp theo +Trang chủ +Chia sẻ +Tiếp tục +Chọn bài hát + diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0a875e2..916f49a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -10,10 +10,12 @@ android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" + android:largeHeap="true" android:theme="@style/Theme.LiquidGlassView"> + @@ -28,5 +30,8 @@ + \ No newline at end of file diff --git a/app/src/main/java/com/qmdeve/liquidglass/demo/LiquidGlassAdapter.java b/app/src/main/java/com/qmdeve/liquidglass/demo/LiquidGlassAdapter.java new file mode 100644 index 0000000..27eecf1 --- /dev/null +++ b/app/src/main/java/com/qmdeve/liquidglass/demo/LiquidGlassAdapter.java @@ -0,0 +1,89 @@ +/** + * Copyright 2025 QmDeve + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @author QmDeve + * @github https://github.com/QmDeve + * @since 2025-11-01 + */ + +package com.qmdeve.liquidglass.demo; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.qmdeve.liquidglass.widget.LiquidGlassView; + +public class LiquidGlassAdapter extends RecyclerView.Adapter { + + private static final int ITEM_COUNT = 50; + private final android.content.Context context; + private final ViewGroup contentContainer; + + public LiquidGlassAdapter(android.content.Context context, ViewGroup contentContainer) { + this.context = context; + this.contentContainer = contentContainer; + } + + @Override + public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) { + super.onAttachedToRecyclerView(recyclerView); + recyclerView.setHasFixedSize(true); + recyclerView.setItemViewCacheSize(10); + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_liquid_glass, parent, false); + ViewHolder holder = new ViewHolder(view); + if (contentContainer != null) { + holder.liquidGlassView.bind(contentContainer); + holder.liquidGlassView.setDraggableEnabled(false); + holder.liquidGlassView.setBlurRadius(10); + holder.liquidGlassView.setElasticEnabled(false); + holder.liquidGlassView.setTouchEffectEnabled(false); + } + return holder; + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { + } + + @Override + public void onViewRecycled(@NonNull ViewHolder holder) { + super.onViewRecycled(holder); + } + + @Override + public int getItemCount() { + return ITEM_COUNT; + } + + static class ViewHolder extends RecyclerView.ViewHolder { + LiquidGlassView liquidGlassView; + + ViewHolder(@NonNull View itemView) { + super(itemView); + liquidGlassView = itemView.findViewById(R.id.liquidGlassView); + } + } +} + diff --git a/app/src/main/java/com/qmdeve/liquidglass/demo/LiquidGlassListActivity.java b/app/src/main/java/com/qmdeve/liquidglass/demo/LiquidGlassListActivity.java new file mode 100644 index 0000000..0cde1c2 --- /dev/null +++ b/app/src/main/java/com/qmdeve/liquidglass/demo/LiquidGlassListActivity.java @@ -0,0 +1,69 @@ +/** + * Copyright 2025 QmDeve + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @author QmDeve + * @github https://github.com/QmDeve + * @since 2025-11-01 + */ + +package com.qmdeve.liquidglass.demo; + +import android.os.Bundle; +import android.view.View; +import android.view.ViewGroup; + +import androidx.activity.EdgeToEdge; +import androidx.appcompat.app.AppCompatActivity; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.qmdeve.liquidglass.demo.util.Utils; + +public class LiquidGlassListActivity extends AppCompatActivity { + + private RecyclerView recyclerView; + private LiquidGlassAdapter adapter; + private ViewGroup contentContainer; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + EdgeToEdge.enable(this); + setContentView(R.layout.activity_liquid_glass_list); + Utils.transparentStatusBar(getWindow()); + Utils.transparentNavigationBar(getWindow()); + initView(); + setupRecyclerView(); + } + + private void initView() { + recyclerView = findViewById(R.id.recyclerView); + contentContainer = findViewById(R.id.content_container); + } + + private void setupRecyclerView() { + adapter = new LiquidGlassAdapter(this, contentContainer); + LinearLayoutManager layoutManager = new GridLayoutManager(this,1); + layoutManager.setItemPrefetchEnabled(true); + layoutManager.setInitialPrefetchItemCount(5); + recyclerView.setLayoutManager(layoutManager); + recyclerView.setItemAnimator(null); + recyclerView.setDrawingCacheEnabled(true); + recyclerView.setDrawingCacheQuality(View.DRAWING_CACHE_QUALITY_HIGH); + recyclerView.setAdapter(adapter); + } +} + diff --git a/app/src/main/java/com/qmdeve/liquidglass/demo/MainActivity.java b/app/src/main/java/com/qmdeve/liquidglass/demo/MainActivity.java index c2afaac..c0aafcb 100644 --- a/app/src/main/java/com/qmdeve/liquidglass/demo/MainActivity.java +++ b/app/src/main/java/com/qmdeve/liquidglass/demo/MainActivity.java @@ -49,6 +49,7 @@ protected void onCreate(Bundle savedInstanceState) { findViewById(R.id.liquidglassview).setOnClickListener(v -> startActivity(new Intent(this, LiquidGlassViewActivity.class))); findViewById(R.id.elasticliquidglassview).setOnClickListener(v -> startActivity(new Intent(this, ElasticLiquidGlassViewActivity.class))); findViewById(R.id.toucheffectview).setOnClickListener(v -> startActivity(new Intent(this, TouchEffectActivity.class))); + findViewById(R.id.liquidglasslist).setOnClickListener(v -> startActivity(new Intent(this, LiquidGlassListActivity.class))); findViewById(R.id.github).setOnClickListener(v -> startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/QmDeve/AndroidLiquidGlassView")))); } } \ No newline at end of file diff --git a/app/src/main/res/layout/activity_liquid_glass_list.xml b/app/src/main/res/layout/activity_liquid_glass_list.xml new file mode 100644 index 0000000..10c6a42 --- /dev/null +++ b/app/src/main/res/layout/activity_liquid_glass_list.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index a816903..ef41606 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -37,6 +37,16 @@ app:buttonTextBold="true" app:overlayColor="#0248FB" /> + + + + + + + + + + + + + +