@sbaiahmed1/react-native-blur 4.6.2 → 4.6.3-beta.0
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/android/build.gradle +1 -1
- package/android/src/main/java/com/sbaiahmed1/reactnativeblur/BlurType.kt +16 -25
- package/android/src/main/java/com/sbaiahmed1/reactnativeblur/ReactNativeBlurView.kt +82 -154
- package/android/src/main/java/com/sbaiahmed1/reactnativeblur/ReactNativeBlurViewManager.kt +20 -0
- package/android/src/main/java/com/sbaiahmed1/reactnativeblur/ReactNativeProgressiveBlurView.kt +1 -13
- package/ios/Helpers/BlurStyleHelpers.swift +17 -0
- package/ios/Views/AdvancedBlurView.swift +8 -5
- package/ios/Views/BasicColoredView.swift +1 -19
- package/ios/Views/BlurEffectView.swift +19 -19
- package/ios/Views/LiquidGlassContainerView.swift +4 -7
- package/ios/Views/ProgressiveBlurView.swift +8 -3
- package/ios/Views/VariableBlurView.swift +0 -11
- package/ios/Views/VibrancyEffectView.swift +3 -12
- package/package.json +1 -1
package/android/build.gradle
CHANGED
|
@@ -74,7 +74,7 @@ def kotlin_version = getExtOrDefault("kotlinVersion")
|
|
|
74
74
|
dependencies {
|
|
75
75
|
implementation "com.facebook.react:react-android"
|
|
76
76
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
|
77
|
-
implementation 'com.qmdeve
|
|
77
|
+
implementation 'com.github.qmdeve:qmblurview:v1.1.5'
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
react {
|
|
@@ -1,64 +1,55 @@
|
|
|
1
1
|
package com.sbaiahmed1.reactnativeblur
|
|
2
2
|
|
|
3
|
-
import android.content.res.Configuration
|
|
4
3
|
import android.graphics.Color
|
|
5
4
|
|
|
6
|
-
/**
|
|
7
|
-
* Enum representing different blur types with their corresponding overlay colors.
|
|
8
|
-
* Maps iOS blur types to Android overlay colors to approximate the visual appearance.
|
|
9
|
-
*/
|
|
10
5
|
enum class BlurType(val overlayColor: Int) {
|
|
11
6
|
XLIGHT(Color.argb(140, 240, 240, 240)),
|
|
12
7
|
LIGHT(Color.argb(42, 255, 255, 255)),
|
|
13
8
|
DARK(Color.argb(120, 26, 22, 22)),
|
|
14
9
|
EXTRA_DARK(Color.argb(160, 35, 35, 35)),
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
PROMINENT_DARK(Color.argb(140, 28, 28, 30)),
|
|
10
|
+
REGULAR(Color.argb(35, 255, 255, 255)),
|
|
11
|
+
PROMINENT(Color.argb(140, 240, 240, 240)),
|
|
12
|
+
SYSTEM_ULTRA_THIN_MATERIAL(Color.argb(75, 240, 240, 240)),
|
|
19
13
|
SYSTEM_ULTRA_THIN_MATERIAL_LIGHT(Color.argb(75, 240, 240, 240)),
|
|
20
14
|
SYSTEM_ULTRA_THIN_MATERIAL_DARK(Color.argb(65, 40, 40, 40)),
|
|
15
|
+
SYSTEM_THIN_MATERIAL(Color.argb(102, 240, 240, 240)),
|
|
21
16
|
SYSTEM_THIN_MATERIAL_LIGHT(Color.argb(102, 240, 240, 240)),
|
|
22
17
|
SYSTEM_THIN_MATERIAL_DARK(Color.argb(102, 35, 35, 35)),
|
|
18
|
+
SYSTEM_MATERIAL(Color.argb(140, 245, 245, 245)),
|
|
23
19
|
SYSTEM_MATERIAL_LIGHT(Color.argb(140, 245, 245, 245)),
|
|
24
20
|
SYSTEM_MATERIAL_DARK(Color.argb(215, 65, 60, 60)),
|
|
21
|
+
SYSTEM_THICK_MATERIAL(Color.argb(210, 248, 248, 248)),
|
|
25
22
|
SYSTEM_THICK_MATERIAL_LIGHT(Color.argb(210, 248, 248, 248)),
|
|
26
23
|
SYSTEM_THICK_MATERIAL_DARK(Color.argb(160, 35, 35, 35)),
|
|
24
|
+
SYSTEM_CHROME_MATERIAL(Color.argb(165, 248, 248, 248)),
|
|
27
25
|
SYSTEM_CHROME_MATERIAL_LIGHT(Color.argb(165, 248, 248, 248)),
|
|
28
26
|
SYSTEM_CHROME_MATERIAL_DARK(Color.argb(100, 32, 32, 32));
|
|
29
27
|
|
|
30
28
|
companion object {
|
|
31
|
-
|
|
32
|
-
* Get BlurType from string, with fallback to LIGHT for unknown types.
|
|
33
|
-
* Uses the provided configuration to determine if dark mode is active for
|
|
34
|
-
* appropriate defaults.
|
|
35
|
-
*/
|
|
36
|
-
fun fromString(type: String, configuration: Configuration): BlurType {
|
|
37
|
-
val isDarkMode = (configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES
|
|
38
|
-
|
|
29
|
+
fun fromString(type: String): BlurType {
|
|
39
30
|
return when (type.lowercase()) {
|
|
40
31
|
"xlight" -> XLIGHT
|
|
41
32
|
"light" -> LIGHT
|
|
42
33
|
"dark" -> DARK
|
|
43
34
|
"extradark" -> EXTRA_DARK
|
|
44
|
-
"regular" ->
|
|
45
|
-
"prominent" ->
|
|
46
|
-
"systemultrathinmaterial" ->
|
|
35
|
+
"regular" -> REGULAR
|
|
36
|
+
"prominent" -> PROMINENT
|
|
37
|
+
"systemultrathinmaterial" -> SYSTEM_ULTRA_THIN_MATERIAL
|
|
47
38
|
"systemultrathinmateriallight" -> SYSTEM_ULTRA_THIN_MATERIAL_LIGHT
|
|
48
39
|
"systemultrathinmaterialdark" -> SYSTEM_ULTRA_THIN_MATERIAL_DARK
|
|
49
|
-
"systemthinmaterial" ->
|
|
40
|
+
"systemthinmaterial" -> SYSTEM_THIN_MATERIAL
|
|
50
41
|
"systemthinmateriallight" -> SYSTEM_THIN_MATERIAL_LIGHT
|
|
51
42
|
"systemthinmaterialdark" -> SYSTEM_THIN_MATERIAL_DARK
|
|
52
|
-
"systemmaterial" ->
|
|
43
|
+
"systemmaterial" -> SYSTEM_MATERIAL
|
|
53
44
|
"systemmateriallight" -> SYSTEM_MATERIAL_LIGHT
|
|
54
45
|
"systemmaterialdark" -> SYSTEM_MATERIAL_DARK
|
|
55
|
-
"systemthickmaterial" ->
|
|
46
|
+
"systemthickmaterial" -> SYSTEM_THICK_MATERIAL
|
|
56
47
|
"systemthickmateriallight" -> SYSTEM_THICK_MATERIAL_LIGHT
|
|
57
48
|
"systemthickmaterialdark" -> SYSTEM_THICK_MATERIAL_DARK
|
|
58
|
-
"systemchromematerial" ->
|
|
49
|
+
"systemchromematerial" -> SYSTEM_CHROME_MATERIAL
|
|
59
50
|
"systemchromemateriallight" -> SYSTEM_CHROME_MATERIAL_LIGHT
|
|
60
51
|
"systemchromematerialdark" -> SYSTEM_CHROME_MATERIAL_DARK
|
|
61
|
-
else -> XLIGHT
|
|
52
|
+
else -> XLIGHT
|
|
62
53
|
}
|
|
63
54
|
}
|
|
64
55
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
package com.sbaiahmed1.reactnativeblur
|
|
2
2
|
|
|
3
3
|
import android.content.Context
|
|
4
|
-
import android.content.res.Configuration
|
|
5
4
|
import android.graphics.Color
|
|
6
5
|
import android.graphics.Outline
|
|
6
|
+
import android.graphics.Path
|
|
7
|
+
import android.os.Build
|
|
7
8
|
import android.util.AttributeSet
|
|
8
9
|
import android.util.Log
|
|
9
10
|
import android.util.TypedValue
|
|
@@ -32,7 +33,11 @@ class ReactNativeBlurView : BlurViewGroup {
|
|
|
32
33
|
private var currentBlurRadius = DEFAULT_BLUR_RADIUS
|
|
33
34
|
private var currentOverlayColor = Color.TRANSPARENT
|
|
34
35
|
private var currentBlurRounds = DEFAULT_BLUR_ROUNDS
|
|
35
|
-
private var
|
|
36
|
+
private var borderRadius = 0f
|
|
37
|
+
private var borderTopLeftRadius = -1f
|
|
38
|
+
private var borderTopRightRadius = -1f
|
|
39
|
+
private var borderBottomLeftRadius = -1f
|
|
40
|
+
private var borderBottomRightRadius = -1f
|
|
36
41
|
private var glassTintColor: Int = Color.TRANSPARENT
|
|
37
42
|
private var glassOpacity: Float = 1.0f
|
|
38
43
|
private var viewType: String = "blur"
|
|
@@ -48,7 +53,6 @@ class ReactNativeBlurView : BlurViewGroup {
|
|
|
48
53
|
private const val DEFAULT_BLUR_ROUNDS = 5
|
|
49
54
|
private const val DEBUG = false
|
|
50
55
|
|
|
51
|
-
// Cross-platform blur amount constants
|
|
52
56
|
private const val MIN_BLUR_AMOUNT = 0f
|
|
53
57
|
private const val MAX_BLUR_AMOUNT = 100f
|
|
54
58
|
|
|
@@ -66,12 +70,6 @@ class ReactNativeBlurView : BlurViewGroup {
|
|
|
66
70
|
Log.e(TAG, message, throwable)
|
|
67
71
|
}
|
|
68
72
|
|
|
69
|
-
/**
|
|
70
|
-
* Maps blur amount (0-100) to Android blur radius (0-25).
|
|
71
|
-
* This ensures cross-platform consistency while respecting Android's limitations.
|
|
72
|
-
* @param amount The blur amount from 0-100
|
|
73
|
-
* @return The corresponding blur radius from 0-25
|
|
74
|
-
*/
|
|
75
73
|
private fun mapBlurAmountToRadius(amount: Float): Float {
|
|
76
74
|
val clampedAmount = amount.coerceIn(MIN_BLUR_AMOUNT, MAX_BLUR_AMOUNT)
|
|
77
75
|
return (clampedAmount / MAX_BLUR_AMOUNT) * MAX_BLUR_RADIUS
|
|
@@ -86,11 +84,6 @@ class ReactNativeBlurView : BlurViewGroup {
|
|
|
86
84
|
setupView()
|
|
87
85
|
}
|
|
88
86
|
|
|
89
|
-
/**
|
|
90
|
-
* Initial view setup in constructor - only sets up visual defaults.
|
|
91
|
-
* Blur initialization is deferred to onAttachedToWindow to ensure the
|
|
92
|
-
* view hierarchy is fully mounted, preventing flickering and wrong frame capture.
|
|
93
|
-
*/
|
|
94
87
|
private fun setupView() {
|
|
95
88
|
super.setBackgroundColor(currentOverlayColor)
|
|
96
89
|
clipChildren = true
|
|
@@ -99,45 +92,24 @@ class ReactNativeBlurView : BlurViewGroup {
|
|
|
99
92
|
super.setDownsampleFactor(6.0F)
|
|
100
93
|
}
|
|
101
94
|
|
|
102
|
-
/**
|
|
103
|
-
* Called when the view is attached to a window.
|
|
104
|
-
* After QmBlurView's onAttachedToWindow sets the decor view as blur root,
|
|
105
|
-
* we use reflection to redirect it to the nearest Screen ancestor.
|
|
106
|
-
* This scopes the blur capture to just the current screen, preventing
|
|
107
|
-
* navigation transition artifacts.
|
|
108
|
-
*/
|
|
109
95
|
override fun onAttachedToWindow() {
|
|
110
96
|
super.onAttachedToWindow()
|
|
111
97
|
|
|
112
98
|
if (isBlurInitialized) return
|
|
113
99
|
|
|
114
|
-
// Immediately try to swap blur root and initialize.
|
|
115
|
-
// We avoid posting a runnable to prevent the 1-second delay artifact.
|
|
116
|
-
// If the parent hierarchy is not ready yet (unlikely in onAttachedToWindow),
|
|
117
|
-
// we could fall back to post, but for now we prioritize immediate execution.
|
|
118
100
|
swapBlurRootToScreenAncestor()
|
|
119
101
|
initializeBlur()
|
|
120
102
|
}
|
|
121
103
|
|
|
122
|
-
/**
|
|
123
|
-
* Uses reflection to redirect QmBlurView's internal blur capture root
|
|
124
|
-
* from the activity decor view to the nearest react-native-screens Screen ancestor.
|
|
125
|
-
*
|
|
126
|
-
* Reflection path: BlurViewGroup.mBaseBlurViewGroup -> BaseBlurViewGroup.mDecorView
|
|
127
|
-
* Also moves the OnPreDrawListener from the old root to the new one.
|
|
128
|
-
*/
|
|
129
104
|
private fun swapBlurRootToScreenAncestor() {
|
|
130
|
-
// Pinned to QmBlurView 1.1.4 – depends on: mBaseBlurViewGroup, mDecorView, preDrawListener, mDifferentRoot, mForceRedraw
|
|
131
105
|
val newRoot = findOptimalBlurRoot() ?: return
|
|
132
106
|
|
|
133
107
|
try {
|
|
134
|
-
// Step 1: Get BlurViewGroup's private mBaseBlurViewGroup field
|
|
135
108
|
val blurViewGroupClass = BlurViewGroup::class.java
|
|
136
109
|
val baseField = blurViewGroupClass.getDeclaredField("mBaseBlurViewGroup")
|
|
137
110
|
baseField.isAccessible = true
|
|
138
111
|
val baseBlurViewGroup = baseField.get(this) ?: return
|
|
139
112
|
|
|
140
|
-
// Step 2: Get BaseBlurViewGroup's private fields
|
|
141
113
|
val baseClass = BaseBlurViewGroup::class.java
|
|
142
114
|
|
|
143
115
|
val decorViewField = baseClass.getDeclaredField("mDecorView")
|
|
@@ -156,25 +128,20 @@ class ReactNativeBlurView : BlurViewGroup {
|
|
|
156
128
|
}
|
|
157
129
|
|
|
158
130
|
if (preDrawListener != null && oldDecorView != null) {
|
|
159
|
-
// Step 3: Remove listener from old root's ViewTreeObserver
|
|
160
131
|
try {
|
|
161
132
|
oldDecorView.viewTreeObserver.removeOnPreDrawListener(preDrawListener)
|
|
162
133
|
} catch (e: Exception) {
|
|
163
134
|
logDebug("Could not remove old pre-draw listener: ${e.message}")
|
|
164
135
|
}
|
|
165
136
|
|
|
166
|
-
// Step 4: Set new root as mDecorView
|
|
167
137
|
decorViewField.set(baseBlurViewGroup, newRoot)
|
|
168
138
|
|
|
169
|
-
// Step 5: Add listener to new root's ViewTreeObserver
|
|
170
139
|
newRoot.viewTreeObserver.addOnPreDrawListener(preDrawListener)
|
|
171
140
|
|
|
172
|
-
// Step 6: Update mDifferentRoot flag
|
|
173
141
|
val differentRootField = baseClass.getDeclaredField("mDifferentRoot")
|
|
174
142
|
differentRootField.isAccessible = true
|
|
175
143
|
differentRootField.setBoolean(baseBlurViewGroup, newRoot.rootView != this.rootView)
|
|
176
144
|
|
|
177
|
-
// Step 7: Force a redraw
|
|
178
145
|
val forceRedrawField = baseClass.getDeclaredField("mForceRedraw")
|
|
179
146
|
forceRedrawField.isAccessible = true
|
|
180
147
|
forceRedrawField.setBoolean(baseBlurViewGroup, true)
|
|
@@ -188,27 +155,10 @@ class ReactNativeBlurView : BlurViewGroup {
|
|
|
188
155
|
}
|
|
189
156
|
}
|
|
190
157
|
|
|
191
|
-
/**
|
|
192
|
-
* Finds the optimal view to use as blur capture root.
|
|
193
|
-
*
|
|
194
|
-
* Priority:
|
|
195
|
-
* 1. Nearest react-native-screens Screen ancestor — scopes blur to the current
|
|
196
|
-
* screen and prevents capturing navigation transition artifacts.
|
|
197
|
-
* 2. Nearest ReactRootView ancestor — scopes blur to the React Native root when
|
|
198
|
-
* the component is not inside a Screen (e.g. plain View hierarchies). Without
|
|
199
|
-
* this fallback, QmBlurView defaults to the activity decor view and blurs the
|
|
200
|
-
* entire screen instead of just the component area (issue #89).
|
|
201
|
-
* 3. null — returned for modals, which intentionally need to blur content from
|
|
202
|
-
* the main activity window (decor view is correct there).
|
|
203
|
-
*/
|
|
204
158
|
private fun findOptimalBlurRoot(): ViewGroup? {
|
|
205
159
|
return findNearestScreenAncestor() ?: findNearestReactRootView()
|
|
206
160
|
}
|
|
207
161
|
|
|
208
|
-
/**
|
|
209
|
-
* Walks up the view hierarchy looking for react-native-screens Screen components
|
|
210
|
-
* using class name detection to avoid hard dependencies on react-native-screens.
|
|
211
|
-
*/
|
|
212
162
|
private fun findNearestScreenAncestor(): ViewGroup? {
|
|
213
163
|
var currentParent = this.parent
|
|
214
164
|
while (currentParent != null) {
|
|
@@ -220,11 +170,6 @@ class ReactNativeBlurView : BlurViewGroup {
|
|
|
220
170
|
return null
|
|
221
171
|
}
|
|
222
172
|
|
|
223
|
-
/**
|
|
224
|
-
* Walks up the view hierarchy looking for the React Native root view.
|
|
225
|
-
* Used as a fallback when no Screen ancestor exists, to scope the blur
|
|
226
|
-
* capture to the RN root rather than the full activity decor view.
|
|
227
|
-
*/
|
|
228
173
|
private fun findNearestReactRootView(): ViewGroup? {
|
|
229
174
|
var currentParent = this.parent
|
|
230
175
|
while (currentParent != null) {
|
|
@@ -236,11 +181,6 @@ class ReactNativeBlurView : BlurViewGroup {
|
|
|
236
181
|
return null
|
|
237
182
|
}
|
|
238
183
|
|
|
239
|
-
/**
|
|
240
|
-
* Initialize the blur view with current settings.
|
|
241
|
-
* Called after the view is attached and the blur root has been swapped.
|
|
242
|
-
* Guarded by isBlurInitialized to prevent duplicate setup.
|
|
243
|
-
*/
|
|
244
184
|
private fun initializeBlur() {
|
|
245
185
|
if (isBlurInitialized) return
|
|
246
186
|
|
|
@@ -256,20 +196,11 @@ class ReactNativeBlurView : BlurViewGroup {
|
|
|
256
196
|
}
|
|
257
197
|
}
|
|
258
198
|
|
|
259
|
-
/**
|
|
260
|
-
* Called when the view is detached from a window.
|
|
261
|
-
* Performs cleanup to prevent memory leaks and resets initialization state
|
|
262
|
-
* so blur is re-initialized on next attach (e.g. navigation transitions).
|
|
263
|
-
*/
|
|
264
199
|
override fun onDetachedFromWindow() {
|
|
265
200
|
super.onDetachedFromWindow()
|
|
266
201
|
cleanup()
|
|
267
202
|
}
|
|
268
203
|
|
|
269
|
-
/**
|
|
270
|
-
* Cleanup method to reset state.
|
|
271
|
-
* Helps prevent memory leaks and ensures clean state.
|
|
272
|
-
*/
|
|
273
204
|
fun cleanup() {
|
|
274
205
|
isBlurInitialized = false
|
|
275
206
|
initRunnable?.let { removeCallbacks(it) }
|
|
@@ -277,10 +208,6 @@ class ReactNativeBlurView : BlurViewGroup {
|
|
|
277
208
|
logDebug("View cleaned up")
|
|
278
209
|
}
|
|
279
210
|
|
|
280
|
-
/**
|
|
281
|
-
* Set the blur amount with cross-platform mapping.
|
|
282
|
-
* @param amount The blur amount value (0-100), will be mapped to Android's 0-25 radius range
|
|
283
|
-
*/
|
|
284
211
|
fun setBlurAmount(amount: Float) {
|
|
285
212
|
currentBlurRadius = mapBlurAmountToRadius(amount)
|
|
286
213
|
logDebug("setBlurAmount: $amount -> $currentBlurRadius (mapped from 0-100 to 0-25 range)")
|
|
@@ -292,10 +219,6 @@ class ReactNativeBlurView : BlurViewGroup {
|
|
|
292
219
|
}
|
|
293
220
|
}
|
|
294
221
|
|
|
295
|
-
/**
|
|
296
|
-
* Set the number of blur rounds.
|
|
297
|
-
* @param rounds The number of blur rounds (1-15)
|
|
298
|
-
*/
|
|
299
222
|
fun setRounds(rounds: Int) {
|
|
300
223
|
val blurRounds = rounds.coerceIn(1, 15)
|
|
301
224
|
currentBlurRounds = blurRounds
|
|
@@ -314,7 +237,7 @@ class ReactNativeBlurView : BlurViewGroup {
|
|
|
314
237
|
*/
|
|
315
238
|
fun setBlurType(type: String) {
|
|
316
239
|
currentBlurType = type
|
|
317
|
-
val blurType = BlurType.fromString(type
|
|
240
|
+
val blurType = BlurType.fromString(type)
|
|
318
241
|
currentOverlayColor = blurType.overlayColor
|
|
319
242
|
logDebug("setBlurType: $type -> ${blurType.name}")
|
|
320
243
|
|
|
@@ -328,9 +251,6 @@ class ReactNativeBlurView : BlurViewGroup {
|
|
|
328
251
|
|
|
329
252
|
/**
|
|
330
253
|
* Set the glass tint color for liquid glass effect.
|
|
331
|
-
* @param color The color string in hex format (e.g., "#FF0000") or null to clear
|
|
332
|
-
*/
|
|
333
|
-
fun setGlassTintColor(color: String?) {
|
|
334
254
|
color?.let {
|
|
335
255
|
try {
|
|
336
256
|
glassTintColor = it.toColorInt()
|
|
@@ -347,58 +267,37 @@ class ReactNativeBlurView : BlurViewGroup {
|
|
|
347
267
|
}
|
|
348
268
|
}
|
|
349
269
|
|
|
350
|
-
/**
|
|
351
|
-
* Set the glass opacity for liquid glass effect.
|
|
352
|
-
* @param opacity The opacity value (0.0 to 1.0)
|
|
353
|
-
*/
|
|
354
270
|
fun setGlassOpacity(opacity: Float) {
|
|
355
271
|
glassOpacity = opacity.coerceIn(0.0f, 1.0f)
|
|
356
272
|
logDebug("setGlassOpacity: $opacity")
|
|
357
273
|
updateGlassEffect()
|
|
358
274
|
}
|
|
359
275
|
|
|
360
|
-
/**
|
|
361
|
-
* Set the view type (blur or liquidGlass).
|
|
362
|
-
* @param type The view type string
|
|
363
|
-
*/
|
|
364
276
|
fun setType(type: String) {
|
|
365
277
|
viewType = type
|
|
366
278
|
logDebug("setType: $type")
|
|
367
279
|
updateViewType()
|
|
368
280
|
}
|
|
369
281
|
|
|
370
|
-
/**
|
|
371
|
-
* Set the view type (blur or liquidGlass).
|
|
372
|
-
* @param isInteractive The view type string
|
|
373
|
-
*/
|
|
374
282
|
fun setIsInteractive(isInteractive: Boolean) {
|
|
375
283
|
logDebug("setType: $isInteractive")
|
|
376
284
|
}
|
|
377
285
|
|
|
378
|
-
/**
|
|
379
|
-
* Set the glass type for liquid glass effect.
|
|
380
|
-
* @param type The glass type string
|
|
381
|
-
*/
|
|
382
286
|
fun setGlassType(type: String) {
|
|
383
287
|
glassType = type
|
|
384
288
|
logDebug("setGlassType: $type")
|
|
385
289
|
updateGlassEffect()
|
|
386
290
|
}
|
|
387
291
|
|
|
388
|
-
/**
|
|
389
|
-
* Update the glass effect based on current glass properties.
|
|
390
|
-
*/
|
|
391
292
|
private fun updateGlassEffect() {
|
|
392
293
|
if (viewType == "liquidGlass") {
|
|
393
294
|
try {
|
|
394
|
-
// Apply glass tint with opacity
|
|
395
295
|
val glassColor = Color.argb(
|
|
396
296
|
(glassOpacity * 255).toInt(),
|
|
397
297
|
Color.red(glassTintColor),
|
|
398
298
|
Color.green(glassTintColor),
|
|
399
299
|
Color.blue(glassTintColor)
|
|
400
300
|
)
|
|
401
|
-
// Use QmBlurView's setOverlayColor method
|
|
402
301
|
super.setOverlayColor(glassColor)
|
|
403
302
|
logDebug("Applied glass effect: color=$glassColor, opacity=$glassOpacity")
|
|
404
303
|
} catch (e: Exception) {
|
|
@@ -407,16 +306,12 @@ class ReactNativeBlurView : BlurViewGroup {
|
|
|
407
306
|
}
|
|
408
307
|
}
|
|
409
308
|
|
|
410
|
-
/**
|
|
411
|
-
* Update the view type and apply appropriate effects.
|
|
412
|
-
*/
|
|
413
309
|
private fun updateViewType() {
|
|
414
310
|
when (viewType) {
|
|
415
311
|
"liquidGlass" -> {
|
|
416
312
|
updateGlassEffect()
|
|
417
313
|
}
|
|
418
314
|
"blur" -> {
|
|
419
|
-
// Restore original blur overlay color
|
|
420
315
|
try {
|
|
421
316
|
super.setBackgroundColor(currentOverlayColor)
|
|
422
317
|
super.setOverlayColor(currentOverlayColor)
|
|
@@ -427,40 +322,90 @@ class ReactNativeBlurView : BlurViewGroup {
|
|
|
427
322
|
}
|
|
428
323
|
}
|
|
429
324
|
|
|
430
|
-
/**
|
|
431
|
-
* Set the border radius from React Native StyleSheet.
|
|
432
|
-
* React Native provides values in logical pixels (dp), which we convert for the native view.
|
|
433
|
-
* @param radius The border radius value in dp
|
|
434
|
-
*/
|
|
435
325
|
fun setBorderRadius(radius: Float) {
|
|
436
|
-
|
|
326
|
+
borderRadius = radius
|
|
437
327
|
logDebug("setBorderRadius: $radius dp")
|
|
438
328
|
updateCornerRadius()
|
|
439
329
|
}
|
|
440
330
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
331
|
+
fun setBorderTopLeftRadius(radius: Float) {
|
|
332
|
+
borderTopLeftRadius = radius
|
|
333
|
+
logDebug("setBorderTopLeftRadius: $radius dp")
|
|
334
|
+
updateCornerRadius()
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
fun setBorderTopRightRadius(radius: Float) {
|
|
338
|
+
borderTopRightRadius = radius
|
|
339
|
+
logDebug("setBorderTopRightRadius: $radius dp")
|
|
340
|
+
updateCornerRadius()
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
fun setBorderBottomLeftRadius(radius: Float) {
|
|
344
|
+
borderBottomLeftRadius = radius
|
|
345
|
+
logDebug("setBorderBottomLeftRadius: $radius dp")
|
|
346
|
+
updateCornerRadius()
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
fun setBorderBottomRightRadius(radius: Float) {
|
|
350
|
+
borderBottomRightRadius = radius
|
|
351
|
+
logDebug("setBorderBottomRightRadius: $radius dp")
|
|
352
|
+
updateCornerRadius()
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
private fun convertDpToPx(dp: Float): Float {
|
|
356
|
+
val displayMetrics = context.resources.displayMetrics
|
|
357
|
+
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, displayMetrics)
|
|
358
|
+
}
|
|
359
|
+
|
|
446
360
|
private fun updateCornerRadius() {
|
|
447
361
|
try {
|
|
448
|
-
|
|
449
|
-
val
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
362
|
+
val baseRadius = convertDpToPx(borderRadius)
|
|
363
|
+
val topLeft = if (borderTopLeftRadius > 0) convertDpToPx(borderTopLeftRadius) else baseRadius
|
|
364
|
+
val topRight = if (borderTopRightRadius > 0) convertDpToPx(borderTopRightRadius) else baseRadius
|
|
365
|
+
val bottomLeft = if (borderBottomLeftRadius > 0) convertDpToPx(borderBottomLeftRadius) else baseRadius
|
|
366
|
+
val bottomRight = if (borderBottomRightRadius > 0) convertDpToPx(borderBottomRightRadius) else baseRadius
|
|
367
|
+
|
|
368
|
+
super.setTopLeftCornerRadius(topLeft)
|
|
369
|
+
super.setTopRightCornerRadius(topRight)
|
|
370
|
+
super.setBottomLeftCornerRadius(bottomLeft)
|
|
371
|
+
super.setBottomRightCornerRadius(bottomRight)
|
|
372
|
+
super.setCornerRadius(baseRadius)
|
|
373
|
+
|
|
374
|
+
val isUniform = topLeft == topRight && topRight == bottomLeft && bottomLeft == bottomRight
|
|
375
|
+
|
|
376
|
+
if (isUniform) {
|
|
377
|
+
outlineProvider = object : ViewOutlineProvider() {
|
|
378
|
+
override fun getOutline(view: View, outline: Outline?) {
|
|
379
|
+
outline?.setRoundRect(0, 0, view.width, view.height, baseRadius)
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
} else {
|
|
383
|
+
outlineProvider = object : ViewOutlineProvider() {
|
|
384
|
+
override fun getOutline(view: View, outline: Outline?) {
|
|
385
|
+
val path = Path()
|
|
386
|
+
val radii = floatArrayOf(
|
|
387
|
+
topLeft,
|
|
388
|
+
topLeft,
|
|
389
|
+
topRight,
|
|
390
|
+
topRight,
|
|
391
|
+
bottomRight,
|
|
392
|
+
bottomRight,
|
|
393
|
+
bottomLeft,
|
|
394
|
+
bottomLeft
|
|
395
|
+
)
|
|
396
|
+
path.addRoundRect(0f, 0f, view.width.toFloat(), view.height.toFloat(), radii, Path.Direction.CW)
|
|
397
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
398
|
+
outline?.setPath(path)
|
|
399
|
+
} else {
|
|
400
|
+
@Suppress("DEPRECATION")
|
|
401
|
+
outline?.setConvexPath(path)
|
|
402
|
+
}
|
|
403
|
+
}
|
|
458
404
|
}
|
|
459
405
|
}
|
|
460
|
-
clipToOutline = true
|
|
461
406
|
|
|
462
|
-
|
|
463
|
-
logDebug("Updated corner radius:
|
|
407
|
+
clipToOutline = true
|
|
408
|
+
logDebug("Updated corner radius: topLeft=$topLeft, topRight=$topRight, bottomLeft=$bottomLeft, bottomRight=$bottomRight (px)")
|
|
464
409
|
} catch (e: Exception) {
|
|
465
410
|
logError("Failed to update corner radius: ${e.message}", e)
|
|
466
411
|
}
|
|
@@ -495,22 +440,5 @@ class ReactNativeBlurView : BlurViewGroup {
|
|
|
495
440
|
*/
|
|
496
441
|
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
|
|
497
442
|
// No-op: Layout is handled by React Native's UIManager.
|
|
498
|
-
// We override this to prevent the superclass (BlurViewGroup/FrameLayout) from
|
|
499
|
-
// re-positioning children based on its own logic (e.g. gravity), which would
|
|
500
|
-
// conflict with React Native's layout.
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
/**
|
|
504
|
-
* Handle configuration changes, such as dark mode or orientation changes.
|
|
505
|
-
* This ensures the blur view updates its overlay color based on the new
|
|
506
|
-
* configuration.
|
|
507
|
-
*/
|
|
508
|
-
override fun onConfigurationChanged(newConfig: Configuration) {
|
|
509
|
-
super.onConfigurationChanged(newConfig)
|
|
510
|
-
|
|
511
|
-
if (viewType == "blur") {
|
|
512
|
-
// Re-apply blur type to update overlay color based on new configuration
|
|
513
|
-
setBlurType(currentBlurType)
|
|
514
|
-
}
|
|
515
443
|
}
|
|
516
444
|
}
|
|
@@ -49,6 +49,26 @@ class ReactNativeBlurViewManager : ViewGroupManager<ReactNativeBlurView>(),
|
|
|
49
49
|
view?.setBorderRadius(borderRadius)
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
@ReactProp(name = "borderTopLeftRadius")
|
|
53
|
+
override fun setBorderTopLeftRadius(view: ReactNativeBlurView?, borderTopLeftRadius: Float) {
|
|
54
|
+
view?.setBorderTopLeftRadius(borderTopLeftRadius)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@ReactProp(name = "borderTopRightRadius")
|
|
58
|
+
override fun setBorderTopRightRadius(view: ReactNativeBlurView?, borderTopRightRadius: Float) {
|
|
59
|
+
view?.setBorderTopRightRadius(borderTopRightRadius)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@ReactProp(name = "borderBottomLeftRadius")
|
|
63
|
+
override fun setBorderBottomLeftRadius(view: ReactNativeBlurView?, borderBottomLeftRadius: Float) {
|
|
64
|
+
view?.setBorderBottomLeftRadius(borderBottomLeftRadius)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
@ReactProp(name = "borderBottomRightRadius")
|
|
68
|
+
override fun setBorderBottomRightRadius(view: ReactNativeBlurView?, borderBottomRightRadius: Float) {
|
|
69
|
+
view?.setBorderBottomRightRadius(borderBottomRightRadius)
|
|
70
|
+
}
|
|
71
|
+
|
|
52
72
|
@ReactProp(name = "reducedTransparencyFallbackColor")
|
|
53
73
|
override fun setReducedTransparencyFallbackColor(view: ReactNativeBlurView?, reducedTransparencyFallbackColor: String?) {
|
|
54
74
|
// no-op
|
package/android/src/main/java/com/sbaiahmed1/reactnativeblur/ReactNativeProgressiveBlurView.kt
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
package com.sbaiahmed1.reactnativeblur
|
|
2
2
|
|
|
3
3
|
import android.content.Context
|
|
4
|
-
import android.content.res.Configuration
|
|
5
4
|
import android.graphics.Canvas
|
|
6
5
|
import android.graphics.Color
|
|
7
6
|
import android.graphics.LinearGradient
|
|
@@ -397,17 +396,6 @@ class ReactNativeProgressiveBlurView : FrameLayout {
|
|
|
397
396
|
cleanup()
|
|
398
397
|
}
|
|
399
398
|
|
|
400
|
-
/**
|
|
401
|
-
* Handle configuration changes, such as dark mode or orientation changes.
|
|
402
|
-
* This ensures the blur view updates its overlay color based on the new
|
|
403
|
-
* configuration.
|
|
404
|
-
*/
|
|
405
|
-
override fun onConfigurationChanged(newConfig: Configuration) {
|
|
406
|
-
super.onConfigurationChanged(newConfig)
|
|
407
|
-
|
|
408
|
-
setBlurType(currentBlurType)
|
|
409
|
-
}
|
|
410
|
-
|
|
411
399
|
/**
|
|
412
400
|
* Cleanup method to prevent memory leaks.
|
|
413
401
|
* Resets initialization state so blur is re-initialized on next attach.
|
|
@@ -526,7 +514,7 @@ class ReactNativeProgressiveBlurView : FrameLayout {
|
|
|
526
514
|
*/
|
|
527
515
|
fun setBlurType(type: String) {
|
|
528
516
|
currentBlurType = type
|
|
529
|
-
val blurType = BlurType.fromString(type
|
|
517
|
+
val blurType = BlurType.fromString(type)
|
|
530
518
|
currentOverlayColor = blurType.overlayColor
|
|
531
519
|
logDebug("setBlurType: $type -> ${blurType.name} -> ${Integer.toHexString(currentOverlayColor)}")
|
|
532
520
|
|
|
@@ -55,6 +55,23 @@ func blurStyleFromString(_ styleString: String) -> UIBlurEffect.Style {
|
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
/// Determines the fixed interface style for a blur type to prevent system adaptation.
|
|
59
|
+
/// Returns nil for ambiguous styles that should inherit from the system.
|
|
60
|
+
func interfaceStyleForBlurType(_ styleString: String) -> UIUserInterfaceStyle? {
|
|
61
|
+
switch styleString {
|
|
62
|
+
case "xlight", "light",
|
|
63
|
+
"systemUltraThinMaterialLight", "systemThinMaterialLight",
|
|
64
|
+
"systemMaterialLight", "systemThickMaterialLight", "systemChromeMaterialLight":
|
|
65
|
+
return .light
|
|
66
|
+
case "dark", "extraDark",
|
|
67
|
+
"systemUltraThinMaterialDark", "systemThinMaterialDark",
|
|
68
|
+
"systemMaterialDark", "systemThickMaterialDark", "systemChromeMaterialDark":
|
|
69
|
+
return .dark
|
|
70
|
+
default:
|
|
71
|
+
return nil
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
58
75
|
/// Maps string glass type names to Glass effect values (iOS 26.0+)
|
|
59
76
|
#if compiler(>=6.2)
|
|
60
77
|
@available(iOS 26.0, *)
|
|
@@ -49,7 +49,6 @@ import UIKit
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
private func setupHostingController() {
|
|
52
|
-
// Completely remove old hosting controller
|
|
53
52
|
if let oldHosting = hostingController {
|
|
54
53
|
oldHosting.view.removeFromSuperview()
|
|
55
54
|
oldHosting.removeFromParent()
|
|
@@ -68,8 +67,10 @@ import UIKit
|
|
|
68
67
|
hosting.view.backgroundColor = .clear
|
|
69
68
|
hosting.view.translatesAutoresizingMaskIntoConstraints = false
|
|
70
69
|
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
let interfaceStyle = interfaceStyleForBlurType(blurTypeString) ?? .unspecified
|
|
71
|
+
overrideUserInterfaceStyle = interfaceStyle
|
|
72
|
+
hosting.overrideUserInterfaceStyle = interfaceStyle
|
|
73
|
+
|
|
73
74
|
if !subviews.isEmpty {
|
|
74
75
|
insertSubview(hosting.view, at: 0)
|
|
75
76
|
} else {
|
|
@@ -87,9 +88,11 @@ import UIKit
|
|
|
87
88
|
}
|
|
88
89
|
|
|
89
90
|
private func updateView() {
|
|
91
|
+
let interfaceStyle = interfaceStyleForBlurType(blurTypeString) ?? .unspecified
|
|
92
|
+
overrideUserInterfaceStyle = interfaceStyle
|
|
93
|
+
|
|
90
94
|
if let hosting = hostingController {
|
|
91
|
-
|
|
92
|
-
// This fixes performance bottlenecks and state synchronization issues
|
|
95
|
+
hosting.overrideUserInterfaceStyle = interfaceStyle
|
|
93
96
|
let blurStyle = blurStyleFromString(blurTypeString)
|
|
94
97
|
let swiftUIView = BasicColoredView(
|
|
95
98
|
blurAmount: blurAmount,
|
|
@@ -3,17 +3,12 @@
|
|
|
3
3
|
import SwiftUI
|
|
4
4
|
import UIKit
|
|
5
5
|
|
|
6
|
-
// MARK: - SwiftUI View Component for Blur
|
|
7
|
-
|
|
8
6
|
struct BasicColoredView: View {
|
|
9
7
|
let blurAmount: Double
|
|
10
8
|
let blurStyle: UIBlurEffect.Style
|
|
11
|
-
let reducedTransparencyFallbackColor: UIColor
|
|
12
9
|
let blurIntensity: Double
|
|
13
10
|
let ignoreSafeArea: Bool
|
|
14
11
|
|
|
15
|
-
let isReducedTransparencyEnabled = UIAccessibility.isReduceTransparencyEnabled
|
|
16
|
-
|
|
17
12
|
init(blurAmount: Double,
|
|
18
13
|
blurStyle: UIBlurEffect.Style,
|
|
19
14
|
ignoreSafeArea: Bool,
|
|
@@ -21,28 +16,15 @@ struct BasicColoredView: View {
|
|
|
21
16
|
self.blurAmount = blurAmount
|
|
22
17
|
self.blurStyle = blurStyle
|
|
23
18
|
self.ignoreSafeArea = ignoreSafeArea
|
|
24
|
-
self.reducedTransparencyFallbackColor = reducedTransparencyFallbackColor
|
|
25
19
|
self.blurIntensity = mapBlurAmountToIntensity(blurAmount)
|
|
26
20
|
}
|
|
27
21
|
|
|
28
22
|
var body: some View {
|
|
29
|
-
|
|
23
|
+
regularBlurView
|
|
30
24
|
.ignoresSafeArea(ignoreSafeArea ? .all : [])
|
|
31
25
|
}
|
|
32
26
|
|
|
33
|
-
private var content: some View {
|
|
34
|
-
if isReducedTransparencyEnabled {
|
|
35
|
-
AnyView(
|
|
36
|
-
Rectangle()
|
|
37
|
-
.fill(Color(reducedTransparencyFallbackColor))
|
|
38
|
-
)
|
|
39
|
-
} else {
|
|
40
|
-
AnyView(regularBlurView)
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
27
|
private var regularBlurView: some View {
|
|
45
|
-
// Use proper blur intensity control for regular blur
|
|
46
28
|
Rectangle()
|
|
47
29
|
.fill(Color(.clear))
|
|
48
30
|
.background(Blur(style: blurStyle, intensity: blurIntensity))
|
|
@@ -21,26 +21,29 @@ class BlurEffectView: UIVisualEffectView {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
func updateBlur(style: UIBlurEffect.Style, intensity: Double) {
|
|
24
|
-
// Skip expensive animator recreation when nothing changed.
|
|
25
|
-
// During FlashList recycling, updateUIView fires on every layout pass
|
|
26
|
-
// even when props are identical, causing jank (issue #100).
|
|
27
24
|
guard style != self.blurStyle || intensity != self.intensity else { return }
|
|
28
25
|
self.blurStyle = style
|
|
29
26
|
self.intensity = intensity
|
|
30
|
-
setupBlur()
|
|
31
|
-
}
|
|
32
27
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
28
|
+
if intensity == 1.0 {
|
|
29
|
+
// Fast path: full blur, skip animator entirely
|
|
30
|
+
animator?.stopAnimation(true)
|
|
31
|
+
animator = nil
|
|
32
|
+
effect = UIBlurEffect(style: style)
|
|
33
|
+
} else if intensity == 0.0 {
|
|
34
|
+
// Fast path: no blur
|
|
35
|
+
animator?.stopAnimation(true)
|
|
36
|
+
animator = nil
|
|
37
|
+
effect = nil
|
|
38
|
+
} else {
|
|
39
|
+
// Reuse existing animator if possible, only recreate if style changed
|
|
40
|
+
if let existing = animator,
|
|
41
|
+
existing.state == .active || existing.state == .inactive {
|
|
42
|
+
existing.fractionComplete = intensity
|
|
43
|
+
} else {
|
|
44
|
+
setupBlur()
|
|
45
|
+
}
|
|
46
|
+
}
|
|
44
47
|
}
|
|
45
48
|
|
|
46
49
|
private func setupBlur() {
|
|
@@ -55,9 +58,6 @@ class BlurEffectView: UIVisualEffectView {
|
|
|
55
58
|
newAnimator.addAnimations { [weak self] in
|
|
56
59
|
self?.effect = UIBlurEffect(style: self?.blurStyle ?? .systemMaterial)
|
|
57
60
|
}
|
|
58
|
-
// pausesOnCompletion: if UIKit ever resumes and runs this to the end,
|
|
59
|
-
// the animator stays .active (paused at 1.0) instead of going .inactive.
|
|
60
|
-
// This guarantees didMoveToWindow can always call pauseAnimation() safely.
|
|
61
61
|
newAnimator.pausesOnCompletion = true
|
|
62
62
|
newAnimator.startAnimation()
|
|
63
63
|
newAnimator.pauseAnimation()
|
|
@@ -108,14 +108,12 @@ import UIKit
|
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
private func updateFallback() {
|
|
111
|
-
// If reduce transparency is enabled, show solid color
|
|
112
111
|
if UIAccessibility.isReduceTransparencyEnabled {
|
|
113
112
|
backgroundColor = reducedTransparencyFallbackColor
|
|
114
113
|
glassEffectView?.effect = nil
|
|
115
114
|
} else {
|
|
116
115
|
backgroundColor = .clear
|
|
117
|
-
|
|
118
|
-
// Map glass types to blur styles for fallback
|
|
116
|
+
|
|
119
117
|
let style: UIBlurEffect.Style
|
|
120
118
|
switch glassType {
|
|
121
119
|
case "regular":
|
|
@@ -125,14 +123,13 @@ import UIKit
|
|
|
125
123
|
default:
|
|
126
124
|
style = .regular
|
|
127
125
|
}
|
|
128
|
-
|
|
126
|
+
|
|
129
127
|
let effect = UIBlurEffect(style: style)
|
|
130
128
|
glassEffectView?.effect = effect
|
|
131
|
-
|
|
132
|
-
// Clear any background color on content view
|
|
129
|
+
|
|
133
130
|
glassEffectView?.contentView.backgroundColor = .clear
|
|
134
131
|
}
|
|
135
|
-
|
|
132
|
+
|
|
136
133
|
layer.cornerRadius = allBorderRadius
|
|
137
134
|
glassEffectView?.layer.cornerRadius = allBorderRadius
|
|
138
135
|
glassEffectView?.layer.masksToBounds = true
|
|
@@ -49,7 +49,6 @@ import UIKit
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
private func setupView() {
|
|
52
|
-
// Remove old view if exists
|
|
53
52
|
variableBlurView?.removeFromSuperview()
|
|
54
53
|
|
|
55
54
|
let blurStyle = blurStyleFromString(blurTypeString)
|
|
@@ -74,7 +73,10 @@ import UIKit
|
|
|
74
73
|
|
|
75
74
|
self.variableBlurView = variableBlur
|
|
76
75
|
|
|
77
|
-
|
|
76
|
+
let interfaceStyle = interfaceStyleForBlurType(blurTypeString) ?? .unspecified
|
|
77
|
+
overrideUserInterfaceStyle = interfaceStyle
|
|
78
|
+
variableBlur.overrideUserInterfaceStyle = interfaceStyle
|
|
79
|
+
|
|
78
80
|
if UIAccessibility.isReduceTransparencyEnabled {
|
|
79
81
|
variableBlur.isHidden = true
|
|
80
82
|
backgroundColor = reducedTransparencyFallbackColor
|
|
@@ -100,7 +102,10 @@ import UIKit
|
|
|
100
102
|
blurStyle: blurStyle
|
|
101
103
|
)
|
|
102
104
|
|
|
103
|
-
|
|
105
|
+
let interfaceStyle = interfaceStyleForBlurType(blurTypeString) ?? .unspecified
|
|
106
|
+
overrideUserInterfaceStyle = interfaceStyle
|
|
107
|
+
variableBlurView.overrideUserInterfaceStyle = interfaceStyle
|
|
108
|
+
|
|
104
109
|
if UIAccessibility.isReduceTransparencyEnabled {
|
|
105
110
|
variableBlurView.isHidden = true
|
|
106
111
|
backgroundColor = reducedTransparencyFallbackColor
|
|
@@ -113,17 +113,6 @@ open class VariableBlurView: UIVisualEffectView {
|
|
|
113
113
|
}
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
-
open override func traitCollectionDidChange(
|
|
117
|
-
_ previousTraitCollection: UITraitCollection?
|
|
118
|
-
) {
|
|
119
|
-
super.traitCollectionDidChange(previousTraitCollection)
|
|
120
|
-
// Re-setup blur if needed when trait collection changes
|
|
121
|
-
if let previousTraitCollection = previousTraitCollection,
|
|
122
|
-
traitCollection.userInterfaceStyle != previousTraitCollection.userInterfaceStyle {
|
|
123
|
-
setupVariableBlur()
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
116
|
private func makeGradientImage(
|
|
128
117
|
width: CGFloat = 100,
|
|
129
118
|
height: CGFloat = 100,
|
|
@@ -53,14 +53,15 @@ import UIKit
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
private func updateEffect() {
|
|
56
|
-
|
|
56
|
+
let interfaceStyle = interfaceStyleForBlurType(blurType) ?? .unspecified
|
|
57
|
+
overrideUserInterfaceStyle = interfaceStyle
|
|
58
|
+
|
|
57
59
|
if let animator = blurAnimator {
|
|
58
60
|
animator.stopAnimation(true)
|
|
59
61
|
animator.finishAnimation(at: .current)
|
|
60
62
|
}
|
|
61
63
|
blurAnimator = nil
|
|
62
64
|
|
|
63
|
-
// Reset effects
|
|
64
65
|
blurEffectView.effect = nil
|
|
65
66
|
vibrancyEffectView.effect = nil
|
|
66
67
|
|
|
@@ -68,28 +69,18 @@ import UIKit
|
|
|
68
69
|
let blurEffect = UIBlurEffect(style: style)
|
|
69
70
|
let vibrancyEffect = UIVibrancyEffect(blurEffect: blurEffect)
|
|
70
71
|
|
|
71
|
-
// Set effects directly first to ensure they are visible
|
|
72
|
-
// Animating them from nil often causes issues with UIVibrancyEffect
|
|
73
72
|
blurEffectView.effect = blurEffect
|
|
74
73
|
vibrancyEffectView.effect = vibrancyEffect
|
|
75
74
|
|
|
76
|
-
// Create animator to adjust intensity
|
|
77
75
|
blurAnimator = UIViewPropertyAnimator(duration: 1, curve: .linear) { [weak self] in
|
|
78
76
|
self?.blurEffectView.effect = nil
|
|
79
77
|
self?.vibrancyEffectView.effect = nil
|
|
80
78
|
}
|
|
81
79
|
|
|
82
|
-
// Convert blurAmount (0-100) to intensity (0.0-1.0)
|
|
83
|
-
// We reverse the logic:
|
|
84
|
-
// fractionComplete = 0.0 -> effects are fully applied (start state)
|
|
85
|
-
// fractionComplete = 1.0 -> effects are removed (end state)
|
|
86
|
-
// So to get desired intensity X, we set fractionComplete to (1 - X)
|
|
87
80
|
let intensity = min(max(blurAmount / 100.0, 0.0), 1.0)
|
|
88
81
|
blurAnimator?.fractionComplete = 1.0 - intensity
|
|
89
82
|
|
|
90
|
-
// Stop the animation at the current state
|
|
91
83
|
DispatchQueue.main.async { [weak self, weak blurAnimator] in
|
|
92
|
-
// Only stop the animator if it's still the current one
|
|
93
84
|
guard let self = self, let currentAnimator = self.blurAnimator, currentAnimator === blurAnimator else { return }
|
|
94
85
|
|
|
95
86
|
currentAnimator.stopAnimation(true)
|