react-native-blur-vibe 0.1.5 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/README.md +374 -181
  2. package/android/build.gradle +2 -0
  3. package/android/src/main/java/com/blurvibe/BlurVibeView.kt +189 -161
  4. package/android/src/main/java/com/blurvibe/BlurVibeViewApi31.kt +448 -0
  5. package/android/src/main/java/com/blurvibe/BlurVibeViewManager.kt +74 -22
  6. package/ios/BlurVibeView.swift +28 -27
  7. package/ios/BlurVibeViewManager.m +9 -9
  8. package/ios/Views/BlurVibeSwiftUIView.swift +109 -16
  9. package/ios/Views/ProgressiveBlurView.swift +255 -0
  10. package/lib/commonjs/BlurVibeViewNativeComponent.ts +10 -16
  11. package/lib/commonjs/BlurView.js +34 -7
  12. package/lib/commonjs/BlurView.js.map +1 -1
  13. package/lib/module/BlurVibeViewNativeComponent.ts +10 -16
  14. package/lib/module/BlurView.js +34 -7
  15. package/lib/module/BlurView.js.map +1 -1
  16. package/lib/typescript/commonjs/src/BlurVibeViewNativeComponent.d.ts +4 -14
  17. package/lib/typescript/commonjs/src/BlurVibeViewNativeComponent.d.ts.map +1 -1
  18. package/lib/typescript/commonjs/src/BlurView.d.ts +27 -8
  19. package/lib/typescript/commonjs/src/BlurView.d.ts.map +1 -1
  20. package/lib/typescript/commonjs/src/types.d.ts +236 -18
  21. package/lib/typescript/commonjs/src/types.d.ts.map +1 -1
  22. package/lib/typescript/module/src/BlurVibeViewNativeComponent.d.ts +4 -14
  23. package/lib/typescript/module/src/BlurVibeViewNativeComponent.d.ts.map +1 -1
  24. package/lib/typescript/module/src/BlurView.d.ts +27 -8
  25. package/lib/typescript/module/src/BlurView.d.ts.map +1 -1
  26. package/lib/typescript/module/src/types.d.ts +236 -18
  27. package/lib/typescript/module/src/types.d.ts.map +1 -1
  28. package/package.json +1 -1
  29. package/src/BlurVibeViewNativeComponent.ts +10 -16
  30. package/src/BlurView.tsx +34 -7
  31. package/src/types.ts +267 -18
  32. package/android/src/main/java/com/blurvibe/BlurCaptureCoordinator.kt +0 -230
package/src/types.ts CHANGED
@@ -1,5 +1,44 @@
1
1
  import type { ViewProps } from 'react-native';
2
2
 
3
+ // ─────────────────────────────────────────────────────────────────────────────
4
+ // BlurType
5
+ // ─────────────────────────────────────────────────────────────────────────────
6
+
7
+ /**
8
+ * iOS blur material style — maps directly to `UIBlurEffect.Style`.
9
+ *
10
+ * **iOS only.** Ignored on Android (Android uses `blurAmount` + `overlayColor`
11
+ * to control blur appearance).
12
+ *
13
+ * ### Adaptive styles (recommended)
14
+ * These automatically adapt to light/dark mode:
15
+ * - `"light"` — light frosted glass (default)
16
+ * - `"dark"` — dark frosted glass
17
+ * - `"extraLight"` — brighter than light
18
+ * - `"regular"` — system default material
19
+ * - `"prominent"` — higher contrast than regular
20
+ *
21
+ * ### Material styles (iOS 13+, adaptive)
22
+ * - `"systemUltraThinMaterial"` — thinnest, most transparent
23
+ * - `"systemThinMaterial"` — thin
24
+ * - `"systemMaterial"` — medium (equivalent to iOS sheet backgrounds)
25
+ * - `"systemThickMaterial"` — thick
26
+ * - `"systemChromeMaterial"` — for toolbars and navigation bars
27
+ *
28
+ * ### Light variants (iOS 13+, always light)
29
+ * - `"systemUltraThinMaterialLight"`
30
+ * - `"systemThinMaterialLight"`
31
+ * - `"systemMaterialLight"`
32
+ * - `"systemThickMaterialLight"`
33
+ * - `"systemChromeMaterialLight"`
34
+ *
35
+ * ### Dark variants (iOS 13+, always dark)
36
+ * - `"systemUltraThinMaterialDark"`
37
+ * - `"systemThinMaterialDark"`
38
+ * - `"systemMaterialDark"`
39
+ * - `"systemThickMaterialDark"`
40
+ * - `"systemChromeMaterialDark"`
41
+ */
3
42
  export type BlurType =
4
43
  | 'light'
5
44
  | 'dark'
@@ -22,44 +61,254 @@ export type BlurType =
22
61
  | 'systemThickMaterialDark'
23
62
  | 'systemChromeMaterialDark';
24
63
 
64
+ // ─────────────────────────────────────────────────────────────────────────────
65
+ // ProgressiveBlurDirection
66
+ // ─────────────────────────────────────────────────────────────────────────────
67
+
68
+ /**
69
+ * Direction for progressive (gradient) blur.
70
+ *
71
+ * Controls which axis the blur fades across, and which end starts at full
72
+ * intensity vs. transparent. Use with `progressiveStartIntensity` and
73
+ * `progressiveEndIntensity` for fine control.
74
+ *
75
+ * **iOS**: Uses `CAFilter variableBlur` — true per-pixel variable radius,
76
+ * same technique as Apple's Home Screen and Control Center. Falls back to
77
+ * `maskView` opacity gradient if CAFilter is unavailable.
78
+ *
79
+ * **Android API 31+**: Uses `LinearGradient`/`RadialGradient` as an alpha
80
+ * mask over the GPU `RenderEffect` blur layer.
81
+ *
82
+ * **Android API < 31**: Silently ignored (uniform blur is shown).
83
+ *
84
+ * | Value | Blur starts at | Fades towards |
85
+ * |------------------|----------------|----------------|
86
+ * | `"topToBottom"` | Top edge | Bottom edge |
87
+ * | `"bottomToTop"` | Bottom edge | Top edge |
88
+ * | `"leftToRight"` | Left edge | Right edge |
89
+ * | `"rightToLeft"` | Right edge | Left edge |
90
+ * | `"radial"` | Center | Outer edges |
91
+ * | `"none"` | — uniform blur — no gradient |
92
+ *
93
+ * @example
94
+ * // Sticky header: full blur at top, invisible at bottom
95
+ * progressiveBlurDirection="topToBottom"
96
+ * progressiveStartIntensity={1}
97
+ * progressiveEndIntensity={0}
98
+ *
99
+ * @example
100
+ * // Bottom sheet scrim: invisible at top, full blur at bottom
101
+ * progressiveBlurDirection="bottomToTop"
102
+ * progressiveStartIntensity={1}
103
+ * progressiveEndIntensity={0}
104
+ */
105
+ export type ProgressiveBlurDirection =
106
+ | 'topToBottom'
107
+ | 'bottomToTop'
108
+ | 'leftToRight'
109
+ | 'rightToLeft'
110
+ | 'radial'
111
+ | 'none';
112
+
113
+ // ─────────────────────────────────────────────────────────────────────────────
114
+ // BlurViewProps
115
+ // ─────────────────────────────────────────────────────────────────────────────
116
+
25
117
  export interface BlurViewProps extends ViewProps {
118
+ // ─── Core props (iOS + Android) ───────────────────────────────────────────
119
+
26
120
  /**
27
- * Blur intensity from 0 to 100.
121
+ * Blur intensity. Range: `0` (no blur) to `100` (maximum blur).
122
+ *
123
+ * Approximate CSS `backdrop-filter` equivalents:
124
+ *
125
+ * | `blurAmount` | CSS equivalent | Visual feel |
126
+ * |-------------|-------------------------|---------------------|
127
+ * | `5` | `backdrop-blur-sm` (4px) | Subtle hint of blur |
128
+ * | `15` | `backdrop-blur` (8px) | Light frosted glass |
129
+ * | `25` | `backdrop-blur-md` (12px) | Standard card blur |
130
+ * | `50` | `backdrop-blur-xl` (24px) | Heavy frosted glass |
131
+ * | `75` | `backdrop-blur-2xl` | Dense blur |
132
+ * | `100` | `backdrop-blur-3xl` | Maximum blur |
133
+ *
134
+ * **iOS**: Controls `UIViewPropertyAnimator` fraction on `UIBlurEffect`.
135
+ * **Android API 31+**: Maps quadratically to `RenderEffect.createBlurEffect` radius (0–25px).
136
+ * **Android API < 31**: Maps to `RenderScript` Gaussian radius via QmBlurView.
137
+ *
28
138
  * @default 10
29
139
  */
30
140
  blurAmount?: number;
31
141
 
32
142
  /**
33
- * iOS only maps to UIBlurEffectStyle.
34
- * @default 'light'
143
+ * Overlay color composited **on top of** the blur layer.
144
+ *
145
+ * Equivalent to CSS:
146
+ * ```css
147
+ * backdrop-filter: blur(Xpx);
148
+ * background-color: <overlayColor>;
149
+ * ```
150
+ *
151
+ * The alpha channel controls how much of the blur is visible:
152
+ * - `"#00000000"` — fully transparent, pure blur (no tint)
153
+ * - `"#00000040"` — 25% black tint over blur (dark frosted glass)
154
+ * - `"#FFFFFF30"` — 19% white tint over blur (light frosted glass)
155
+ * - `"#000000FF"` — fully opaque black, blur is hidden
156
+ *
157
+ * Supported color formats: `"transparent"`, `"#RGB"`, `"#RRGGBB"`, `"#RRGGBBAA"`
158
+ *
159
+ * **Works on both iOS and Android.**
160
+ *
161
+ * @default `"transparent"` on iOS, `"#00000030"` on Android
35
162
  */
36
- blurType?: BlurType;
163
+ overlayColor?: string;
37
164
 
38
165
  /**
39
- * Overlay color composited ON TOP of the blur layer. Works on iOS AND Android.
166
+ * Fallback solid color shown when blur effects are unavailable.
40
167
  *
41
- * Alpha channel controls how much blur is visible — like CSS:
42
- * backdrop-filter: blur(Xpx) + background-color: overlayColor
168
+ * Shown when:
169
+ * - **iOS**: User has enabled *Reduce Transparency* in Accessibility settings
170
+ * - **Android**: Device API level < 21
43
171
  *
44
- * "#00000000" transparent overlay = pure blur
45
- * "#00000080" semi-transparent black tint over blur
46
- * "#FFFFFFFF" → fully opaque = blur hidden
172
+ * Should be a color that provides sufficient contrast for your UI without
173
+ * the blur effect. Commonly a semi-opaque version of your background color.
47
174
  *
48
- * @default "transparent" on iOS | "#00000030" on Android
175
+ * Supported formats: `"transparent"`, `"#RGB"`, `"#RRGGBB"`, `"#RRGGBBAA"`
176
+ *
177
+ * **Works on both iOS and Android.**
178
+ *
179
+ * @default `"#F2F2F2"`
49
180
  */
50
- overlayColor?: string;
181
+ reducedTransparencyFallbackColor?: string;
182
+
183
+ // ─── iOS-only props ───────────────────────────────────────────────────────
51
184
 
52
185
  /**
53
- * Fallback color when blur is unavailable.
54
- * (Reduce Transparency on iOS, API < 21 on Android)
55
- * @default "#F2F2F2"
186
+ * iOS blur material style.
187
+ *
188
+ * Maps to `UIBlurEffect.Style`. Controls the visual character of the blur —
189
+ * thickness, color tint, and how content shows through.
190
+ *
191
+ * Use adaptive styles (`"systemMaterial"`, `"light"`, `"dark"`) for apps
192
+ * that support both light and dark mode.
193
+ *
194
+ * **iOS only.** Ignored on Android — use `overlayColor` to tint the blur
195
+ * on Android.
196
+ *
197
+ * @default `"light"`
198
+ * @platform ios
56
199
  */
57
- reducedTransparencyFallbackColor?: string;
200
+ blurType?: BlurType;
201
+
202
+ // ─── Android-only props ───────────────────────────────────────────────────
58
203
 
59
204
  /**
60
- * Android only downscale factor for RenderScript blur path (API 21-30).
61
- * Higher = faster performance, slightly softer blur. Range: 1–8.
205
+ * Android API < 31 only RenderScript capture downsample factor.
206
+ *
207
+ * Controls how aggressively the screen is downsampled before the blur
208
+ * kernel is applied. Higher values are faster but produce a softer,
209
+ * less detailed blur.
210
+ *
211
+ * | Value | Resolution captured | Quality | Performance |
212
+ * |-------|---------------------|----------|-------------|
213
+ * | `1` | Full resolution | Sharpest | Slowest |
214
+ * | `4` | 1/16 pixels (default) | Good | Fast |
215
+ * | `8` | 1/64 pixels | Softer | Fastest |
216
+ *
217
+ * On **Android API 31+** this prop is ignored — blur runs at full
218
+ * resolution on the GPU via `RenderEffect`.
219
+ *
220
+ * On **iOS** this prop is ignored entirely.
221
+ *
62
222
  * @default 4
223
+ * @platform android
63
224
  */
64
225
  blurRadius?: number;
226
+
227
+ // ─── Progressive blur props (iOS + Android API 31+) ──────────────────────
228
+
229
+ /**
230
+ * Direction the blur fades across the view.
231
+ *
232
+ * Creates a gradient blur effect — full blur intensity at one edge,
233
+ * fading to no blur (or a different intensity) at the other.
234
+ * Commonly used for:
235
+ * - Sticky/floating headers (blur fades downward)
236
+ * - Bottom sheet scrims (blur fades upward)
237
+ * - Side drawers (blur fades horizontally)
238
+ * - Spotlight effects (radial, full blur at center)
239
+ *
240
+ * Use `progressiveStartIntensity` and `progressiveEndIntensity` to
241
+ * control the intensity at each end of the gradient.
242
+ *
243
+ * **iOS**: True per-pixel variable-radius blur via `CAFilter variableBlur`
244
+ * (same as Apple's Home Screen / Control Center). Falls back to opacity
245
+ * masking if CAFilter is unavailable.
246
+ *
247
+ * **Android API 31+**: Alpha mask gradient over GPU `RenderEffect` blur.
248
+ *
249
+ * **Android API < 31**: Silently ignored — uniform blur is shown.
250
+ *
251
+ * @default `"none"` (uniform blur)
252
+ * @platform ios, android (API 31+)
253
+ */
254
+ progressiveBlurDirection?: ProgressiveBlurDirection;
255
+
256
+ /**
257
+ * Blur intensity at the **start** of the gradient direction. Range: `0.0`–`1.0`.
258
+ *
259
+ * - `1.0` = full blur (at `blurAmount` intensity)
260
+ * - `0.0` = completely unblurred / transparent
261
+ *
262
+ * What "start" means per direction:
263
+ * - `"topToBottom"` → intensity at the **top** edge
264
+ * - `"bottomToTop"` → intensity at the **bottom** edge
265
+ * - `"leftToRight"` → intensity at the **left** edge
266
+ * - `"rightToLeft"` → intensity at the **right** edge
267
+ * - `"radial"` → intensity at the **center**
268
+ *
269
+ * @default 1.0
270
+ * @platform ios, android (API 31+)
271
+ */
272
+ progressiveStartIntensity?: number;
273
+
274
+ /**
275
+ * Blur intensity at the **end** of the gradient direction. Range: `0.0`–`1.0`.
276
+ *
277
+ * - `1.0` = full blur (at `blurAmount` intensity)
278
+ * - `0.0` = completely unblurred / transparent
279
+ *
280
+ * What "end" means per direction:
281
+ * - `"topToBottom"` → intensity at the **bottom** edge
282
+ * - `"bottomToTop"` → intensity at the **top** edge
283
+ * - `"leftToRight"` → intensity at the **right** edge
284
+ * - `"rightToLeft"` → intensity at the **left** edge
285
+ * - `"radial"` → intensity at the **outer edges**
286
+ *
287
+ * @default 0.0
288
+ * @platform ios, android (API 31+)
289
+ */
290
+ progressiveEndIntensity?: number;
291
+
292
+ /**
293
+ * Noise grain overlay strength — adds tactile frosted-glass texture.
294
+ *
295
+ * Overlays a subtle static noise pattern on top of the blur layer.
296
+ * This mimics the micro-texture of real ground glass, making digital
297
+ * blur feel more physical and premium.
298
+ *
299
+ * | Value | Effect |
300
+ * |--------|-------------------------------------------------|
301
+ * | `0` | No noise — clean digital blur |
302
+ * | `0.08` | Subtle grain, barely perceptible (default) |
303
+ * | `0.15` | Noticeable grain (matches Haze library default) |
304
+ * | `0.30` | Heavy grain — strong tactile texture |
305
+ *
306
+ * **iOS**: Drawn as a tiled `CGImage` noise layer with `.overlay` blend mode.
307
+ * **Android API 31+**: Tiled `BitmapShader` drawn over the blur layer.
308
+ * **Android API < 31**: Silently ignored.
309
+ *
310
+ * @default 0.08
311
+ * @platform ios, android (API 31+)
312
+ */
313
+ noiseFactor?: number;
65
314
  }
@@ -1,230 +0,0 @@
1
- package com.blurvibe
2
-
3
- import android.graphics.Bitmap
4
- import android.graphics.Canvas
5
- import android.graphics.Color
6
- import android.graphics.Paint
7
- import android.os.Build
8
- import android.os.Handler
9
- import android.os.HandlerThread
10
- import android.os.Looper
11
- import android.renderscript.Allocation
12
- import android.renderscript.Element
13
- import android.renderscript.RenderScript
14
- import android.renderscript.ScriptIntrinsicBlur
15
- import android.view.Choreographer
16
- import android.view.ViewGroup
17
- import android.view.ViewTreeObserver
18
- import java.util.concurrent.CopyOnWriteArraySet
19
-
20
- /**
21
- * BlurCaptureCoordinator
22
- *
23
- * Singleton per root-view. Owns ONE preDrawListener, ONE bitmap capture, ONE blur pass
24
- * per vsync — shared across ALL BlurVibeViews that point at the same root.
25
- *
26
- * Cost: O(1) per frame regardless of how many BlurVibeViews are mounted.
27
- * Compare to naive per-view: O(N) per frame.
28
- *
29
- * Thread model:
30
- * rootView.draw() → main thread (Android requires this)
31
- * RenderScript blur → workerThread (non-blocking)
32
- * onBlurReady() → main thread via mainHandler.post()
33
- */
34
- internal class BlurCaptureCoordinator private constructor(
35
- private val rootView: ViewGroup
36
- ) {
37
-
38
- // registered BlurVibeViews — thread-safe, iterated on main thread
39
- private val clients = CopyOnWriteArraySet<BlurVibeView>()
40
-
41
- // bitmap pool — allocated once, reused every frame (zero GC)
42
- private var captureBitmap: Bitmap? = null
43
- private var scaledBitmap: Bitmap? = null
44
-
45
- private val capturePaint = Paint(Paint.FILTER_BITMAP_FLAG)
46
-
47
- // worker thread for blur (keeps main thread free)
48
- private val workerThread = HandlerThread("BlurVibeWorker-${System.identityHashCode(rootView)}")
49
- .also { it.start() }
50
- private val workerHandler = Handler(workerThread.looper)
51
- private val mainHandler = Handler(Looper.getMainLooper())
52
-
53
- // RenderScript state (API < 31 only, created lazily on workerThread)
54
- private var rs: RenderScript? = null
55
- private var blurScript: ScriptIntrinsicBlur? = null
56
- private var inputAlloc: Allocation? = null
57
- private var outputAlloc: Allocation? = null
58
-
59
- // blur params
60
- var blurRadius: Float = 8f
61
- set(value) { field = value.coerceIn(1f, 25f) }
62
- var downsampleFactor: Float = DOWNSAMPLE_FACTOR
63
-
64
- // frame gate — at most one capture queued at a time
65
- private var frameScheduled = false
66
- private val frameCallback = Choreographer.FrameCallback {
67
- frameScheduled = false
68
- captureAndBlur()
69
- }
70
-
71
- // ONE preDrawListener for the entire coordinator
72
- private val preDrawListener = ViewTreeObserver.OnPreDrawListener {
73
- if (!frameScheduled) {
74
- frameScheduled = true
75
- Choreographer.getInstance().postFrameCallback(frameCallback)
76
- }
77
- true // never block the draw pass
78
- }
79
-
80
- // ── Init / destroy ────────────────────────────────────────────────────────
81
-
82
- init {
83
- rootView.viewTreeObserver.addOnPreDrawListener(preDrawListener)
84
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
85
- workerHandler.post { initRenderScript() }
86
- }
87
- }
88
-
89
- private fun destroy() {
90
- rootView.viewTreeObserver.removeOnPreDrawListener(preDrawListener)
91
- Choreographer.getInstance().removeFrameCallback(frameCallback)
92
- cache.remove(rootView)
93
- workerHandler.post {
94
- inputAlloc?.destroy(); inputAlloc = null
95
- outputAlloc?.destroy(); outputAlloc = null
96
- blurScript?.destroy(); blurScript = null
97
- rs?.destroy(); rs = null
98
- captureBitmap?.recycle(); captureBitmap = null
99
- scaledBitmap?.recycle(); scaledBitmap = null
100
- workerThread.quitSafely()
101
- }
102
- }
103
-
104
- // ── Registration ──────────────────────────────────────────────────────────
105
-
106
- fun register(view: BlurVibeView) {
107
- clients.add(view)
108
- // deliver cached result immediately so view doesn't flash blank
109
- scaledBitmap?.takeIf { !it.isRecycled }?.let { view.onBlurReady(it) }
110
- }
111
-
112
- fun unregister(view: BlurVibeView) {
113
- clients.remove(view)
114
- if (clients.isEmpty()) destroy()
115
- }
116
-
117
- // ── Capture + blur pipeline ───────────────────────────────────────────────
118
-
119
- private fun captureAndBlur() {
120
- if (clients.isEmpty()) return
121
- val w = rootView.width; if (w <= 0) return
122
- val h = rootView.height; if (h <= 0) return
123
-
124
- val factor = downsampleFactor
125
- val scaledW = (w / factor).toInt().coerceAtLeast(1)
126
- val scaledH = (h / factor).toInt().coerceAtLeast(1)
127
-
128
- // allocate / reuse bitmaps
129
- val capture = reuseBitmap(captureBitmap, w, h).also { captureBitmap = it }
130
- val scaled = reuseBitmap(scaledBitmap, scaledW, scaledH).also { scaledBitmap = it }
131
-
132
- // ① capture on main thread (required by Android)
133
- try {
134
- val c = Canvas(capture)
135
- c.drawColor(Color.TRANSPARENT, android.graphics.PorterDuff.Mode.CLEAR)
136
- rootView.draw(c)
137
- } catch (_: Exception) { return }
138
-
139
- val radius = blurRadius
140
- val captureRef = capture
141
- val scaledRef = scaled
142
-
143
- // ② blur on worker thread
144
- workerHandler.post {
145
- // downsample
146
- Canvas(scaledRef).drawBitmap(
147
- captureRef,
148
- android.graphics.Rect(0, 0, captureRef.width, captureRef.height),
149
- android.graphics.Rect(0, 0, scaledRef.width, scaledRef.height),
150
- capturePaint
151
- )
152
-
153
- // blur
154
- val blurred = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
155
- blurSoftware(scaledRef, radius) // BlurMaskFilter — fast enough at small size
156
- } else {
157
- blurRenderScript(scaledRef, radius) ?: blurSoftware(scaledRef, radius)
158
- }
159
-
160
- // ③ deliver to all clients on main thread
161
- mainHandler.post {
162
- clients.forEach { it.onBlurReady(blurred) }
163
- }
164
- }
165
- }
166
-
167
- // ── Blur implementations ──────────────────────────────────────────────────
168
-
169
- private fun initRenderScript() {
170
- try {
171
- rs = RenderScript.create(rootView.context)
172
- blurScript = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs))
173
- } catch (_: Exception) {}
174
- }
175
-
176
- private fun blurRenderScript(src: Bitmap, radius: Float): Bitmap? {
177
- val r = this.rs ?: return null
178
- val sc = this.blurScript ?: return null
179
- return try {
180
- val inA = reuseAlloc(inputAlloc, src, r).also { inputAlloc = it }
181
- val outA = reuseAlloc(outputAlloc, src, r).also { outputAlloc = it }
182
- inA.copyFrom(src)
183
- sc.setRadius(radius)
184
- sc.setInput(inA)
185
- sc.forEach(outA)
186
- val out = Bitmap.createBitmap(src.width, src.height, Bitmap.Config.ARGB_8888)
187
- outA.copyTo(out)
188
- out
189
- } catch (_: Exception) { null }
190
- }
191
-
192
- private fun blurSoftware(src: Bitmap, radius: Float): Bitmap {
193
- val out = Bitmap.createBitmap(src.width, src.height, Bitmap.Config.ARGB_8888)
194
- val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
195
- maskFilter = android.graphics.BlurMaskFilter(radius, android.graphics.BlurMaskFilter.Blur.NORMAL)
196
- }
197
- Canvas(out).drawBitmap(src, 0f, 0f, paint)
198
- return out
199
- }
200
-
201
- // ── Bitmap / Allocation helpers ───────────────────────────────────────────
202
-
203
- private fun reuseBitmap(existing: Bitmap?, w: Int, h: Int): Bitmap {
204
- if (existing != null && !existing.isRecycled
205
- && existing.width == w && existing.height == h) return existing
206
- existing?.recycle()
207
- return Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
208
- }
209
-
210
- private fun reuseAlloc(existing: Allocation?, src: Bitmap, rs: RenderScript): Allocation {
211
- if (existing != null
212
- && existing.type.x == src.width
213
- && existing.type.y == src.height) return existing
214
- existing?.destroy()
215
- return Allocation.createFromBitmap(rs, src,
216
- Allocation.MipmapControl.MIPMAP_NONE, Allocation.USAGE_SCRIPT)
217
- }
218
-
219
- // ── Singleton cache ───────────────────────────────────────────────────────
220
-
221
- companion object {
222
- /** Global downsample factor. Higher = faster + softer. Range 2–16. */
223
- var DOWNSAMPLE_FACTOR: Float = 8f
224
-
225
- private val cache = HashMap<ViewGroup, BlurCaptureCoordinator>()
226
-
227
- fun forRoot(rootView: ViewGroup): BlurCaptureCoordinator =
228
- cache.getOrPut(rootView) { BlurCaptureCoordinator(rootView) }
229
- }
230
- }