react-native-smooth-clip-view 0.4.5 → 0.5.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/CHANGELOG.md +66 -0
- package/README.md +52 -0
- package/android/src/main/cpp/SmoothClipAndroid.h +22 -0
- package/android/src/main/cpp/SmoothClipBindings.cpp +37 -3
- package/android/src/main/cpp/SmoothClipRegistry.cpp +85 -2
- package/android/src/main/java/com/smoothclipview/ClipGeometryNormalizer.kt +209 -10
- package/android/src/main/java/com/smoothclipview/SmoothClipBackdropView.kt +59 -0
- package/android/src/main/java/com/smoothclipview/SmoothClipBackdropViewManager.kt +63 -0
- package/android/src/main/java/com/smoothclipview/SmoothClipBindings.kt +15 -0
- package/android/src/main/java/com/smoothclipview/SmoothClipView.kt +316 -34
- package/android/src/main/java/com/smoothclipview/SmoothClipViewManager.kt +19 -0
- package/android/src/main/java/com/smoothclipview/SmoothClipViewPackage.kt +1 -1
- package/cpp/SmoothClipAnimationCurve.h +36 -11
- package/cpp/SmoothClipRegistry.h +11 -4
- package/ios/SmoothClipBackdropView.h +17 -0
- package/ios/SmoothClipBackdropView.mm +267 -0
- package/ios/SmoothClipGeometry.h +8 -7
- package/ios/SmoothClipRegistry.mm +192 -9
- package/ios/SmoothClipTurboModule.cpp +33 -11
- package/ios/SmoothClipView.mm +713 -22
- package/ios/SmoothClipViewRegistry.h +14 -0
- package/lib/module/SmoothClipBackdropView.js +40 -0
- package/lib/module/SmoothClipBackdropView.js.map +1 -0
- package/lib/module/SmoothClipBackdropViewNativeComponent.ts +15 -0
- package/lib/module/SmoothClipView.ios.js +17 -1
- package/lib/module/SmoothClipView.ios.js.map +1 -1
- package/lib/module/SmoothClipViewNativeComponent.ts +4 -0
- package/lib/module/geometry.js +44 -6
- package/lib/module/geometry.js.map +1 -1
- package/lib/module/groups.native.js +10 -3
- package/lib/module/groups.native.js.map +1 -1
- package/lib/module/index.js +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/presentationCodec.js +6 -2
- package/lib/module/presentationCodec.js.map +1 -1
- package/lib/typescript/src/SmoothClipBackdropView.d.ts +155 -0
- package/lib/typescript/src/SmoothClipBackdropView.d.ts.map +1 -0
- package/lib/typescript/src/SmoothClipBackdropViewNativeComponent.d.ts +8 -0
- package/lib/typescript/src/SmoothClipBackdropViewNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/SmoothClipView.android.d.ts +1 -1
- package/lib/typescript/src/SmoothClipView.android.d.ts.map +1 -1
- package/lib/typescript/src/SmoothClipView.d.ts +1 -1
- package/lib/typescript/src/SmoothClipView.d.ts.map +1 -1
- package/lib/typescript/src/SmoothClipView.ios.d.ts +15 -1
- package/lib/typescript/src/SmoothClipView.ios.d.ts.map +1 -1
- package/lib/typescript/src/SmoothClipViewNativeComponent.d.ts +4 -0
- package/lib/typescript/src/SmoothClipViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/geometry.d.ts +19 -0
- package/lib/typescript/src/geometry.d.ts.map +1 -1
- package/lib/typescript/src/groups.native.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +4 -2
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/presentationCodec.d.ts +1 -1
- package/lib/typescript/src/presentationCodec.d.ts.map +1 -1
- package/package.json +4 -1
- package/src/SmoothClipBackdropView.tsx +53 -0
- package/src/SmoothClipBackdropViewNativeComponent.ts +15 -0
- package/src/SmoothClipView.android.tsx +4 -1
- package/src/SmoothClipView.ios.tsx +24 -1
- package/src/SmoothClipView.tsx +4 -1
- package/src/SmoothClipViewNativeComponent.ts +4 -0
- package/src/geometry.ts +59 -4
- package/src/groups.native.ts +9 -3
- package/src/index.ts +7 -1
- package/src/presentationCodec.ts +8 -2
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
package com.smoothclipview
|
|
2
|
+
|
|
3
|
+
import android.content.res.Configuration
|
|
4
|
+
import com.facebook.proguard.annotations.DoNotStrip
|
|
5
|
+
import com.facebook.react.uimanager.PixelUtil
|
|
6
|
+
import com.facebook.react.uimanager.ThemedReactContext
|
|
7
|
+
import com.facebook.react.views.view.ReactViewGroup
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A view translated by its controller's backdrop channel. The registry
|
|
11
|
+
* delivers the channel in pixels on every clip delivery — a `setFrame`, a
|
|
12
|
+
* run's frame, a run's end — inside the same Choreographer pass as the clip,
|
|
13
|
+
* so the content never lands a frame after the aperture. The translation goes
|
|
14
|
+
* on an inner container, leaving this view's own transform to React Native.
|
|
15
|
+
*/
|
|
16
|
+
class SmoothClipBackdropView(context: ThemedReactContext) : ReactViewGroup(context) {
|
|
17
|
+
internal val contentContainer = ReactViewGroup(context)
|
|
18
|
+
|
|
19
|
+
/** Driver this view is bound to in the native registry (0 = none). */
|
|
20
|
+
internal var boundDriverId: Double = 0.0
|
|
21
|
+
|
|
22
|
+
init {
|
|
23
|
+
clipChildren = false
|
|
24
|
+
super.addView(contentContainer)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
|
28
|
+
super.onSizeChanged(w, h, oldw, oldh)
|
|
29
|
+
contentContainer.layout(0, 0, w, h)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Registry hot path: the backdrop translation, in pixels. */
|
|
33
|
+
@DoNotStrip
|
|
34
|
+
fun setBackdropTranslationPx(translateX: Float, translateY: Float) {
|
|
35
|
+
contentContainer.translationX = translateX
|
|
36
|
+
contentContainer.translationY = translateY
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
internal fun bindDriver(driverId: Double) {
|
|
40
|
+
if (boundDriverId != 0.0 && boundDriverId != driverId) {
|
|
41
|
+
SmoothClipBindings.nativeUnregisterBackdropView(boundDriverId, this)
|
|
42
|
+
}
|
|
43
|
+
boundDriverId = driverId
|
|
44
|
+
if (driverId != 0.0) {
|
|
45
|
+
SmoothClipBindings.nativeRegisterBackdropView(driverId, this, densityScale())
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
override fun onConfigurationChanged(newConfig: Configuration) {
|
|
50
|
+
super.onConfigurationChanged(newConfig)
|
|
51
|
+
// Density can change without a remount (display switch); the registry
|
|
52
|
+
// scales the channel by the pushed density.
|
|
53
|
+
if (boundDriverId != 0.0) {
|
|
54
|
+
SmoothClipBindings.nativeRegisterBackdropView(boundDriverId, this, densityScale())
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private fun densityScale(): Double = PixelUtil.toPixelFromDIP(1f).toDouble()
|
|
59
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
package com.smoothclipview
|
|
2
|
+
|
|
3
|
+
import android.view.View
|
|
4
|
+
import com.facebook.react.module.annotations.ReactModule
|
|
5
|
+
import com.facebook.react.uimanager.ThemedReactContext
|
|
6
|
+
import com.facebook.react.uimanager.ViewGroupManager
|
|
7
|
+
import com.facebook.react.uimanager.ViewManagerDelegate
|
|
8
|
+
import com.facebook.react.uimanager.annotations.ReactProp
|
|
9
|
+
import com.facebook.react.viewmanagers.SmoothClipBackdropViewManagerDelegate
|
|
10
|
+
import com.facebook.react.viewmanagers.SmoothClipBackdropViewManagerInterface
|
|
11
|
+
|
|
12
|
+
@ReactModule(name = SmoothClipBackdropViewManager.NAME)
|
|
13
|
+
class SmoothClipBackdropViewManager : ViewGroupManager<SmoothClipBackdropView>(),
|
|
14
|
+
SmoothClipBackdropViewManagerInterface<SmoothClipBackdropView> {
|
|
15
|
+
private val delegate =
|
|
16
|
+
SmoothClipBackdropViewManagerDelegate<SmoothClipBackdropView, SmoothClipBackdropViewManager>(this)
|
|
17
|
+
|
|
18
|
+
// Fabric delivers the prop before the transaction ends; bind once per
|
|
19
|
+
// transaction, after every prop of it has landed.
|
|
20
|
+
private val pendingDriverIds = java.util.WeakHashMap<SmoothClipBackdropView, Double>()
|
|
21
|
+
|
|
22
|
+
override fun getName(): String = NAME
|
|
23
|
+
|
|
24
|
+
override fun getDelegate(): ViewManagerDelegate<SmoothClipBackdropView> = delegate
|
|
25
|
+
|
|
26
|
+
override fun createViewInstance(context: ThemedReactContext): SmoothClipBackdropView =
|
|
27
|
+
SmoothClipBackdropView(context)
|
|
28
|
+
|
|
29
|
+
@ReactProp(name = "driverId")
|
|
30
|
+
override fun setDriverId(view: SmoothClipBackdropView, value: Double) {
|
|
31
|
+
pendingDriverIds[view] = value
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
override fun addView(parent: SmoothClipBackdropView, child: View, index: Int) {
|
|
35
|
+
parent.contentContainer.addView(child, index)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
override fun getChildCount(parent: SmoothClipBackdropView): Int =
|
|
39
|
+
parent.contentContainer.childCount
|
|
40
|
+
|
|
41
|
+
override fun getChildAt(parent: SmoothClipBackdropView, index: Int): View =
|
|
42
|
+
parent.contentContainer.getChildAt(index)
|
|
43
|
+
|
|
44
|
+
override fun removeViewAt(parent: SmoothClipBackdropView, index: Int) {
|
|
45
|
+
parent.contentContainer.removeViewAt(index)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
override fun onAfterUpdateTransaction(view: SmoothClipBackdropView) {
|
|
49
|
+
super.onAfterUpdateTransaction(view)
|
|
50
|
+
val driverId = pendingDriverIds.remove(view) ?: return
|
|
51
|
+
view.bindDriver(driverId)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
override fun onDropViewInstance(view: SmoothClipBackdropView) {
|
|
55
|
+
pendingDriverIds.remove(view)
|
|
56
|
+
view.bindDriver(0.0)
|
|
57
|
+
super.onDropViewInstance(view)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
companion object {
|
|
61
|
+
const val NAME = "SmoothClipBackdropView"
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -45,12 +45,27 @@ internal object SmoothClipBindings {
|
|
|
45
45
|
shadowSpreadDistance: Double,
|
|
46
46
|
rotation: Double,
|
|
47
47
|
opacity: Double,
|
|
48
|
+
backdropTranslateX: Double,
|
|
49
|
+
backdropTranslateY: Double,
|
|
48
50
|
density: Double,
|
|
49
51
|
widthPx: Double,
|
|
50
52
|
heightPx: Double,
|
|
51
53
|
lifecycleVisible: Boolean,
|
|
52
54
|
)
|
|
53
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Binds a backdrop view to a driver: the registry translates it by the
|
|
58
|
+
* presentation's backdrop channel on every delivery. Re-registering an
|
|
59
|
+
* already bound view only updates its density.
|
|
60
|
+
*/
|
|
61
|
+
external fun nativeRegisterBackdropView(
|
|
62
|
+
driverId: Double,
|
|
63
|
+
view: SmoothClipBackdropView,
|
|
64
|
+
density: Double,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
external fun nativeUnregisterBackdropView(driverId: Double, view: SmoothClipBackdropView)
|
|
68
|
+
|
|
54
69
|
/**
|
|
55
70
|
* Pushes a registered view's density and pixel host size. Density converts
|
|
56
71
|
* raw DIP presentations to pixels; host size controls readiness and the
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
package com.smoothclipview
|
|
2
2
|
|
|
3
3
|
import android.content.res.Configuration
|
|
4
|
+
import android.graphics.Bitmap
|
|
4
5
|
import android.graphics.BlurMaskFilter
|
|
5
6
|
import android.graphics.Canvas
|
|
6
7
|
import android.graphics.Color
|
|
@@ -8,8 +9,14 @@ import android.graphics.Outline
|
|
|
8
9
|
import android.graphics.Matrix
|
|
9
10
|
import android.graphics.Paint
|
|
10
11
|
import android.graphics.Path
|
|
12
|
+
import android.graphics.Rect
|
|
13
|
+
import android.graphics.RectF
|
|
11
14
|
import android.graphics.Region
|
|
12
15
|
import android.os.Build
|
|
16
|
+
import android.os.Handler
|
|
17
|
+
import android.os.HandlerThread
|
|
18
|
+
import android.os.Looper
|
|
19
|
+
import android.os.Process
|
|
13
20
|
import android.os.Trace
|
|
14
21
|
import android.view.MotionEvent
|
|
15
22
|
import android.view.View
|
|
@@ -18,6 +25,8 @@ import com.facebook.proguard.annotations.DoNotStrip
|
|
|
18
25
|
import com.facebook.react.uimanager.PixelUtil
|
|
19
26
|
import com.facebook.react.uimanager.ThemedReactContext
|
|
20
27
|
import com.facebook.react.views.view.ReactViewGroup
|
|
28
|
+
import java.lang.ref.WeakReference
|
|
29
|
+
import java.util.concurrent.atomic.AtomicInteger
|
|
21
30
|
import kotlin.math.roundToInt
|
|
22
31
|
import kotlin.math.abs
|
|
23
32
|
import kotlin.math.pow
|
|
@@ -87,6 +96,18 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
|
|
|
87
96
|
private var clipCurveCode = CLIP_CURVE_CIRCULAR
|
|
88
97
|
private var boxShadowPath: Path? = null
|
|
89
98
|
private var boxShadowPaint: Paint? = null
|
|
99
|
+
// `shadowRendering="baked"`: draw the shadow from one pre-blurred tile per
|
|
100
|
+
// radius step, stretched in nine pieces, instead of blurring a path on
|
|
101
|
+
// every frame the aperture moves. Like iOS, the tile is drawn under the
|
|
102
|
+
// aperture too (the blur path cuts it out); content is expected opaque.
|
|
103
|
+
private var bakedShadows = false
|
|
104
|
+
private var bakedShadowTile: BakedShadowTile? = null
|
|
105
|
+
private var bakedShadowPaint: Paint? = null
|
|
106
|
+
private val bakedSrc = Rect()
|
|
107
|
+
private val bakedDst = RectF()
|
|
108
|
+
private val bakedXs = FloatArray(4)
|
|
109
|
+
private val bakedYs = FloatArray(4)
|
|
110
|
+
private val bakedSx = IntArray(4)
|
|
90
111
|
private var clipIsEmpty = true
|
|
91
112
|
private var requestedImportantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_AUTO
|
|
92
113
|
private var autonomousMotion = false
|
|
@@ -410,10 +431,51 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
|
|
|
410
431
|
return (radius + spread * multiplier).coerceAtLeast(0f)
|
|
411
432
|
}
|
|
412
433
|
|
|
434
|
+
fun setBakedShadows(enabled: Boolean) {
|
|
435
|
+
if (bakedShadows == enabled) return
|
|
436
|
+
bakedShadows = enabled
|
|
437
|
+
if (!enabled) bakedShadowTile = null
|
|
438
|
+
if (requestedShadowEnabled && requestedShadowAlpha > 0f) {
|
|
439
|
+
rebuildBoxShadowPath()
|
|
440
|
+
presentationContainer.invalidate()
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
private fun usesBakedShadow(): Boolean {
|
|
445
|
+
if (!bakedShadows || clipTopLeftRadius != clipTopRightRadius ||
|
|
446
|
+
clipTopLeftRadius != clipBottomRightRadius ||
|
|
447
|
+
clipTopLeftRadius != clipBottomLeftRadius
|
|
448
|
+
) {
|
|
449
|
+
return false
|
|
450
|
+
}
|
|
451
|
+
// A shape too small for two corner pieces on a side, 2 x (margin +
|
|
452
|
+
// radius), keeps the blur path: overlapping pieces do not add up to
|
|
453
|
+
// the blur tails the true shadow overlaps.
|
|
454
|
+
val margin = kotlin.math.ceil(1.5f * requestedShadowBlurRadius.coerceAtLeast(0f))
|
|
455
|
+
val minimumSide = 2f * (margin + bakedShadowTileRadius())
|
|
456
|
+
val spread = 2f * requestedShadowSpreadDistance
|
|
457
|
+
return clipRight - clipLeft + spread >= minimumSide &&
|
|
458
|
+
clipBottom - clipTop + spread >= minimumSide
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/** Radius step, in px, of the tile the current shadow draws from. */
|
|
462
|
+
private fun bakedShadowTileRadius(): Float {
|
|
463
|
+
val radius = adjustedRadiusForSpread(clipTopLeftRadius, requestedShadowSpreadDistance)
|
|
464
|
+
val step = BAKED_SHADOW_RADIUS_STEP_DP * resources.displayMetrics.density
|
|
465
|
+
return if (step > 0f) kotlin.math.ceil(radius / step) * step else radius
|
|
466
|
+
}
|
|
467
|
+
|
|
413
468
|
private fun rebuildBoxShadowPath() {
|
|
414
469
|
if (!requestedShadowEnabled || requestedShadowAlpha <= 0f) return
|
|
470
|
+
if (usesBakedShadow()) {
|
|
471
|
+
// The tile replaces the path; a stale path must not be drawn if
|
|
472
|
+
// the radii later stop being uniform before a rebuild.
|
|
473
|
+
boxShadowPath?.rewind()
|
|
474
|
+
return
|
|
475
|
+
}
|
|
415
476
|
val path = boxShadowPath ?: Path().also { boxShadowPath = it }
|
|
416
|
-
path
|
|
477
|
+
// Keeps the path's storage across frames; reset() may free it.
|
|
478
|
+
path.rewind()
|
|
417
479
|
if (clipIsEmpty) return
|
|
418
480
|
val spread = requestedShadowSpreadDistance
|
|
419
481
|
val left = clipLeft - spread + requestedShadowOffsetX
|
|
@@ -445,13 +507,75 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
|
|
|
445
507
|
}
|
|
446
508
|
}
|
|
447
509
|
|
|
510
|
+
private fun bakedShadowTile(): BakedShadowTile? {
|
|
511
|
+
val density = resources.displayMetrics.density
|
|
512
|
+
val step = BAKED_SHADOW_RADIUS_STEP_DP * density
|
|
513
|
+
val tileRadius = bakedShadowTileRadius()
|
|
514
|
+
val band = maxOf(2, (2f * density).roundToInt())
|
|
515
|
+
val rgb = Color.rgb(
|
|
516
|
+
(requestedShadowRed * 255f).roundToInt(),
|
|
517
|
+
(requestedShadowGreen * 255f).roundToInt(),
|
|
518
|
+
(requestedShadowBlue * 255f).roundToInt(),
|
|
519
|
+
)
|
|
520
|
+
val current = bakedShadowTile
|
|
521
|
+
if (current != null && current.matches(tileRadius, clipCurveCode, requestedShadowBlurRadius, rgb)) {
|
|
522
|
+
return current
|
|
523
|
+
}
|
|
524
|
+
// A stand-in from a neighbouring step does not match, so the next
|
|
525
|
+
// frame asks again and picks up the exact tile once it has landed.
|
|
526
|
+
return BakedShadowTiles.getOrRequest(
|
|
527
|
+
tileRadius, clipCurveCode, requestedShadowBlurRadius, rgb, band, step, this,
|
|
528
|
+
).also { bakedShadowTile = it }
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/** The exact tile for the current radius step has landed: draw it. */
|
|
532
|
+
internal fun shadowTileDidLand() {
|
|
533
|
+
if (!requestedShadowEnabled || requestedShadowAlpha <= 0f || !usesBakedShadow()) return
|
|
534
|
+
presentationContainer.invalidate()
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
private fun drawBakedBoxShadow(canvas: Canvas) {
|
|
538
|
+
val tile = bakedShadowTile() ?: return
|
|
539
|
+
val spread = requestedShadowSpreadDistance
|
|
540
|
+
val margin = tile.margin.toFloat()
|
|
541
|
+
val left = clipLeft - spread + requestedShadowOffsetX - margin
|
|
542
|
+
val top = clipTop - spread + requestedShadowOffsetY - margin
|
|
543
|
+
val right = clipRight + spread + requestedShadowOffsetX + margin
|
|
544
|
+
val bottom = clipBottom + spread + requestedShadowOffsetY + margin
|
|
545
|
+
if (right <= left || bottom <= top) return
|
|
546
|
+
val paint = bakedShadowPaint ?: Paint(Paint.FILTER_BITMAP_FLAG).also {
|
|
547
|
+
bakedShadowPaint = it
|
|
548
|
+
}
|
|
549
|
+
paint.alpha = (requestedShadowAlpha * 255f).roundToInt().coerceIn(0, 255)
|
|
550
|
+
val corner = tile.corner.toFloat()
|
|
551
|
+
// Nine pieces: corners keep their size, edges stretch one way, the
|
|
552
|
+
// centre both ways. A destination too small for two corners draws
|
|
553
|
+
// them overlapping and skips the middle pieces.
|
|
554
|
+
bakedXs[0] = left; bakedXs[1] = left + corner
|
|
555
|
+
bakedXs[2] = right - corner; bakedXs[3] = right
|
|
556
|
+
bakedYs[0] = top; bakedYs[1] = top + corner
|
|
557
|
+
bakedYs[2] = bottom - corner; bakedYs[3] = bottom
|
|
558
|
+
bakedSx[0] = 0; bakedSx[1] = tile.corner
|
|
559
|
+
bakedSx[2] = tile.corner + tile.band; bakedSx[3] = tile.side
|
|
560
|
+
for (row in 0 until 3) {
|
|
561
|
+
for (col in 0 until 3) {
|
|
562
|
+
bakedDst.set(bakedXs[col], bakedYs[row], bakedXs[col + 1], bakedYs[row + 1])
|
|
563
|
+
if (bakedDst.right <= bakedDst.left || bakedDst.bottom <= bakedDst.top) continue
|
|
564
|
+
bakedSrc.set(bakedSx[col], bakedSx[row], bakedSx[col + 1], bakedSx[row + 1])
|
|
565
|
+
canvas.drawBitmap(tile.bitmap, bakedSrc, bakedDst, paint)
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
448
570
|
private fun drawBoxShadow(canvas: Canvas) {
|
|
571
|
+
if (!requestedShadowEnabled || clipIsEmpty || requestedShadowAlpha <= 0f) return
|
|
572
|
+
if (usesBakedShadow()) {
|
|
573
|
+
drawBakedBoxShadow(canvas)
|
|
574
|
+
return
|
|
575
|
+
}
|
|
449
576
|
val shadowPath = boxShadowPath
|
|
450
577
|
val shadowPaint = boxShadowPaint
|
|
451
|
-
if (
|
|
452
|
-
requestedShadowAlpha > 0f && shadowPath != null &&
|
|
453
|
-
shadowPaint != null && !shadowPath.isEmpty
|
|
454
|
-
) {
|
|
578
|
+
if (shadowPath != null && shadowPaint != null && !shadowPath.isEmpty) {
|
|
455
579
|
val apertureSaveCount = canvas.save()
|
|
456
580
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
457
581
|
canvas.clipOutPath(clipPath)
|
|
@@ -487,9 +611,6 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
|
|
|
487
611
|
clipTop = top
|
|
488
612
|
clipRight = right
|
|
489
613
|
clipBottom = bottom
|
|
490
|
-
val radiiAreUniform = topLeftRadius == topRightRadius &&
|
|
491
|
-
topLeftRadius == bottomRightRadius &&
|
|
492
|
-
topLeftRadius == bottomLeftRadius
|
|
493
614
|
clipTopLeftRadius = topLeftRadius
|
|
494
615
|
clipTopRightRadius = topRightRadius
|
|
495
616
|
clipBottomRightRadius = bottomRightRadius
|
|
@@ -504,32 +625,22 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
|
|
|
504
625
|
if (!geometryChanged) return
|
|
505
626
|
|
|
506
627
|
updateOverflowInsets()
|
|
507
|
-
clipPath.
|
|
628
|
+
clipPath.rewind()
|
|
508
629
|
if (!isEmpty) {
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
top,
|
|
524
|
-
right,
|
|
525
|
-
bottom,
|
|
526
|
-
topLeftRadius,
|
|
527
|
-
topRightRadius,
|
|
528
|
-
bottomRightRadius,
|
|
529
|
-
bottomLeftRadius,
|
|
530
|
-
curveCode,
|
|
531
|
-
)
|
|
532
|
-
}
|
|
630
|
+
// One builder for clip, shadow and hit testing; it keeps a uniform
|
|
631
|
+
// circular corner as an rrect for the renderer's fast paths.
|
|
632
|
+
appendRoundedRectPath(
|
|
633
|
+
clipPath,
|
|
634
|
+
left,
|
|
635
|
+
top,
|
|
636
|
+
right,
|
|
637
|
+
bottom,
|
|
638
|
+
topLeftRadius,
|
|
639
|
+
topRightRadius,
|
|
640
|
+
bottomRightRadius,
|
|
641
|
+
bottomLeftRadius,
|
|
642
|
+
curveCode,
|
|
643
|
+
)
|
|
533
644
|
}
|
|
534
645
|
rebuildBoxShadowPath()
|
|
535
646
|
if (supportsPathOutlineClipping) {
|
|
@@ -800,9 +911,180 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
|
|
|
800
911
|
requestedShadowSpreadDistance = 0f
|
|
801
912
|
boxShadowPaint = null
|
|
802
913
|
boxShadowPath = null
|
|
914
|
+
bakedShadowTile = null
|
|
803
915
|
applyContentTransform()
|
|
804
|
-
clipPath.
|
|
916
|
+
clipPath.rewind()
|
|
805
917
|
requestedImportantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_AUTO
|
|
806
918
|
applyRequestedGeometry()
|
|
807
919
|
}
|
|
808
920
|
}
|
|
921
|
+
|
|
922
|
+
/** Corner-radius step baked shadow tiles are rounded up to, in dp. */
|
|
923
|
+
internal const val BAKED_SHADOW_RADIUS_STEP_DP = 4f
|
|
924
|
+
|
|
925
|
+
/**
|
|
926
|
+
* One pre-blurred rounded-rect shadow, square, with the shape inset by
|
|
927
|
+
* [margin] (3σ = 1.5 × the CSS blur) on every side and a stretchable band of
|
|
928
|
+
* [band] px between the corner regions of [corner] px. Colour is opaque; the
|
|
929
|
+
* shadow alpha is applied by the drawing paint.
|
|
930
|
+
*/
|
|
931
|
+
internal class BakedShadowTile(
|
|
932
|
+
val bitmap: Bitmap,
|
|
933
|
+
val radius: Float,
|
|
934
|
+
val curveCode: Int,
|
|
935
|
+
val blurRadius: Float,
|
|
936
|
+
val rgb: Int,
|
|
937
|
+
val margin: Int,
|
|
938
|
+
val band: Int,
|
|
939
|
+
) {
|
|
940
|
+
val corner: Int = 2 * margin + radius.roundToInt()
|
|
941
|
+
val side: Int = bitmap.width
|
|
942
|
+
|
|
943
|
+
fun matches(radius: Float, curveCode: Int, blurRadius: Float, rgb: Int): Boolean =
|
|
944
|
+
this.radius == radius && this.curveCode == curveCode &&
|
|
945
|
+
this.blurRadius == blurRadius && this.rgb == rgb
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
/**
|
|
949
|
+
* Process-wide tile cache: a handful of (radius step, curve, blur, colour).
|
|
950
|
+
*
|
|
951
|
+
* A bake costs a bitmap allocation plus a software blur, growing with
|
|
952
|
+
* blur² × density², so it stays off the frame: a missing tile is requested on
|
|
953
|
+
* a background thread while the nearest cached step stands in, and only a
|
|
954
|
+
* cold cache (nothing near to show, at mount) bakes on the calling thread.
|
|
955
|
+
*/
|
|
956
|
+
internal object BakedShadowTiles {
|
|
957
|
+
private const val MAX_TILES = 12
|
|
958
|
+
/** Radius steps searched on either side of a missing tile for a stand-in. */
|
|
959
|
+
private const val SEARCH_STEPS = 16
|
|
960
|
+
private val tiles = LinkedHashMap<String, BakedShadowTile>(MAX_TILES, 0.75f, true)
|
|
961
|
+
private val pending = LinkedHashMap<String, PendingTile>()
|
|
962
|
+
private val mainHandler by lazy { Handler(Looper.getMainLooper()) }
|
|
963
|
+
private val worker by lazy {
|
|
964
|
+
val thread = HandlerThread("SmoothClipShadowTiles", Process.THREAD_PRIORITY_BACKGROUND)
|
|
965
|
+
thread.start()
|
|
966
|
+
Handler(thread.looper)
|
|
967
|
+
}
|
|
968
|
+
/** Where a requested bake runs and where its result lands; tests swap in queues they drain. */
|
|
969
|
+
internal var runBake: (Runnable) -> Unit = { worker.post(it) }
|
|
970
|
+
internal var runOnMain: (Runnable) -> Unit = { mainHandler.post(it) }
|
|
971
|
+
/** Tiles baked so far in this process; tests read it. */
|
|
972
|
+
val bakeCount = AtomicInteger()
|
|
973
|
+
|
|
974
|
+
private class PendingTile(
|
|
975
|
+
val radius: Float,
|
|
976
|
+
val curveCode: Int,
|
|
977
|
+
val blurRadius: Float,
|
|
978
|
+
val rgb: Int,
|
|
979
|
+
val band: Int,
|
|
980
|
+
) {
|
|
981
|
+
val requesters = mutableListOf<WeakReference<SmoothClipView>>()
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
private fun key(radius: Float, curveCode: Int, blurRadius: Float, rgb: Int, band: Int): String =
|
|
985
|
+
"$radius|$curveCode|$blurRadius|$rgb|$band"
|
|
986
|
+
|
|
987
|
+
/**
|
|
988
|
+
* The tile for this radius step when cached. Otherwise the nearest cached
|
|
989
|
+
* step (the rounder neighbour first: a tile corner tighter than the clip
|
|
990
|
+
* corner over it reads as a dark sliver, a rounder one only recedes under
|
|
991
|
+
* the clip) while the exact tile bakes on the background thread, after
|
|
992
|
+
* which [requester] is asked to draw again. Nothing near: baked now.
|
|
993
|
+
*/
|
|
994
|
+
@Synchronized
|
|
995
|
+
fun getOrRequest(
|
|
996
|
+
radius: Float,
|
|
997
|
+
curveCode: Int,
|
|
998
|
+
blurRadius: Float,
|
|
999
|
+
rgb: Int,
|
|
1000
|
+
band: Int,
|
|
1001
|
+
stepPx: Float,
|
|
1002
|
+
requester: SmoothClipView?,
|
|
1003
|
+
): BakedShadowTile? {
|
|
1004
|
+
val key = key(radius, curveCode, blurRadius, rgb, band)
|
|
1005
|
+
tiles[key]?.let { return it }
|
|
1006
|
+
val standIn = if (stepPx > 0f) nearest(radius, curveCode, blurRadius, rgb, band, stepPx) else null
|
|
1007
|
+
if (standIn == null) {
|
|
1008
|
+
return bake(radius, curveCode, blurRadius, rgb, band)?.also { store(key, it) }
|
|
1009
|
+
}
|
|
1010
|
+
request(key, PendingTile(radius, curveCode, blurRadius, rgb, band), requester)
|
|
1011
|
+
return standIn
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
private fun nearest(
|
|
1015
|
+
radius: Float,
|
|
1016
|
+
curveCode: Int,
|
|
1017
|
+
blurRadius: Float,
|
|
1018
|
+
rgb: Int,
|
|
1019
|
+
band: Int,
|
|
1020
|
+
stepPx: Float,
|
|
1021
|
+
): BakedShadowTile? {
|
|
1022
|
+
for (distance in 1..SEARCH_STEPS) {
|
|
1023
|
+
val delta = distance * stepPx
|
|
1024
|
+
tiles[key(radius + delta, curveCode, blurRadius, rgb, band)]?.let { return it }
|
|
1025
|
+
if (radius - delta < 0f) continue
|
|
1026
|
+
tiles[key(radius - delta, curveCode, blurRadius, rgb, band)]?.let { return it }
|
|
1027
|
+
}
|
|
1028
|
+
return null
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
private fun store(key: String, tile: BakedShadowTile) {
|
|
1032
|
+
tiles[key] = tile
|
|
1033
|
+
if (tiles.size > MAX_TILES) {
|
|
1034
|
+
val eldest = tiles.entries.iterator()
|
|
1035
|
+
eldest.next()
|
|
1036
|
+
eldest.remove()
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
private fun request(key: String, tile: PendingTile, requester: SmoothClipView?) {
|
|
1041
|
+
val existing = pending[key]
|
|
1042
|
+
if (existing != null) {
|
|
1043
|
+
// One entry per view: a stand-in never matches, so the same view
|
|
1044
|
+
// asks again on every draw until the exact tile lands.
|
|
1045
|
+
if (requester != null && existing.requesters.none { it.get() === requester }) {
|
|
1046
|
+
existing.requesters += WeakReference(requester)
|
|
1047
|
+
}
|
|
1048
|
+
return
|
|
1049
|
+
}
|
|
1050
|
+
if (requester != null) tile.requesters += WeakReference(requester)
|
|
1051
|
+
pending[key] = tile
|
|
1052
|
+
runBake {
|
|
1053
|
+
val baked = bake(tile.radius, tile.curveCode, tile.blurRadius, tile.rgb, tile.band)
|
|
1054
|
+
runOnMain { land(key, baked) }
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
private fun land(key: String, tile: BakedShadowTile?) {
|
|
1059
|
+
val entry = synchronized(this) {
|
|
1060
|
+
val removed = pending.remove(key)
|
|
1061
|
+
if (tile != null && removed != null) store(key, tile)
|
|
1062
|
+
removed
|
|
1063
|
+
}
|
|
1064
|
+
if (tile == null || entry == null) return
|
|
1065
|
+
for (requester in entry.requesters) requester.get()?.shadowTileDidLand()
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
private fun bake(radius: Float, curveCode: Int, blurRadius: Float, rgb: Int, band: Int): BakedShadowTile? {
|
|
1069
|
+
val margin = kotlin.math.ceil(1.5f * blurRadius.coerceAtLeast(0f)).toInt()
|
|
1070
|
+
val corner = 2 * margin + radius.roundToInt()
|
|
1071
|
+
val side = 2 * corner + band
|
|
1072
|
+
if (side <= 0 || side > 4096) return null
|
|
1073
|
+
bakeCount.incrementAndGet()
|
|
1074
|
+
val bitmap = Bitmap.createBitmap(side, side, Bitmap.Config.ARGB_8888)
|
|
1075
|
+
val path = Path()
|
|
1076
|
+
val inset = margin.toFloat()
|
|
1077
|
+
appendRoundedRectPath(
|
|
1078
|
+
path, inset, inset, side - inset, side - inset,
|
|
1079
|
+
radius, radius, radius, radius, curveCode,
|
|
1080
|
+
)
|
|
1081
|
+
val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
|
1082
|
+
paint.color = rgb or (0xFF shl 24)
|
|
1083
|
+
val sigma = blurRadius * 0.5f
|
|
1084
|
+
if (sigma > 0.5f) {
|
|
1085
|
+
paint.maskFilter = BlurMaskFilter((sigma - 0.5f) / 0.57735f, BlurMaskFilter.Blur.NORMAL)
|
|
1086
|
+
}
|
|
1087
|
+
Canvas(bitmap).drawPath(path, paint)
|
|
1088
|
+
return BakedShadowTile(bitmap, radius, curveCode, blurRadius, rgb, margin, band)
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
@@ -39,6 +39,8 @@ class SmoothClipViewManager : ViewGroupManager<SmoothClipView>(),
|
|
|
39
39
|
var shadowSpreadDistance: Double = 0.0,
|
|
40
40
|
var rotation: Double = 0.0,
|
|
41
41
|
var opacity: Double = 1.0,
|
|
42
|
+
var backdropTranslateX: Double = 0.0,
|
|
43
|
+
var backdropTranslateY: Double = 0.0,
|
|
42
44
|
var driverId: Double = 0.0,
|
|
43
45
|
// Whether any prop setter ran in the current update transaction.
|
|
44
46
|
var dirty: Boolean = false,
|
|
@@ -181,6 +183,21 @@ class SmoothClipViewManager : ViewGroupManager<SmoothClipView>(),
|
|
|
181
183
|
@ReactProp(name = "initialOpacity", defaultDouble = 1.0)
|
|
182
184
|
override fun setInitialOpacity(view: SmoothClipView, value: Double) { pending(view).opacity = value }
|
|
183
185
|
|
|
186
|
+
@ReactProp(name = "shadowRendering", defaultInt = 0)
|
|
187
|
+
override fun setShadowRendering(view: SmoothClipView, value: Int) {
|
|
188
|
+
view.setBakedShadows(value == 1)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
@ReactProp(name = "initialBackdropTranslateX", defaultDouble = 0.0)
|
|
192
|
+
override fun setInitialBackdropTranslateX(view: SmoothClipView, value: Double) {
|
|
193
|
+
pending(view).backdropTranslateX = value
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
@ReactProp(name = "initialBackdropTranslateY", defaultDouble = 0.0)
|
|
197
|
+
override fun setInitialBackdropTranslateY(view: SmoothClipView, value: Double) {
|
|
198
|
+
pending(view).backdropTranslateY = value
|
|
199
|
+
}
|
|
200
|
+
|
|
184
201
|
override fun setClipPresentation(
|
|
185
202
|
view: SmoothClipView,
|
|
186
203
|
x: Double,
|
|
@@ -276,6 +293,8 @@ class SmoothClipViewManager : ViewGroupManager<SmoothClipView>(),
|
|
|
276
293
|
initial.shadowSpreadDistance,
|
|
277
294
|
initial.rotation,
|
|
278
295
|
initial.opacity,
|
|
296
|
+
initial.backdropTranslateX,
|
|
297
|
+
initial.backdropTranslateY,
|
|
279
298
|
view.densityScale(),
|
|
280
299
|
view.width.toDouble(),
|
|
281
300
|
view.height.toDouble(),
|
|
@@ -10,7 +10,7 @@ import com.facebook.react.uimanager.ViewManager
|
|
|
10
10
|
class SmoothClipViewPackage : BaseReactPackage() {
|
|
11
11
|
override fun createViewManagers(
|
|
12
12
|
reactContext: ReactApplicationContext,
|
|
13
|
-
): List<ViewManager<*, *>> = listOf(SmoothClipViewManager())
|
|
13
|
+
): List<ViewManager<*, *>> = listOf(SmoothClipViewManager(), SmoothClipBackdropViewManager())
|
|
14
14
|
|
|
15
15
|
override fun getModule(
|
|
16
16
|
name: String,
|