react-native-parity-blur 0.1.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/LICENSE +20 -0
- package/ParityBlur.podspec +20 -0
- package/README.md +198 -0
- package/android/build.gradle +57 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/parityblur/AndroidBlurCalibration.kt +82 -0
- package/android/src/main/java/com/parityblur/BlurEngine.kt +132 -0
- package/android/src/main/java/com/parityblur/ParityBlurDebug.kt +24 -0
- package/android/src/main/java/com/parityblur/ParityBlurPackage.kt +17 -0
- package/android/src/main/java/com/parityblur/ParityBlurView.kt +542 -0
- package/android/src/main/java/com/parityblur/ParityBlurViewManager.kt +161 -0
- package/android/src/main/java/com/parityblur/PipelineMath.kt +202 -0
- package/android/src/main/java/com/parityblur/PixelCopySnapshotProvider.kt +108 -0
- package/android/src/main/java/com/parityblur/SoftwareSnapshotProvider.kt +173 -0
- package/android/src/main/java/com/parityblur/WindowBlurContext.kt +116 -0
- package/android/src/test/java/com/parityblur/MiniJson.kt +179 -0
- package/android/src/test/java/com/parityblur/PipelineFixturesTest.kt +222 -0
- package/ios/BlurEngine.swift +106 -0
- package/ios/ColorPipeline.swift +72 -0
- package/ios/MetalPresentationSurface.swift +51 -0
- package/ios/ParityBlurCoreView.swift +382 -0
- package/ios/ParityBlurDebug.swift +18 -0
- package/ios/ParityBlurView.h +14 -0
- package/ios/ParityBlurView.mm +135 -0
- package/ios/PipelineMath.swift +177 -0
- package/ios/SnapshotProvider.swift +128 -0
- package/ios/WindowBlurContext.swift +111 -0
- package/lib/module/BlurView.js +124 -0
- package/lib/module/BlurView.js.map +1 -0
- package/lib/module/ParityBlurViewNativeComponent.ts +82 -0
- package/lib/module/defaults.js +13 -0
- package/lib/module/defaults.js.map +1 -0
- package/lib/module/index.js +5 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/pipeline/androidCalibration.js +55 -0
- package/lib/module/pipeline/androidCalibration.js.map +1 -0
- package/lib/module/pipeline/captureRect.js +151 -0
- package/lib/module/pipeline/captureRect.js.map +1 -0
- package/lib/module/pipeline/color.js +82 -0
- package/lib/module/pipeline/color.js.map +1 -0
- package/lib/module/pipeline/constants.js +65 -0
- package/lib/module/pipeline/constants.js.map +1 -0
- package/lib/module/pipeline/downsample.js +61 -0
- package/lib/module/pipeline/downsample.js.map +1 -0
- package/lib/module/pipeline/index.js +23 -0
- package/lib/module/pipeline/index.js.map +1 -0
- package/lib/module/pipeline/overlay.js +56 -0
- package/lib/module/pipeline/overlay.js.map +1 -0
- package/lib/module/pipeline/saturation.js +52 -0
- package/lib/module/pipeline/saturation.js.map +1 -0
- package/lib/module/pipeline/types.js +23 -0
- package/lib/module/pipeline/types.js.map +1 -0
- package/lib/module/pipeline/units.js +33 -0
- package/lib/module/pipeline/units.js.map +1 -0
- package/lib/module/presets.js +96 -0
- package/lib/module/presets.js.map +1 -0
- package/lib/module/types.js +4 -0
- package/lib/module/types.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/BlurView.d.ts +16 -0
- package/lib/typescript/src/BlurView.d.ts.map +1 -0
- package/lib/typescript/src/ParityBlurViewNativeComponent.d.ts +63 -0
- package/lib/typescript/src/ParityBlurViewNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/defaults.d.ts +12 -0
- package/lib/typescript/src/defaults.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +5 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/pipeline/androidCalibration.d.ts +42 -0
- package/lib/typescript/src/pipeline/androidCalibration.d.ts.map +1 -0
- package/lib/typescript/src/pipeline/captureRect.d.ts +84 -0
- package/lib/typescript/src/pipeline/captureRect.d.ts.map +1 -0
- package/lib/typescript/src/pipeline/color.d.ts +19 -0
- package/lib/typescript/src/pipeline/color.d.ts.map +1 -0
- package/lib/typescript/src/pipeline/constants.d.ts +52 -0
- package/lib/typescript/src/pipeline/constants.d.ts.map +1 -0
- package/lib/typescript/src/pipeline/downsample.d.ts +31 -0
- package/lib/typescript/src/pipeline/downsample.d.ts.map +1 -0
- package/lib/typescript/src/pipeline/index.d.ts +20 -0
- package/lib/typescript/src/pipeline/index.d.ts.map +1 -0
- package/lib/typescript/src/pipeline/overlay.d.ts +31 -0
- package/lib/typescript/src/pipeline/overlay.d.ts.map +1 -0
- package/lib/typescript/src/pipeline/saturation.d.ts +38 -0
- package/lib/typescript/src/pipeline/saturation.d.ts.map +1 -0
- package/lib/typescript/src/pipeline/types.d.ts +32 -0
- package/lib/typescript/src/pipeline/types.d.ts.map +1 -0
- package/lib/typescript/src/pipeline/units.d.ts +23 -0
- package/lib/typescript/src/pipeline/units.d.ts.map +1 -0
- package/lib/typescript/src/presets.d.ts +57 -0
- package/lib/typescript/src/presets.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +116 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/package.json +167 -0
- package/src/BlurView.tsx +195 -0
- package/src/ParityBlurViewNativeComponent.ts +82 -0
- package/src/defaults.ts +20 -0
- package/src/index.ts +10 -0
- package/src/pipeline/androidCalibration.ts +64 -0
- package/src/pipeline/captureRect.ts +172 -0
- package/src/pipeline/color.ts +66 -0
- package/src/pipeline/constants.ts +65 -0
- package/src/pipeline/downsample.ts +74 -0
- package/src/pipeline/index.ts +20 -0
- package/src/pipeline/overlay.ts +46 -0
- package/src/pipeline/saturation.ts +65 -0
- package/src/pipeline/types.ts +37 -0
- package/src/pipeline/units.ts +32 -0
- package/src/presets.ts +103 -0
- package/src/types.ts +132 -0
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
package com.parityblur
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.graphics.Bitmap
|
|
5
|
+
import android.graphics.Canvas
|
|
6
|
+
import android.graphics.Color
|
|
7
|
+
import android.graphics.ColorMatrix
|
|
8
|
+
import android.graphics.ColorMatrixColorFilter
|
|
9
|
+
import android.graphics.Paint
|
|
10
|
+
import android.graphics.Path
|
|
11
|
+
import android.graphics.RectF
|
|
12
|
+
import android.graphics.RenderEffect
|
|
13
|
+
import android.graphics.RenderNode
|
|
14
|
+
import android.graphics.Shader
|
|
15
|
+
import android.os.Build
|
|
16
|
+
import android.util.Log
|
|
17
|
+
import android.view.View
|
|
18
|
+
import android.view.ViewGroup
|
|
19
|
+
import com.facebook.react.views.view.ReactViewGroup
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Milestone 3 static-backend implementation of ParityBlurView (docs/MASTER_PLAN.md §14, §20,
|
|
23
|
+
* §36; docs/PIPELINE_SPEC.md).
|
|
24
|
+
*
|
|
25
|
+
* API 31+: captures the backdrop via [SoftwareSnapshotProvider] into a downsampled bitmap, then
|
|
26
|
+
* presents it through a [RenderNode] carrying a chained `blur -> saturation` [RenderEffect]
|
|
27
|
+
* (PIPELINE_SPEC §9 steps 7-9), with the overlay color composited on top in [onDraw] (source-over
|
|
28
|
+
* on an opaque destination -- PIPELINE_SPEC §8) and rounded clipping applied to that whole
|
|
29
|
+
* presentation (blur + overlay), separate from child clipping (plan §31).
|
|
30
|
+
*
|
|
31
|
+
* API <31: [fallbackColor] fill ONLY. Must never touch [BlurEngine] / bitmaps / RenderNode /
|
|
32
|
+
* RenderEffect (plan §27, §45.1) -- every method below that reaches those is gated behind
|
|
33
|
+
* [isRealBlurSupported].
|
|
34
|
+
*
|
|
35
|
+
* State machine (plan §14.4):
|
|
36
|
+
* `DETACHED -> ATTACHED_WAITING_LAYOUT -> WAITING_STABLE_FRAME -> CAPTURE_PENDING -> CAPTURED`.
|
|
37
|
+
* Capture is requested (not performed synchronously) on every trigger; the per-window
|
|
38
|
+
* [WindowBlurContext] coalesces repeated requests to one capture at the next valid frame
|
|
39
|
+
* (plan §45.8: never capture synchronously inside JS command dispatch, never invalidate from
|
|
40
|
+
* preDraw).
|
|
41
|
+
*/
|
|
42
|
+
class ParityBlurView(context: Context) : ReactViewGroup(context) {
|
|
43
|
+
|
|
44
|
+
private enum class CaptureState {
|
|
45
|
+
DETACHED,
|
|
46
|
+
ATTACHED_WAITING_LAYOUT,
|
|
47
|
+
WAITING_STABLE_FRAME,
|
|
48
|
+
CAPTURE_PENDING,
|
|
49
|
+
CAPTURED,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ---------------------------------------------------------------------------------- props
|
|
53
|
+
|
|
54
|
+
var blurRadius: Double = 0.0
|
|
55
|
+
private set
|
|
56
|
+
var mode: String = "static"
|
|
57
|
+
private set
|
|
58
|
+
var overlayColor: Int? = null
|
|
59
|
+
private set
|
|
60
|
+
var saturation: Double = 1.0
|
|
61
|
+
private set
|
|
62
|
+
var quality: String = "balanced"
|
|
63
|
+
private set
|
|
64
|
+
|
|
65
|
+
/** 0 = 'auto' sentinel (see ParityBlurViewNativeComponent.ts); else 1|2|4|8. */
|
|
66
|
+
var downsample: Int = 0
|
|
67
|
+
private set
|
|
68
|
+
var maxFps: Int = 30
|
|
69
|
+
private set
|
|
70
|
+
var fallbackColor: Int? = null
|
|
71
|
+
private set
|
|
72
|
+
|
|
73
|
+
fun setBlurRadius(value: Double) {
|
|
74
|
+
if (value == blurRadius) return
|
|
75
|
+
blurRadius = value
|
|
76
|
+
debugLog { "blurRadius=$value" }
|
|
77
|
+
requestCapture() // blurRadius changes the sigma -> capture region/downsample/radius (spec).
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
fun setMode(value: String?) {
|
|
81
|
+
val next = value ?: "static"
|
|
82
|
+
if (next == mode) return
|
|
83
|
+
mode = next
|
|
84
|
+
debugLog { "mode=$mode" }
|
|
85
|
+
syncLiveRegistration()
|
|
86
|
+
if (mode == "static") requestCapture() // leaving live: freeze on a fresh static capture
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Milestone 6: live views register with the per-window coordinator (plan §21/§39). */
|
|
90
|
+
private fun syncLiveRegistration() {
|
|
91
|
+
if (!isRealBlurSupported()) return
|
|
92
|
+
val ctx = windowContext ?: return
|
|
93
|
+
if (mode == "live" && isAttachedToWindow) ctx.registerLive(this) else ctx.unregisterLive(this)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
fun setOverlayColor(value: Int?) {
|
|
97
|
+
overlayColor = value
|
|
98
|
+
debugLog { "overlayColor=$value" }
|
|
99
|
+
invalidate() // re-present without recapture (plan §20): overlay is applied at draw time only.
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
fun setSaturation(value: Double) {
|
|
103
|
+
if (value == saturation) return
|
|
104
|
+
saturation = value
|
|
105
|
+
debugLog { "saturation=$value" }
|
|
106
|
+
// re-present without recapture: rebuild the existing RenderNode's effect chain in place.
|
|
107
|
+
if (isRealBlurSupported()) rebuildEffectChain()
|
|
108
|
+
invalidate()
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
fun setQuality(value: String?) {
|
|
112
|
+
val next = value ?: "balanced"
|
|
113
|
+
if (next == quality) return
|
|
114
|
+
quality = next
|
|
115
|
+
debugLog { "quality=$quality" }
|
|
116
|
+
requestCapture() // affects downsample selection -> recapture required (spec).
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
fun setDownsample(value: Int) {
|
|
120
|
+
if (value == downsample) return
|
|
121
|
+
downsample = value
|
|
122
|
+
debugLog { "downsample=$value (0 = auto)" }
|
|
123
|
+
requestCapture()
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
fun setMaxFps(value: Int) {
|
|
127
|
+
maxFps = value.coerceIn(1, 120)
|
|
128
|
+
debugLog { "maxFps=$maxFps" }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
fun setFallbackColor(value: Int?) {
|
|
132
|
+
fallbackColor = value
|
|
133
|
+
debugLog { "fallbackColor=$value" }
|
|
134
|
+
updateFallbackBackground()
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Corner radius setter invoked by [ParityBlurViewManager] (plan §31). `corner`: 0 = uniform
|
|
139
|
+
* (`borderRadius`), 1 = topLeft, 2 = topRight, 3 = bottomRight, 4 = bottomLeft -- matching
|
|
140
|
+
* React Native's `BorderRadiusProp` physical-corner ordering for indices 0..4. `radiusPx` is
|
|
141
|
+
* already density-resolved device px, or null to clear that slot. Percentage border radii are
|
|
142
|
+
* not supported for the blur-output clip in v1 (documented limitation; see manager).
|
|
143
|
+
*/
|
|
144
|
+
fun setCornerRadiusPx(corner: Int, radiusPx: Float?) {
|
|
145
|
+
when (corner) {
|
|
146
|
+
0 -> uniformRadiusPx = radiusPx
|
|
147
|
+
1 -> topLeftRadiusPx = radiusPx
|
|
148
|
+
2 -> topRightRadiusPx = radiusPx
|
|
149
|
+
3 -> bottomRightRadiusPx = radiusPx
|
|
150
|
+
4 -> bottomLeftRadiusPx = radiusPx
|
|
151
|
+
}
|
|
152
|
+
val u = uniformRadiusPx ?: 0f
|
|
153
|
+
cornerRadii = floatArrayOf(
|
|
154
|
+
topLeftRadiusPx ?: u,
|
|
155
|
+
topRightRadiusPx ?: u,
|
|
156
|
+
bottomRightRadiusPx ?: u,
|
|
157
|
+
bottomLeftRadiusPx ?: u
|
|
158
|
+
)
|
|
159
|
+
clipPathDirty = true
|
|
160
|
+
invalidate()
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Fabric command handler for `refresh()` (plan §29): schedules a coalesced recapture on the
|
|
165
|
+
* next valid frame. Never captures synchronously during command dispatch.
|
|
166
|
+
*/
|
|
167
|
+
fun refresh() {
|
|
168
|
+
debugLog { "refresh() requested" }
|
|
169
|
+
requestCapture()
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// --------------------------------------------------------------------- corner radii state
|
|
173
|
+
|
|
174
|
+
private var uniformRadiusPx: Float? = null
|
|
175
|
+
private var topLeftRadiusPx: Float? = null
|
|
176
|
+
private var topRightRadiusPx: Float? = null
|
|
177
|
+
private var bottomRightRadiusPx: Float? = null
|
|
178
|
+
private var bottomLeftRadiusPx: Float? = null
|
|
179
|
+
private var cornerRadii: FloatArray? = null
|
|
180
|
+
private var clipPath: Path? = null
|
|
181
|
+
private var clipPathDirty = true
|
|
182
|
+
|
|
183
|
+
// -------------------------------------------------------------------------- capture target
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Default capture target (plan §18): the window's root view. `blurTarget` ref override is not
|
|
187
|
+
* wired to native in this milestone (documented gap -- see ParityBlurViewManager); the root
|
|
188
|
+
* view is a reasonable default target and is guaranteed to never itself be a ParityBlurView.
|
|
189
|
+
*/
|
|
190
|
+
private var resolvedTarget: ViewGroup? = null
|
|
191
|
+
private var targetLayoutListener: View.OnLayoutChangeListener? = null
|
|
192
|
+
|
|
193
|
+
private fun resolveTarget(): ViewGroup? {
|
|
194
|
+
resolvedTarget?.let { return it }
|
|
195
|
+
val target = rootView as? ViewGroup ?: return null
|
|
196
|
+
val listener = View.OnLayoutChangeListener { _, l, t, r, b, ol, ot, oR, oB ->
|
|
197
|
+
if ((r - l) != (oR - ol) || (b - t) != (oB - ot)) requestCapture()
|
|
198
|
+
}
|
|
199
|
+
target.addOnLayoutChangeListener(listener)
|
|
200
|
+
resolvedTarget = target
|
|
201
|
+
targetLayoutListener = listener
|
|
202
|
+
return target
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private fun releaseTargetListener() {
|
|
206
|
+
targetLayoutListener?.let { resolvedTarget?.removeOnLayoutChangeListener(it) }
|
|
207
|
+
targetLayoutListener = null
|
|
208
|
+
resolvedTarget = null
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ------------------------------------------------------------------------- state machine
|
|
212
|
+
|
|
213
|
+
private var captureState = CaptureState.DETACHED
|
|
214
|
+
private var generation = 0
|
|
215
|
+
private var windowContext: WindowBlurContext? = null
|
|
216
|
+
private var lastLeft = Int.MIN_VALUE
|
|
217
|
+
private var lastTop = Int.MIN_VALUE
|
|
218
|
+
|
|
219
|
+
private fun isRealBlurSupported(): Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
|
|
220
|
+
|
|
221
|
+
private fun isEligibleForCapture(): Boolean {
|
|
222
|
+
if (!isAttachedToWindow) return false
|
|
223
|
+
if (width <= 0 || height <= 0) return false
|
|
224
|
+
val target = resolveTarget() ?: return false
|
|
225
|
+
return target.width > 0 && target.height > 0
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Request a capture: bumps the generation and asks the window context to coalesce a capture
|
|
230
|
+
* at the next valid frame boundary (plan §14.4/§20/§23). No-ops entirely on API<31 (guardrail:
|
|
231
|
+
* fallback-only instances must never touch BlurEngine).
|
|
232
|
+
*/
|
|
233
|
+
private fun requestCapture() {
|
|
234
|
+
if (!isRealBlurSupported()) return
|
|
235
|
+
generation++
|
|
236
|
+
if (!isEligibleForCapture()) {
|
|
237
|
+
captureState = if (isAttachedToWindow) CaptureState.ATTACHED_WAITING_LAYOUT else CaptureState.DETACHED
|
|
238
|
+
return
|
|
239
|
+
}
|
|
240
|
+
if (captureState != CaptureState.CAPTURED) captureState = CaptureState.WAITING_STABLE_FRAME
|
|
241
|
+
val ctx = windowContext ?: BlurEngine.get(context).windowContextFor(rootView).also {
|
|
242
|
+
it.register(this)
|
|
243
|
+
windowContext = it
|
|
244
|
+
}
|
|
245
|
+
ctx.scheduleCapture(this)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** Invoked by [WindowBlurContext]'s coalesced Choreographer callback -- runs on the main thread. */
|
|
249
|
+
internal fun performScheduledCapture() {
|
|
250
|
+
if (!isRealBlurSupported()) return
|
|
251
|
+
if (!isEligibleForCapture()) {
|
|
252
|
+
captureState = CaptureState.ATTACHED_WAITING_LAYOUT
|
|
253
|
+
return
|
|
254
|
+
}
|
|
255
|
+
val target = resolveTarget() ?: return
|
|
256
|
+
captureState = CaptureState.CAPTURE_PENDING
|
|
257
|
+
val myGeneration = generation
|
|
258
|
+
val engine = BlurEngine.get(context)
|
|
259
|
+
|
|
260
|
+
// First capture after mount: GPU-composited PixelCopy so HARDWARE bitmaps (RN Images) are
|
|
261
|
+
// included -- the software canvas cannot rasterize them (see PixelCopySnapshotProvider).
|
|
262
|
+
// Safe only before this view has ever presented (no self-content on screen to feed back).
|
|
263
|
+
// Async: generation + attachment re-validated on completion (plan §23).
|
|
264
|
+
if (!hasContent) {
|
|
265
|
+
PixelCopySnapshotProvider.capture(this, target, blurRadius, downsample, quality, engine) { result ->
|
|
266
|
+
if (myGeneration != generation || !isAttachedToWindow) {
|
|
267
|
+
result?.let { engine.releaseBitmap(it.bitmap) }
|
|
268
|
+
return@capture
|
|
269
|
+
}
|
|
270
|
+
if (result != null) {
|
|
271
|
+
presentCapture(result, engine)
|
|
272
|
+
captureState = CaptureState.CAPTURED
|
|
273
|
+
} else {
|
|
274
|
+
// PixelCopy unavailable (no window surface, teardown race): software fallback.
|
|
275
|
+
completeSoftwareCapture(target, engine, myGeneration)
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
completeSoftwareCapture(target, engine, myGeneration)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ------------------------------------------------------------------- live (Milestone 6)
|
|
285
|
+
|
|
286
|
+
private var lastLiveCaptureNanos = 0L
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Visibility heuristic + per-view maxFps throttle (plan §24, §21). Called by the window
|
|
290
|
+
* coordinator on every pre-draw tick while any live view is registered.
|
|
291
|
+
*/
|
|
292
|
+
internal fun isLiveEligible(nowNanos: Long): Boolean {
|
|
293
|
+
if (mode != "live" || !isRealBlurSupported()) return false
|
|
294
|
+
if (!isAttachedToWindow || windowVisibility != View.VISIBLE) return false
|
|
295
|
+
if (!isShown || alpha <= 0.02f || width <= 0 || height <= 0) return false
|
|
296
|
+
val minInterval = 1_000_000_000L / maxFps
|
|
297
|
+
if (nowNanos - lastLiveCaptureNanos < minInterval) return false
|
|
298
|
+
val loc = IntArray(2)
|
|
299
|
+
getLocationInWindow(loc)
|
|
300
|
+
val root = rootView
|
|
301
|
+
if (loc[0] + width <= 0 || loc[1] + height <= 0) return false
|
|
302
|
+
if (loc[0] >= root.width || loc[1] >= root.height) return false
|
|
303
|
+
return true
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Live recapture INSIDE the in-flight frame (M0-proven): synchronous software capture, then
|
|
308
|
+
* re-record the presentation node WITHOUT invalidate() -- the retained RenderNode propagates
|
|
309
|
+
* its own damage, so no frame loop forms (plan §45.8). Software-only: PixelCopy would read
|
|
310
|
+
* back this view's own committed output (feedback). Backpressure is inherent -- one capture
|
|
311
|
+
* per view per frame, no queue (plan §22).
|
|
312
|
+
*/
|
|
313
|
+
internal fun performLiveCapture(nowNanos: Long) {
|
|
314
|
+
val target = resolveTarget() ?: return
|
|
315
|
+
val engine = BlurEngine.get(context)
|
|
316
|
+
val result = SoftwareSnapshotProvider.capture(this, target, blurRadius, downsample, quality, engine)
|
|
317
|
+
?: return
|
|
318
|
+
lastLiveCaptureNanos = nowNanos
|
|
319
|
+
presentCapture(result, engine, invalidateAfter = false)
|
|
320
|
+
captureState = CaptureState.CAPTURED
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** Synchronous software-canvas capture (recaptures; PixelCopy fallback). */
|
|
324
|
+
private fun completeSoftwareCapture(target: ViewGroup, engine: BlurEngine, myGeneration: Int) {
|
|
325
|
+
val result = SoftwareSnapshotProvider.capture(this, target, blurRadius, downsample, quality, engine)
|
|
326
|
+
if (myGeneration != generation) {
|
|
327
|
+
result?.let { engine.releaseBitmap(it.bitmap) }
|
|
328
|
+
return
|
|
329
|
+
}
|
|
330
|
+
if (result == null) {
|
|
331
|
+
captureState = CaptureState.ATTACHED_WAITING_LAYOUT
|
|
332
|
+
return
|
|
333
|
+
}
|
|
334
|
+
presentCapture(result, engine)
|
|
335
|
+
captureState = CaptureState.CAPTURED
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// ------------------------------------------------------------------------- presentation
|
|
339
|
+
|
|
340
|
+
private var presentNode: RenderNode? = null
|
|
341
|
+
private var capturedBitmap: Bitmap? = null
|
|
342
|
+
private var cropRect: PipelineMath.Rect? = null
|
|
343
|
+
private var capturedDownsample: Int = 1
|
|
344
|
+
private var noBlurCurrent = true
|
|
345
|
+
private var radiusPlatformCurrent = 0f
|
|
346
|
+
private var hasContent = false
|
|
347
|
+
|
|
348
|
+
private val overlayPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.FILL }
|
|
349
|
+
|
|
350
|
+
private fun presentCapture(
|
|
351
|
+
result: SoftwareSnapshotProvider.Result,
|
|
352
|
+
engine: BlurEngine,
|
|
353
|
+
invalidateAfter: Boolean = true,
|
|
354
|
+
) {
|
|
355
|
+
capturedBitmap?.let { old -> if (old !== result.bitmap) engine.releaseBitmap(old) }
|
|
356
|
+
capturedBitmap = result.bitmap
|
|
357
|
+
cropRect = result.cropRect
|
|
358
|
+
capturedDownsample = result.downsample
|
|
359
|
+
noBlurCurrent = result.noBlur
|
|
360
|
+
radiusPlatformCurrent = result.radiusPlatform
|
|
361
|
+
|
|
362
|
+
val node = presentNode ?: RenderNode("parityBlurPresent").also { presentNode = it }
|
|
363
|
+
node.setPosition(0, 0, result.bitmap.width, result.bitmap.height)
|
|
364
|
+
val recordingCanvas = node.beginRecording()
|
|
365
|
+
try {
|
|
366
|
+
recordingCanvas.drawBitmap(result.bitmap, 0f, 0f, null)
|
|
367
|
+
} finally {
|
|
368
|
+
node.endRecording()
|
|
369
|
+
}
|
|
370
|
+
rebuildEffectChain()
|
|
371
|
+
hasContent = true
|
|
372
|
+
// Live captures run inside preDraw: the re-recorded RenderNode propagates damage itself,
|
|
373
|
+
// and invalidating from preDraw would schedule an infinite frame loop (plan §45.8).
|
|
374
|
+
if (invalidateAfter) invalidate()
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Rebuilds the presentation node's RenderEffect chain: blur (CLAMP) then saturation
|
|
379
|
+
* (PIPELINE_SPEC §9 steps 8-9), via `createColorFilterEffect(filter, blurEffect)` so saturation
|
|
380
|
+
* composes AFTER blur. blurRadius=0 / no-blur clamp presents the un-blurred snapshot (spec §2)
|
|
381
|
+
* by simply omitting the blur stage; overlay compositing (spec §8, the only tint) happens
|
|
382
|
+
* separately in [onDraw] on top of this node's output, not inside the effect chain.
|
|
383
|
+
*/
|
|
384
|
+
private fun rebuildEffectChain() {
|
|
385
|
+
val node = presentNode ?: return
|
|
386
|
+
var effect: RenderEffect? = if (!noBlurCurrent && radiusPlatformCurrent > 0f) {
|
|
387
|
+
RenderEffect.createBlurEffect(radiusPlatformCurrent, radiusPlatformCurrent, Shader.TileMode.CLAMP)
|
|
388
|
+
} else {
|
|
389
|
+
null
|
|
390
|
+
}
|
|
391
|
+
if (saturation != 1.0) {
|
|
392
|
+
val colorFilter = ColorMatrixColorFilter(ColorMatrix(PipelineMath.saturationMatrixFloat(saturation)))
|
|
393
|
+
effect = if (effect != null) {
|
|
394
|
+
RenderEffect.createColorFilterEffect(colorFilter, effect)
|
|
395
|
+
} else {
|
|
396
|
+
RenderEffect.createColorFilterEffect(colorFilter)
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
node.setRenderEffect(effect)
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
private fun releaseCapturedResources() {
|
|
403
|
+
if (isRealBlurSupported()) {
|
|
404
|
+
capturedBitmap?.let { bmp -> BlurEngine.get(context).releaseBitmap(bmp) }
|
|
405
|
+
}
|
|
406
|
+
capturedBitmap = null
|
|
407
|
+
presentNode = null
|
|
408
|
+
cropRect = null
|
|
409
|
+
hasContent = false
|
|
410
|
+
releaseTargetListener()
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// ------------------------------------------------------------------------- fallback path
|
|
414
|
+
|
|
415
|
+
/** API<31: fallbackColor fill only. Never touches BlurEngine/bitmaps (plan §27). */
|
|
416
|
+
private fun updateFallbackBackground() {
|
|
417
|
+
if (isRealBlurSupported()) {
|
|
418
|
+
setBackgroundColor(Color.TRANSPARENT)
|
|
419
|
+
} else {
|
|
420
|
+
setBackgroundColor(fallbackColor ?: Color.TRANSPARENT)
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// --------------------------------------------------------------------------- lifecycle
|
|
425
|
+
|
|
426
|
+
init {
|
|
427
|
+
setWillNotDraw(false)
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
override fun onAttachedToWindow() {
|
|
431
|
+
super.onAttachedToWindow()
|
|
432
|
+
captureState = CaptureState.ATTACHED_WAITING_LAYOUT
|
|
433
|
+
if (isRealBlurSupported()) {
|
|
434
|
+
CapturePass.registered.add(this)
|
|
435
|
+
val ctx = BlurEngine.get(context).windowContextFor(rootView)
|
|
436
|
+
ctx.register(this)
|
|
437
|
+
windowContext = ctx
|
|
438
|
+
requestCapture()
|
|
439
|
+
syncLiveRegistration()
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
override fun onDetachedFromWindow() {
|
|
444
|
+
captureState = CaptureState.DETACHED
|
|
445
|
+
windowContext?.let {
|
|
446
|
+
it.unregister(this)
|
|
447
|
+
it.cancelScheduledCapture(this)
|
|
448
|
+
}
|
|
449
|
+
windowContext = null
|
|
450
|
+
CapturePass.registered.remove(this)
|
|
451
|
+
releaseCapturedResources()
|
|
452
|
+
ParityBlurDebug.log { "instance-release id=${System.identityHashCode(this)} mode=$mode" }
|
|
453
|
+
super.onDetachedFromWindow()
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
|
457
|
+
super.onSizeChanged(w, h, oldw, oldh)
|
|
458
|
+
clipPathDirty = true
|
|
459
|
+
if (w > 0 && h > 0) requestCapture()
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
|
|
463
|
+
super.onLayout(changed, left, top, right, bottom)
|
|
464
|
+
val moved = left != lastLeft || top != lastTop
|
|
465
|
+
lastLeft = left
|
|
466
|
+
lastTop = top
|
|
467
|
+
// Only treat this as a recapture trigger once we already have a captured frame -- the
|
|
468
|
+
// initial layout is handled by onSizeChanged/onAttachedToWindow already scheduling one.
|
|
469
|
+
if (moved && captureState == CaptureState.CAPTURED) requestCapture()
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// ------------------------------------------------------------------------------- drawing
|
|
473
|
+
|
|
474
|
+
override fun draw(canvas: Canvas) {
|
|
475
|
+
// Software-path in-tree exclusion (plan §14.2; M0 finding 1): during any capture pass this
|
|
476
|
+
// view contributes NOTHING -- neither its blur result nor its children -- so it can never
|
|
477
|
+
// recursively capture its own (or another registered view's) presentation. Normal
|
|
478
|
+
// composition (CapturePass.active == false) is unaffected: children stay sharp above the
|
|
479
|
+
// blur result (plan §30).
|
|
480
|
+
if (CapturePass.active) return
|
|
481
|
+
super.draw(canvas)
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
override fun onDraw(canvas: Canvas) {
|
|
485
|
+
super.onDraw(canvas)
|
|
486
|
+
if (!isRealBlurSupported()) return // fallback path: background color only, already drawn.
|
|
487
|
+
if (!hasContent || !canvas.isHardwareAccelerated) return
|
|
488
|
+
val node = presentNode ?: return
|
|
489
|
+
val crop = cropRect ?: return
|
|
490
|
+
|
|
491
|
+
val saved = canvas.save()
|
|
492
|
+
ensureClipPath()?.let { canvas.clipPath(it) }
|
|
493
|
+
|
|
494
|
+
canvas.save()
|
|
495
|
+
canvas.clipRect(0, 0, width, height)
|
|
496
|
+
val d = capturedDownsample.toFloat()
|
|
497
|
+
canvas.scale(d, d)
|
|
498
|
+
canvas.translate(-crop.x.toFloat(), -crop.y.toFloat())
|
|
499
|
+
canvas.drawRenderNode(node)
|
|
500
|
+
canvas.restore()
|
|
501
|
+
|
|
502
|
+
// Overlay compositing (spec §8): the ONLY tint in the pipeline. Drawing the already-parsed
|
|
503
|
+
// overlayColor with a normal Paint is exactly straight-alpha source-over on the opaque
|
|
504
|
+
// blurred destination beneath it -- see PipelineMath.sourceOver for the mirrored pure-math
|
|
505
|
+
// form exercised by the fixture suite.
|
|
506
|
+
overlayColor?.let { color ->
|
|
507
|
+
if (Color.alpha(color) > 0) {
|
|
508
|
+
overlayPaint.color = color
|
|
509
|
+
canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), overlayPaint)
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
canvas.restoreToCount(saved)
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
private fun ensureClipPath(): Path? {
|
|
517
|
+
val radii = cornerRadii
|
|
518
|
+
if (radii == null || radii.all { it <= 0f }) return null
|
|
519
|
+
if (!clipPathDirty && clipPath != null) return clipPath
|
|
520
|
+
val path = clipPath ?: Path().also { clipPath = it }
|
|
521
|
+
path.reset()
|
|
522
|
+
val rect = RectF(0f, 0f, width.toFloat(), height.toFloat())
|
|
523
|
+
val r = floatArrayOf(
|
|
524
|
+
radii[0], radii[0], // top-left
|
|
525
|
+
radii[1], radii[1], // top-right
|
|
526
|
+
radii[2], radii[2], // bottom-right
|
|
527
|
+
radii[3], radii[3] // bottom-left
|
|
528
|
+
)
|
|
529
|
+
path.addRoundRect(rect, r, Path.Direction.CW)
|
|
530
|
+
clipPathDirty = false
|
|
531
|
+
return path
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
private inline fun debugLog(msg: () -> String) {
|
|
535
|
+
if (DEBUG_LOGS) Log.d(TAG, msg())
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
companion object {
|
|
539
|
+
private const val TAG = "ParityBlurView"
|
|
540
|
+
private const val DEBUG_LOGS = false
|
|
541
|
+
}
|
|
542
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
package com.parityblur
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.bridge.Dynamic
|
|
4
|
+
import com.facebook.react.bridge.DynamicFromObject
|
|
5
|
+
import com.facebook.react.bridge.ReadableArray
|
|
6
|
+
import com.facebook.react.module.annotations.ReactModule
|
|
7
|
+
import com.facebook.react.uimanager.BackgroundStyleApplicator
|
|
8
|
+
import com.facebook.react.uimanager.LengthPercentage
|
|
9
|
+
import com.facebook.react.uimanager.LengthPercentageType
|
|
10
|
+
import com.facebook.react.uimanager.PixelUtil
|
|
11
|
+
import com.facebook.react.uimanager.ThemedReactContext
|
|
12
|
+
import com.facebook.react.uimanager.ViewGroupManager
|
|
13
|
+
import com.facebook.react.uimanager.ViewManagerDelegate
|
|
14
|
+
import com.facebook.react.uimanager.ViewProps
|
|
15
|
+
import com.facebook.react.uimanager.annotations.ReactPropGroup
|
|
16
|
+
import com.facebook.react.uimanager.style.BorderRadiusProp
|
|
17
|
+
import com.facebook.react.viewmanagers.ParityBlurViewManagerDelegate
|
|
18
|
+
import com.facebook.react.viewmanagers.ParityBlurViewManagerInterface
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Milestone 3 ViewManager for ParityBlurView (docs/MASTER_PLAN.md §36).
|
|
22
|
+
*
|
|
23
|
+
* Extends [ViewGroupManager] (not SimpleViewManager) so React Native children are hosted
|
|
24
|
+
* normally above the blur result -- see plan §30. Codegen-declared props/commands (plan §28/§29)
|
|
25
|
+
* are plumbed straight through to [ParityBlurView] via [ParityBlurViewManagerInterface].
|
|
26
|
+
*
|
|
27
|
+
* Border-radius handling (plan §31) is wired here directly rather than through codegen: common
|
|
28
|
+
* View style props like `borderRadius` are NOT part of the codegen-generated
|
|
29
|
+
* [ParityBlurViewManagerInterface] (that interface only covers props explicitly declared in
|
|
30
|
+
* ParityBlurViewNativeComponent.ts). Android's Fabric ViewManagers apply such common style props
|
|
31
|
+
* through the same legacy `@ReactProp`/`@ReactPropGroup` reflection path used by the plain
|
|
32
|
+
* `<View>` -- see `BaseViewManager.setBorderRadius` (a no-op stub that just logs "doesn't support
|
|
33
|
+
* property 'borderRadius'", which is exactly the warning Milestone 1 observed) and
|
|
34
|
+
* `ReactViewManager.setBorderRadius` (the real, annotated override). We mirror
|
|
35
|
+
* `ReactViewManager`'s approach: apply the radius to the view's background/border machinery via
|
|
36
|
+
* [BackgroundStyleApplicator] (so any backgroundColor/border the view might have also respects
|
|
37
|
+
* it) AND forward a resolved device-px value to [ParityBlurView.setCornerRadiusPx] so the blur
|
|
38
|
+
* OUTPUT can be clipped with a matching rounded-rect path (plan §31: blur-output clipping and
|
|
39
|
+
* child clipping are separate concerns -- children keep normal RN overflow semantics and are not
|
|
40
|
+
* force-clipped here).
|
|
41
|
+
*
|
|
42
|
+
* Only the five physical corner properties from plan §31 are wired (uniform + four corners);
|
|
43
|
+
* RTL logical corners (borderStartStartRadius etc.) and percentage radii are out of scope for
|
|
44
|
+
* v1's blur-output clip (documented limitation) -- BackgroundStyleApplicator still receives the
|
|
45
|
+
* raw value either way so the view's own background/border rendering is unaffected.
|
|
46
|
+
*/
|
|
47
|
+
@ReactModule(name = ParityBlurViewManager.NAME)
|
|
48
|
+
class ParityBlurViewManager :
|
|
49
|
+
ViewGroupManager<ParityBlurView>(),
|
|
50
|
+
ParityBlurViewManagerInterface<ParityBlurView> {
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* On Fabric, ALL prop updates route through the codegen [ViewManagerDelegate]; the
|
|
54
|
+
* `@ReactPropGroup` reflection path below is consulted only on the legacy architecture.
|
|
55
|
+
* Standard View style props not in our codegen spec (the border radii) fall through the
|
|
56
|
+
* generated delegate to BaseViewManagerDelegate, which does not handle them and logs
|
|
57
|
+
* "ParityBlurView doesn't support property 'borderRadius'" (observed on-device on RN 0.85).
|
|
58
|
+
* This subclass intercepts the five physical corner props and routes them to the same
|
|
59
|
+
* handler the legacy path uses, restoring plan §31 rounded clipping under Fabric.
|
|
60
|
+
*/
|
|
61
|
+
private class BorderRadiusAwareDelegate(
|
|
62
|
+
private val manager: ParityBlurViewManager,
|
|
63
|
+
) : ViewManagerDelegate<ParityBlurView> {
|
|
64
|
+
// Composition, not inheritance: subclassing the generated Java delegate from Kotlin trips
|
|
65
|
+
// an erased-JVM-signature clash on receiveCommand (platform declaration clash).
|
|
66
|
+
private val inner =
|
|
67
|
+
ParityBlurViewManagerDelegate<ParityBlurView, ParityBlurViewManager>(manager)
|
|
68
|
+
|
|
69
|
+
override fun setProperty(view: ParityBlurView, propName: String, value: Any?) {
|
|
70
|
+
val cornerIndex = when (propName) {
|
|
71
|
+
ViewProps.BORDER_RADIUS -> 0
|
|
72
|
+
ViewProps.BORDER_TOP_LEFT_RADIUS -> 1
|
|
73
|
+
ViewProps.BORDER_TOP_RIGHT_RADIUS -> 2
|
|
74
|
+
ViewProps.BORDER_BOTTOM_RIGHT_RADIUS -> 3
|
|
75
|
+
ViewProps.BORDER_BOTTOM_LEFT_RADIUS -> 4
|
|
76
|
+
else -> null
|
|
77
|
+
}
|
|
78
|
+
if (cornerIndex != null) {
|
|
79
|
+
manager.setBorderRadius(view, cornerIndex, DynamicFromObject(value))
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
@Suppress("DEPRECATION")
|
|
83
|
+
inner.setProperty(view, propName, value)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
override fun receiveCommand(view: ParityBlurView, commandName: String, args: ReadableArray) {
|
|
87
|
+
inner.receiveCommand(view, commandName, args)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private val mDelegate: ViewManagerDelegate<ParityBlurView> =
|
|
92
|
+
BorderRadiusAwareDelegate(this)
|
|
93
|
+
|
|
94
|
+
override fun getDelegate(): ViewManagerDelegate<ParityBlurView> = mDelegate
|
|
95
|
+
|
|
96
|
+
override fun getName(): String = NAME
|
|
97
|
+
|
|
98
|
+
override fun createViewInstance(context: ThemedReactContext): ParityBlurView =
|
|
99
|
+
ParityBlurView(context)
|
|
100
|
+
|
|
101
|
+
override fun setBlurRadius(view: ParityBlurView?, value: Double) {
|
|
102
|
+
view?.setBlurRadius(value)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
override fun setMode(view: ParityBlurView?, value: String?) {
|
|
106
|
+
view?.setMode(value)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
override fun setOverlayColor(view: ParityBlurView?, value: Int?) {
|
|
110
|
+
view?.setOverlayColor(value)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
override fun setSaturation(view: ParityBlurView?, value: Double) {
|
|
114
|
+
view?.setSaturation(value)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
override fun setQuality(view: ParityBlurView?, value: String?) {
|
|
118
|
+
view?.setQuality(value)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
override fun setDownsample(view: ParityBlurView?, value: Int) {
|
|
122
|
+
view?.setDownsample(value)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
override fun setMaxFps(view: ParityBlurView?, value: Int) {
|
|
126
|
+
view?.setMaxFps(value)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
override fun setFallbackColor(view: ParityBlurView?, value: Int?) {
|
|
130
|
+
view?.setFallbackColor(value)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
override fun refresh(view: ParityBlurView?) {
|
|
134
|
+
view?.refresh()
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
@ReactPropGroup(
|
|
138
|
+
names = [
|
|
139
|
+
ViewProps.BORDER_RADIUS,
|
|
140
|
+
ViewProps.BORDER_TOP_LEFT_RADIUS,
|
|
141
|
+
ViewProps.BORDER_TOP_RIGHT_RADIUS,
|
|
142
|
+
ViewProps.BORDER_BOTTOM_RIGHT_RADIUS,
|
|
143
|
+
ViewProps.BORDER_BOTTOM_LEFT_RADIUS,
|
|
144
|
+
]
|
|
145
|
+
)
|
|
146
|
+
fun setBorderRadius(view: ParityBlurView, index: Int, rawBorderRadius: Dynamic) {
|
|
147
|
+
val lengthPercentage = LengthPercentage.setFromDynamic(rawBorderRadius)
|
|
148
|
+
BackgroundStyleApplicator.setBorderRadius(view, BorderRadiusProp.values()[index], lengthPercentage)
|
|
149
|
+
|
|
150
|
+
val px = if (lengthPercentage != null && lengthPercentage.type == LengthPercentageType.POINT) {
|
|
151
|
+
PixelUtil.toPixelFromDIP(lengthPercentage.resolve(0f).toDouble())
|
|
152
|
+
} else {
|
|
153
|
+
null
|
|
154
|
+
}
|
|
155
|
+
view.setCornerRadiusPx(index, px)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
companion object {
|
|
159
|
+
const val NAME = "ParityBlurView"
|
|
160
|
+
}
|
|
161
|
+
}
|