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.
- package/README.md +374 -181
- package/android/build.gradle +2 -0
- package/android/src/main/java/com/blurvibe/BlurVibeView.kt +189 -161
- package/android/src/main/java/com/blurvibe/BlurVibeViewApi31.kt +448 -0
- package/android/src/main/java/com/blurvibe/BlurVibeViewManager.kt +74 -22
- package/ios/BlurVibeView.swift +28 -27
- package/ios/BlurVibeViewManager.m +9 -9
- package/ios/Views/BlurVibeSwiftUIView.swift +109 -16
- package/ios/Views/ProgressiveBlurView.swift +255 -0
- package/lib/commonjs/BlurVibeViewNativeComponent.ts +10 -16
- package/lib/commonjs/BlurView.js +34 -7
- package/lib/commonjs/BlurView.js.map +1 -1
- package/lib/module/BlurVibeViewNativeComponent.ts +10 -16
- package/lib/module/BlurView.js +34 -7
- package/lib/module/BlurView.js.map +1 -1
- package/lib/typescript/commonjs/src/BlurVibeViewNativeComponent.d.ts +4 -14
- package/lib/typescript/commonjs/src/BlurVibeViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/BlurView.d.ts +27 -8
- package/lib/typescript/commonjs/src/BlurView.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/types.d.ts +236 -18
- package/lib/typescript/commonjs/src/types.d.ts.map +1 -1
- package/lib/typescript/module/src/BlurVibeViewNativeComponent.d.ts +4 -14
- package/lib/typescript/module/src/BlurVibeViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/module/src/BlurView.d.ts +27 -8
- package/lib/typescript/module/src/BlurView.d.ts.map +1 -1
- package/lib/typescript/module/src/types.d.ts +236 -18
- package/lib/typescript/module/src/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/BlurVibeViewNativeComponent.ts +10 -16
- package/src/BlurView.tsx +34 -7
- package/src/types.ts +267 -18
- 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
|
|
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
|
-
*
|
|
34
|
-
*
|
|
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
|
-
|
|
163
|
+
overlayColor?: string;
|
|
37
164
|
|
|
38
165
|
/**
|
|
39
|
-
*
|
|
166
|
+
* Fallback solid color shown when blur effects are unavailable.
|
|
40
167
|
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
168
|
+
* Shown when:
|
|
169
|
+
* - **iOS**: User has enabled *Reduce Transparency* in Accessibility settings
|
|
170
|
+
* - **Android**: Device API level < 21
|
|
43
171
|
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
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
|
-
*
|
|
175
|
+
* Supported formats: `"transparent"`, `"#RGB"`, `"#RRGGBB"`, `"#RRGGBBAA"`
|
|
176
|
+
*
|
|
177
|
+
* **Works on both iOS and Android.**
|
|
178
|
+
*
|
|
179
|
+
* @default `"#F2F2F2"`
|
|
49
180
|
*/
|
|
50
|
-
|
|
181
|
+
reducedTransparencyFallbackColor?: string;
|
|
182
|
+
|
|
183
|
+
// ─── iOS-only props ───────────────────────────────────────────────────────
|
|
51
184
|
|
|
52
185
|
/**
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
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
|
-
|
|
200
|
+
blurType?: BlurType;
|
|
201
|
+
|
|
202
|
+
// ─── Android-only props ───────────────────────────────────────────────────
|
|
58
203
|
|
|
59
204
|
/**
|
|
60
|
-
* Android
|
|
61
|
-
*
|
|
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
|
-
}
|