Hi everyone ![]()
I’m happy to introduce my first extension!
RealtimeBlurBackground was born primarily out of the needs of my own projects.
Since I often use overlapping layouts to create notifications, drawers, and other UI components displayed on top of the main interface, I wanted to add a blur effect to better highlight the foreground element.
Over time, I tried several existing solutions, but I was looking for an extension that better matched my needs in terms of simplicity, integration, and overall behavior (with the utmost respect for all the blur extensions out there).
That’s how RealtimeBlurBackground came to life.
Its goal is to provide a simple and reliable solution for both static and real-time blur effects, while remaining compatible even with older Android devices.
To give you an idea of how straightforward it is, here’s the workflow:
- Capture a snapshot of the source layout.
- Apply a blur using the Stack Blur Algorithm by
Mario Klingemann
(Public Domain). - Set the resulting blurred image as the background of the target layout.
- Done!
The real-time effect simply repeats this process continuously, updating the blurred background as the source layout changes.
🧩 RealtimeBlurBackground
Live/real-time blurred background for layout components.
Highly cross-version compatible and performance-optimized.
Specifications
Package: com.rayzzz.reltimebackgroundblur.realtimeblurbackground
Size: 20,17 KB
Version: 1.0
Minimum API Level: 14
Updated On: 2026-07-20T22:00:00Z
Built & documented using: FAST
v7.0.0
Methods:
RealtimeBlurBackground has total 5 methods.
1. SetSource
Sets the source view component to capture and blur. Call this once before Start.
| Parameter | Type |
|---|---|
| component | component |
2. SetTarget
Sets the target view component where the blurred background drawable will be attached. Call this once before Start.
| Parameter | Type |
|---|---|
| component | component |
3. Start
Starts the live blur rendering cycle. Ensure SetSource and SetTarget have been defined first. LiveUpdatesEnabled defaults to false, so enable it separately (or call Refresh manually) if you need continuous updates.
4. Stop
Stops the live blur background loop and clears the background image from the target component.
5. Refresh
Forces an instantaneous frame capture and blur refresh step, bypassing LiveUpdatesEnabled locks.
Designer:
RealtimeBlurBackground has total 4 designer properties.
1. Radius
- Input type:
non_negative_integer - Default value:
12
2. DownsampleFactor
- Input type:
non_negative_integer - Default value:
4
3. MaxFrameRate
- Input type:
non_negative_integer - Default value:
30
4. LiveUpdatesEnabled
- Input type:
boolean - Default value:
False
Setters:
RealtimeBlurBackground has total 4 setter properties.
1. Radius
The blur radius intensity. Valid range: 0 to 100.
- Input type:
number
2. DownsampleFactor
Bitmap downsampling scale factor (1 to 8). Higher values yield faster execution but reduce clarity. Default: 4.
- Input type:
number
3. MaxFrameRate
Maximum execution updates per second (e.g., 30 or 60 FPS cap).
- Input type:
number
4. LiveUpdatesEnabled
When true, background captures update continuously in real time. When false, the background remains frozen on the last rendered frame until Refresh() is invoked manually. Defaults to false: enable it explicitly from blocks.
- Input type:
boolean
Getters:
RealtimeBlurBackground has total 4 getter properties.
1. Radius
The blur radius intensity. Valid range: 0 to 100.
- Return type:
number
2. DownsampleFactor
Bitmap downsampling scale factor (1 to 8). Higher values yield faster execution but reduce clarity. Default: 4.
- Return type:
number
3. MaxFrameRate
Maximum execution updates per second (e.g., 30 or 60 FPS cap).
- Return type:
number
4. LiveUpdatesEnabled
When true, background captures update continuously in real time. When false, the background remains frozen on the last rendered frame until Refresh() is invoked manually. Defaults to false: enable it explicitly from blocks.
- Return type:
boolean
AIA
RealtimeBlurBackground.aia (815.6 KB)
AIX
com.rayzzz.reltimebackgroundblur.realtimeblurbackground.aix (20.2 KB)
Source code
Source code
package com.rayzzz.reltimebackgroundblur.realtimeblurbackground;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.view.View;
import com.google.appinventor.components.annotations.DesignerComponent;
import com.google.appinventor.components.annotations.DesignerProperty;
import com.google.appinventor.components.annotations.PropertyCategory;
import com.google.appinventor.components.annotations.SimpleFunction;
import com.google.appinventor.components.annotations.SimpleProperty;
import com.google.appinventor.components.common.ComponentCategory;
import com.google.appinventor.components.common.PropertyTypeConstants;
import com.google.appinventor.components.runtime.AndroidNonvisibleComponent;
import com.google.appinventor.components.runtime.AndroidViewComponent;
import com.google.appinventor.components.runtime.ComponentContainer;
@DesignerComponent(
version = 1,
versionName = "1.0",
description = "Live/real-time blurred background for layout components. Highly cross-version compatible and performance-optimized.<br><br>Developed by RaYzZz using Fast <3.",
nonVisible = true,
iconName = "icon.png"
)
public class RealtimeBlurBackground extends AndroidNonvisibleComponent {
private final Handler handler = new Handler(Looper.getMainLooper());
private AndroidViewComponent sourceComponent;
private AndroidViewComponent targetComponent;
private BlurDrawable drawable;
private boolean running = false;
private View.OnAttachStateChangeListener attachListener;
private View listenerAttachedView;
private int radius = 12;
private int downsampleFactor = 4;
private int maxFrameRate = 30;
private boolean liveUpdatesEnabled = false;
public RealtimeBlurBackground(ComponentContainer container) {
super(container.$form());
}
@SimpleFunction(description = "Sets the source view component to capture and blur. Call this once before Start.")
public void SetSource(AndroidViewComponent component) {
this.sourceComponent = component;
if (drawable != null) {
restartIfRunning();
}
}
@SimpleFunction(description = "Sets the target view component where the blurred background drawable will be attached. Call this once before Start.")
public void SetTarget(AndroidViewComponent component) {
this.targetComponent = component;
if (drawable != null) {
restartIfRunning();
}
}
@SimpleProperty(description = "The blur radius intensity. Valid range: 0 to 100.")
public int Radius() {
return radius;
}
@DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_NON_NEGATIVE_INTEGER, defaultValue = "12")
@SimpleProperty(description = "The blur radius intensity. Valid range: 0 to 100.")
public void Radius(int value) {
this.radius = Math.max(0, Math.min(100, value));
if (drawable != null) drawable.setRadius(this.radius);
}
@SimpleProperty(description = "Bitmap downsampling scale factor (1 to 8). Higher values yield faster execution but reduce clarity. Default: 4.")
public int DownsampleFactor() {
return downsampleFactor;
}
@DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_NON_NEGATIVE_INTEGER, defaultValue = "4")
@SimpleProperty(description = "Bitmap downsampling scale factor (1 to 8). Higher values yield faster execution but reduce clarity. Default: 4.")
public void DownsampleFactor(int value) {
this.downsampleFactor = Math.max(1, Math.min(8, value));
if (drawable != null) drawable.setDownsampleFactor(this.downsampleFactor);
}
@SimpleProperty(description = "Maximum execution updates per second (e.g., 30 or 60 FPS cap).")
public int MaxFrameRate() {
return maxFrameRate;
}
@DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_NON_NEGATIVE_INTEGER, defaultValue = "30")
@SimpleProperty(description = "Maximum execution updates per second (e.g., 30 or 60 FPS cap).")
public void MaxFrameRate(int fps) {
this.maxFrameRate = Math.max(1, fps);
if (drawable != null) drawable.setFrameIntervalMs(1000L / this.maxFrameRate);
}
@SimpleProperty(description = "When true, background captures update continuously in real time. When false, the background remains frozen on the last rendered frame until Refresh() is invoked manually. Defaults to false: enable it explicitly from blocks.")
public boolean LiveUpdatesEnabled() {
return liveUpdatesEnabled;
}
@DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_BOOLEAN, defaultValue = "False")
@SimpleProperty(category = PropertyCategory.BEHAVIOR, description = "When true, background captures update continuously in real time. When false, the background remains frozen on the last rendered frame until Refresh() is invoked manually. Defaults to false: enable it explicitly from blocks.")
public void LiveUpdatesEnabled(boolean enabled) {
this.liveUpdatesEnabled = enabled;
if (drawable != null) drawable.setLiveUpdatesEnabled(enabled);
}
@SimpleFunction(description = "Starts the live blur rendering cycle. Ensure SetSource and SetTarget have been defined first. LiveUpdatesEnabled defaults to false, so enable it separately (or call Refresh manually) if you need continuous updates.")
public void Start() {
if (sourceComponent == null || targetComponent == null) {
form.dispatchErrorOccurredEvent(this, "Start",
101, "You must set both Source and Target components before executing Start.");
return;
}
Stop();
View sourceView = sourceComponent.getView();
final View targetView = targetComponent.getView();
drawable = new BlurDrawable(sourceView, targetView, handler);
drawable.setRadius(radius);
drawable.setDownsampleFactor(downsampleFactor);
drawable.setFrameIntervalMs(1000L / maxFrameRate);
drawable.setLiveUpdatesEnabled(liveUpdatesEnabled);
setBackgroundCompat(targetView, drawable);
attachListener = new View.OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(View v) {
if (drawable != null) drawable.resume();
}
@Override
public void onViewDetachedFromWindow(View v) {
if (drawable != null) drawable.pause();
}
};
targetView.addOnAttachStateChangeListener(attachListener);
listenerAttachedView = targetView;
running = true;
drawable.resume();
drawable.forceUpdate();
}
@SimpleFunction(description = "Stops the live blur background loop and clears the background image from the target component.")
public void Stop() {
running = false;
if (listenerAttachedView != null && attachListener != null) {
listenerAttachedView.removeOnAttachStateChangeListener(attachListener);
}
attachListener = null;
listenerAttachedView = null;
if (drawable != null) {
drawable.pause();
View targetView = targetComponent != null ? targetComponent.getView() : null;
if (targetView != null && targetView.getBackground() == drawable) {
setBackgroundCompat(targetView, null);
}
drawable.release();
drawable = null;
}
}
@SimpleFunction(description = "Forces an instantaneous frame capture and blur refresh step, bypassing LiveUpdatesEnabled locks.")
public void Refresh() {
if (drawable != null) drawable.forceUpdate();
}
private void restartIfRunning() {
if (running) {
Start();
}
}
@SuppressWarnings("deprecation")
private static void setBackgroundCompat(View view, Drawable drawable) {
if (Build.VERSION.SDK_INT >= 16) {
view.setBackground(drawable);
} else {
view.setBackgroundDrawable(drawable);
}
}
private static final class BlurDrawable extends Drawable {
private final View sourceView;
private final View targetView;
private final Handler handler;
private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
private Bitmap workingBitmap;
private Bitmap frontBitmap;
private int radius = 12;
private int downsampleFactor = 4;
private long frameIntervalMs = 1000L / 30L;
private boolean liveUpdatesEnabled = false;
private boolean running = false;
private int[] pixels;
private int[] blurR, blurG, blurB;
private int[] dv;
private int[] stackR, stackG, stackB;
private int cachedScaledRadius = -1;
private final Runnable tick = new Runnable() {
@Override
public void run() {
if (!running) return;
if (liveUpdatesEnabled) {
updateBlur();
}
handler.postDelayed(this, frameIntervalMs);
}
};
BlurDrawable(View sourceView, View targetView, Handler handler) {
this.sourceView = sourceView;
this.targetView = targetView;
this.handler = handler;
}
void resume() {
if (running) return;
running = true;
handler.post(tick);
}
void pause() {
running = false;
handler.removeCallbacks(tick);
}
void forceUpdate() {
updateBlur();
}
void setRadius(int r) {
this.radius = Math.max(0, Math.min(100, r));
}
void setDownsampleFactor(int f) {
int newFactor = Math.max(1, Math.min(8, f));
if (newFactor != this.downsampleFactor) {
this.downsampleFactor = newFactor;
workingBitmap = null;
pixels = null;
}
}
void setFrameIntervalMs(long ms) {
this.frameIntervalMs = Math.max(1L, ms);
}
void setLiveUpdatesEnabled(boolean enabled) {
this.liveUpdatesEnabled = enabled;
}
void release() {
pause();
if (workingBitmap != null && !workingBitmap.isRecycled()) {
workingBitmap.recycle();
}
workingBitmap = null;
frontBitmap = null;
pixels = null;
blurR = blurG = blurB = null;
dv = null;
stackR = stackG = stackB = null;
cachedScaledRadius = -1;
}
private void updateBlur() {
int sw = sourceView.getWidth();
int sh = sourceView.getHeight();
if (sw <= 0 || sh <= 0) return;
int w = Math.max(1, sw / downsampleFactor);
int h = Math.max(1, sh / downsampleFactor);
if (workingBitmap == null || workingBitmap.getWidth() != w || workingBitmap.getHeight() != h) {
if (workingBitmap != null) workingBitmap.recycle();
workingBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
pixels = null;
}
Canvas canvas = new Canvas(workingBitmap);
canvas.drawColor(0, PorterDuff.Mode.CLEAR);
float scale = 1f / downsampleFactor;
canvas.save();
canvas.scale(scale, scale);
sourceView.draw(canvas);
canvas.restore();
int scaledRadius = Math.max(1, radius / downsampleFactor);
stackBlur(workingBitmap, scaledRadius);
frontBitmap = workingBitmap;
targetView.invalidate();
}
@Override
public void draw(Canvas canvas) {
if (frontBitmap == null || frontBitmap.isRecycled()) return;
Rect bounds = getBounds();
if (bounds.width() <= 0 || bounds.height() <= 0) return;
canvas.save();
canvas.translate(bounds.left, bounds.top);
canvas.scale(
bounds.width() / (float) frontBitmap.getWidth(),
bounds.height() / (float) frontBitmap.getHeight());
canvas.drawBitmap(frontBitmap, 0, 0, paint);
canvas.restore();
}
@Override
public void setAlpha(int alpha) {
paint.setAlpha(alpha);
}
@Override
public void setColorFilter(ColorFilter colorFilter) {
paint.setColorFilter(colorFilter);
}
@Override
public int getOpacity() {
return PixelFormat.TRANSLUCENT;
}
private void ensureStackBuffers(int radius) {
if (dv != null && radius == cachedScaledRadius) {
return;
}
int div = radius + radius + 1;
int divsum = ((div + 1) >> 1);
divsum *= divsum;
dv = new int[256 * divsum];
for (int i = 0; i < dv.length; i++) {
dv[i] = i / divsum;
}
stackR = new int[div];
stackG = new int[div];
stackB = new int[div];
cachedScaledRadius = radius;
}
/**
* Stack Blur Algorithm by Mario Klingemann (Public Domain).
* Executed in-place across two discrete drawing passes (Horizontal then Vertical).
* All backing buffers (pixel arrays, per-channel intermediate arrays, the dv[]
* lookup table and the stack ring buffers) are instance fields reused across
* frames, so no allocation happens on the steady-state path once sizes settle.
*/
private void stackBlur(Bitmap bitmap, int radius) {
int w = bitmap.getWidth();
int h = bitmap.getHeight();
int size = w * h;
if (pixels == null || pixels.length != size) {
pixels = new int[size];
}
if (blurR == null || blurR.length != size) {
blurR = new int[size];
blurG = new int[size];
blurB = new int[size];
}
bitmap.getPixels(pixels, 0, w, 0, 0, w, h);
ensureStackBuffers(radius);
int div = radius + radius + 1;
int wm = w - 1;
int hm = h - 1;
// --- Horizontal Processing Pass ---
int yi = 0;
for (int y = 0; y < h; y++) {
int rSum = 0, gSum = 0, bSum = 0;
int rIn = 0, gIn = 0, bIn = 0, rOut = 0, gOut = 0, bOut = 0;
int rowStart = yi;
for (int i = -radius; i <= radius; i++) {
int p = pixels[rowStart + Math.min(wm, Math.max(i, 0))];
int idx = i + radius;
int rr = (p >> 16) & 0xff, gg = (p >> 8) & 0xff, bb = p & 0xff;
stackR[idx] = rr; stackG[idx] = gg; stackB[idx] = bb;
int weight = radius + 1 - Math.abs(i);
rSum += rr * weight; gSum += gg * weight; bSum += bb * weight;
if (i > 0) { rIn += rr; gIn += gg; bIn += bb; }
else { rOut += rr; gOut += gg; bOut += bb; }
}
int sp = radius;
for (int x = 0; x < w; x++) {
blurR[yi] = dv[rSum]; blurG[yi] = dv[gSum]; blurB[yi] = dv[bSum];
rSum -= rOut; gSum -= gOut; bSum -= bOut;
int outIdx = (sp - radius + div) % div;
rOut -= stackR[outIdx]; gOut -= stackG[outIdx]; bOut -= stackB[outIdx];
int nextX = Math.min(x + radius + 1, wm);
int p = pixels[rowStart + nextX];
stackR[outIdx] = (p >> 16) & 0xff;
stackG[outIdx] = (p >> 8) & 0xff;
stackB[outIdx] = p & 0xff;
rIn += stackR[outIdx]; gIn += stackG[outIdx]; bIn += stackB[outIdx];
rSum += rIn; gSum += gIn; bSum += bIn;
sp = (sp + 1) % div;
int inIdx = sp;
rIn -= stackR[inIdx]; gIn -= stackG[inIdx]; bIn -= stackB[inIdx];
rOut += stackR[inIdx]; gOut += stackG[inIdx]; bOut += stackB[inIdx];
yi++;
}
}
// --- Vertical Processing Pass ---
for (int x = 0; x < w; x++) {
int rSum = 0, gSum = 0, bSum = 0;
int rIn = 0, gIn = 0, bIn = 0, rOut = 0, gOut = 0, bOut = 0;
for (int i = -radius; i <= radius; i++) {
int yPos = Math.min(hm, Math.max(i, 0));
int idx2 = yPos * w + x;
int idx = i + radius;
int rr = blurR[idx2], gg = blurG[idx2], bb = blurB[idx2];
stackR[idx] = rr; stackG[idx] = gg; stackB[idx] = bb;
int weight = radius + 1 - Math.abs(i);
rSum += rr * weight; gSum += gg * weight; bSum += bb * weight;
if (i > 0) { rIn += rr; gIn += gg; bIn += bb; }
else { rOut += rr; gOut += gg; bOut += bb; }
}
int sp = radius;
int yi2 = x;
for (int y = 0; y < h; y++) {
int rr = dv[rSum], gg = dv[gSum], bb = dv[bSum];
pixels[yi2] = (pixels[yi2] & 0xff000000) | (rr << 16) | (gg << 8) | bb;
rSum -= rOut; gSum -= gOut; bSum -= bOut;
int outIdx = (sp - radius + div) % div;
rOut -= stackR[outIdx]; gOut -= stackG[outIdx]; bOut -= stackB[outIdx];
int nextY = Math.min(y + radius + 1, hm);
int idx2 = nextY * w + x;
stackR[outIdx] = blurR[idx2];
stackG[outIdx] = blurG[idx2];
stackB[outIdx] = blurB[idx2];
rIn += stackR[outIdx]; gIn += stackG[outIdx]; bIn += stackB[outIdx];
rSum += rIn; gSum += gIn; bSum += bIn;
sp = (sp + 1) % div;
int inIdx = sp;
rIn -= stackR[inIdx]; gIn -= stackG[inIdx]; bIn -= stackB[inIdx];
rOut += stackR[inIdx]; gOut += stackG[inIdx]; bOut += stackB[inIdx];
yi2 += w;
}
}
bitmap.setPixels(pixels, 0, w, 0, 0, w, h);
}
}
}
RealtimeBlurBackground.java (19.0 KB)


