react-native-smooth-clip-view 0.3.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +135 -386
- package/android/build.gradle +4 -7
- package/android/proguard-rules.pro +2 -2
- package/android/src/main/cpp/SmoothClipAndroid.h +9 -12
- package/android/src/main/cpp/SmoothClipBindings.cpp +300 -704
- package/android/src/main/cpp/SmoothClipRegistry.cpp +348 -594
- package/android/src/main/java/com/smoothclipview/ClipGeometryNormalizer.kt +36 -227
- package/android/src/main/java/com/smoothclipview/SmoothClipBindings.kt +16 -19
- package/android/src/main/java/com/smoothclipview/SmoothClipModule.kt +36 -294
- package/android/src/main/java/com/smoothclipview/SmoothClipView.kt +369 -486
- package/android/src/main/java/com/smoothclipview/SmoothClipViewManager.kt +75 -77
- package/cpp/SmoothClipAnimationCurve.h +237 -346
- package/cpp/SmoothClipRegistry.h +24 -52
- package/cpp/SmoothClipSharedGeometry.h +64 -94
- package/cpp/SmoothClipVelocityTracker.h +5 -5
- package/ios/SmoothClipGeometry.h +23 -34
- package/ios/SmoothClipRegistry.mm +206 -711
- package/ios/SmoothClipTurboModule.cpp +217 -715
- package/ios/SmoothClipTurboModule.h +54 -297
- package/ios/SmoothClipView.mm +599 -505
- package/ios/SmoothClipViewRegistry.h +2 -7
- package/lib/module/NativeSmoothClipModule.js.map +1 -1
- package/lib/module/SmoothClipView.ios.js +84 -17
- package/lib/module/SmoothClipView.ios.js.map +1 -1
- package/lib/module/SmoothClipView.js +1 -1
- package/lib/module/SmoothClipView.js.map +1 -1
- package/lib/module/SmoothClipViewNativeComponent.ts +20 -26
- package/lib/module/capabilities.native.js +1 -19
- package/lib/module/capabilities.native.js.map +1 -1
- package/lib/module/capabilityTypes.js +0 -9
- package/lib/module/capabilityTypes.js.map +1 -1
- package/lib/module/controllerInternals.js +28 -0
- package/lib/module/controllerInternals.js.map +1 -0
- package/lib/module/controllerLifecycle.js +9 -0
- package/lib/module/controllerLifecycle.js.map +1 -0
- package/lib/module/controllerTypes.js +4 -0
- package/lib/module/controllerTypes.js.map +1 -0
- package/lib/module/controllers.android.js +4 -0
- package/lib/module/controllers.android.js.map +1 -0
- package/lib/module/controllers.ios.js +4 -0
- package/lib/module/controllers.ios.js.map +1 -0
- package/lib/module/controllers.js +4 -0
- package/lib/module/controllers.js.map +1 -0
- package/lib/module/controllers.native.js +89 -0
- package/lib/module/controllers.native.js.map +1 -0
- package/lib/module/geometry.js +71 -51
- package/lib/module/geometry.js.map +1 -1
- package/lib/module/groupTypes.js +4 -0
- package/lib/module/{groupDriverTypes.js.map → groupTypes.js.map} +1 -1
- package/lib/module/groups.android.js +4 -0
- package/lib/module/groups.android.js.map +1 -0
- package/lib/module/groups.ios.js +4 -0
- package/lib/module/groups.ios.js.map +1 -0
- package/lib/module/groups.js +4 -0
- package/lib/module/groups.js.map +1 -0
- package/lib/module/groups.native.js +399 -0
- package/lib/module/groups.native.js.map +1 -0
- package/lib/module/ids.js +8 -0
- package/lib/module/ids.js.map +1 -0
- package/lib/module/index.js +3 -3
- package/lib/module/index.js.map +1 -1
- package/lib/module/nativeCompletion.js +37 -16
- package/lib/module/nativeCompletion.js.map +1 -1
- package/lib/module/presentationCodec.js +81 -0
- package/lib/module/presentationCodec.js.map +1 -0
- package/lib/typescript/src/NativeSmoothClipModule.d.ts +14 -23
- package/lib/typescript/src/NativeSmoothClipModule.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 +18 -14
- package/lib/typescript/src/SmoothClipView.ios.d.ts.map +1 -1
- package/lib/typescript/src/SmoothClipViewNativeComponent.d.ts +10 -5
- package/lib/typescript/src/SmoothClipViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/capabilities.native.d.ts +1 -1
- package/lib/typescript/src/capabilities.native.d.ts.map +1 -1
- package/lib/typescript/src/capabilityTypes.d.ts +0 -6
- package/lib/typescript/src/capabilityTypes.d.ts.map +1 -1
- package/lib/typescript/src/controllerInternals.d.ts +14 -0
- package/lib/typescript/src/controllerInternals.d.ts.map +1 -0
- package/lib/typescript/src/controllerLifecycle.d.ts +3 -0
- package/lib/typescript/src/controllerLifecycle.d.ts.map +1 -0
- package/lib/typescript/src/controllerTypes.d.ts +62 -0
- package/lib/typescript/src/controllerTypes.d.ts.map +1 -0
- package/lib/typescript/src/controllers.android.d.ts +2 -0
- package/lib/typescript/src/controllers.android.d.ts.map +1 -0
- package/lib/typescript/src/controllers.d.ts +2 -0
- package/lib/typescript/src/controllers.d.ts.map +1 -0
- package/lib/typescript/src/controllers.ios.d.ts +2 -0
- package/lib/typescript/src/controllers.ios.d.ts.map +1 -0
- package/lib/typescript/src/controllers.native.d.ts +3 -0
- package/lib/typescript/src/controllers.native.d.ts.map +1 -0
- package/lib/typescript/src/geometry.d.ts +22 -12
- package/lib/typescript/src/geometry.d.ts.map +1 -1
- package/lib/typescript/src/groupTypes.d.ts +31 -0
- package/lib/typescript/src/groupTypes.d.ts.map +1 -0
- package/lib/typescript/src/groups.android.d.ts +2 -0
- package/lib/typescript/src/groups.android.d.ts.map +1 -0
- package/lib/typescript/src/groups.d.ts +2 -0
- package/lib/typescript/src/groups.d.ts.map +1 -0
- package/lib/typescript/src/groups.ios.d.ts +2 -0
- package/lib/typescript/src/groups.ios.d.ts.map +1 -0
- package/lib/typescript/src/groups.native.d.ts +3 -0
- package/lib/typescript/src/groups.native.d.ts.map +1 -0
- package/lib/typescript/src/ids.d.ts +2 -0
- package/lib/typescript/src/ids.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +6 -6
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/nativeCompletion.d.ts +16 -6
- package/lib/typescript/src/nativeCompletion.d.ts.map +1 -1
- package/lib/typescript/src/presentationCodec.d.ts +10 -0
- package/lib/typescript/src/presentationCodec.d.ts.map +1 -0
- package/package.json +8 -3
- package/src/NativeSmoothClipModule.ts +41 -292
- package/src/SmoothClipView.ios.tsx +130 -23
- package/src/SmoothClipView.tsx +5 -1
- package/src/SmoothClipViewNativeComponent.ts +20 -26
- package/src/capabilities.native.ts +2 -52
- package/src/capabilityTypes.ts +0 -14
- package/src/controllerInternals.ts +52 -0
- package/src/controllerLifecycle.ts +6 -0
- package/src/controllerTypes.ts +87 -0
- package/src/controllers.android.ts +1 -0
- package/src/controllers.ios.ts +1 -0
- package/src/controllers.native.ts +104 -0
- package/src/controllers.ts +1 -0
- package/src/geometry.ts +118 -74
- package/src/groupTypes.ts +53 -0
- package/src/groups.android.ts +1 -0
- package/src/groups.ios.ts +1 -0
- package/src/groups.native.ts +604 -0
- package/src/groups.ts +1 -0
- package/src/ids.ts +6 -0
- package/src/index.ts +20 -28
- package/src/nativeCompletion.ts +67 -32
- package/src/presentationCodec.ts +134 -0
- package/lib/module/driverState.js +0 -82
- package/lib/module/driverState.js.map +0 -1
- package/lib/module/driverTypes.js +0 -4
- package/lib/module/driverTypes.js.map +0 -1
- package/lib/module/drivers.android.js +0 -6
- package/lib/module/drivers.android.js.map +0 -1
- package/lib/module/drivers.ios.js +0 -6
- package/lib/module/drivers.ios.js.map +0 -1
- package/lib/module/drivers.js +0 -4
- package/lib/module/drivers.js.map +0 -1
- package/lib/module/drivers.native.js +0 -756
- package/lib/module/drivers.native.js.map +0 -1
- package/lib/module/groupDriverTypes.js +0 -4
- package/lib/module/groupDrivers.android.js +0 -4
- package/lib/module/groupDrivers.android.js.map +0 -1
- package/lib/module/groupDrivers.ios.js +0 -4
- package/lib/module/groupDrivers.ios.js.map +0 -1
- package/lib/module/groupDrivers.js +0 -4
- package/lib/module/groupDrivers.js.map +0 -1
- package/lib/module/groupDrivers.native.js +0 -799
- package/lib/module/groupDrivers.native.js.map +0 -1
- package/lib/module/presentationProtocol.js +0 -15
- package/lib/module/presentationProtocol.js.map +0 -1
- package/lib/module/reactRequests.js +0 -56
- package/lib/module/reactRequests.js.map +0 -1
- package/lib/typescript/src/driverState.d.ts +0 -26
- package/lib/typescript/src/driverState.d.ts.map +0 -1
- package/lib/typescript/src/driverTypes.d.ts +0 -121
- package/lib/typescript/src/driverTypes.d.ts.map +0 -1
- package/lib/typescript/src/drivers.android.d.ts +0 -3
- package/lib/typescript/src/drivers.android.d.ts.map +0 -1
- package/lib/typescript/src/drivers.d.ts +0 -3
- package/lib/typescript/src/drivers.d.ts.map +0 -1
- package/lib/typescript/src/drivers.ios.d.ts +0 -3
- package/lib/typescript/src/drivers.ios.d.ts.map +0 -1
- package/lib/typescript/src/drivers.native.d.ts +0 -5
- package/lib/typescript/src/drivers.native.d.ts.map +0 -1
- package/lib/typescript/src/groupDriverTypes.d.ts +0 -66
- package/lib/typescript/src/groupDriverTypes.d.ts.map +0 -1
- package/lib/typescript/src/groupDrivers.android.d.ts +0 -2
- package/lib/typescript/src/groupDrivers.android.d.ts.map +0 -1
- package/lib/typescript/src/groupDrivers.d.ts +0 -2
- package/lib/typescript/src/groupDrivers.d.ts.map +0 -1
- package/lib/typescript/src/groupDrivers.ios.d.ts +0 -2
- package/lib/typescript/src/groupDrivers.ios.d.ts.map +0 -1
- package/lib/typescript/src/groupDrivers.native.d.ts +0 -4
- package/lib/typescript/src/groupDrivers.native.d.ts.map +0 -1
- package/lib/typescript/src/presentationProtocol.d.ts +0 -4
- package/lib/typescript/src/presentationProtocol.d.ts.map +0 -1
- package/lib/typescript/src/reactRequests.d.ts +0 -9
- package/lib/typescript/src/reactRequests.d.ts.map +0 -1
- package/src/driverState.ts +0 -118
- package/src/driverTypes.ts +0 -171
- package/src/drivers.android.ts +0 -8
- package/src/drivers.ios.ts +0 -8
- package/src/drivers.native.ts +0 -1599
- package/src/drivers.ts +0 -6
- package/src/groupDriverTypes.ts +0 -117
- package/src/groupDrivers.android.ts +0 -1
- package/src/groupDrivers.ios.ts +0 -1
- package/src/groupDrivers.native.ts +0 -1189
- package/src/groupDrivers.ts +0 -1
- package/src/presentationProtocol.ts +0 -28
- package/src/reactRequests.ts +0 -76
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
package com.smoothclipview
|
|
2
2
|
|
|
3
3
|
import android.content.res.Configuration
|
|
4
|
+
import android.graphics.BlurMaskFilter
|
|
5
|
+
import android.graphics.Canvas
|
|
6
|
+
import android.graphics.Color
|
|
4
7
|
import android.graphics.Outline
|
|
8
|
+
import android.graphics.Paint
|
|
5
9
|
import android.graphics.Path
|
|
10
|
+
import android.graphics.Region
|
|
11
|
+
import android.os.Build
|
|
6
12
|
import android.os.Trace
|
|
7
13
|
import android.view.MotionEvent
|
|
8
14
|
import android.view.View
|
|
@@ -11,14 +17,35 @@ import com.facebook.proguard.annotations.DoNotStrip
|
|
|
11
17
|
import com.facebook.react.uimanager.PixelUtil
|
|
12
18
|
import com.facebook.react.uimanager.ThemedReactContext
|
|
13
19
|
import com.facebook.react.views.view.ReactViewGroup
|
|
20
|
+
import kotlin.math.roundToInt
|
|
21
|
+
import kotlin.math.abs
|
|
22
|
+
import kotlin.math.pow
|
|
14
23
|
|
|
15
24
|
class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
|
|
25
|
+
private val clipPath = Path()
|
|
26
|
+
private val supportsPathOutlineClipping =
|
|
27
|
+
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
|
|
28
|
+
private val clipContainer: ReactViewGroup =
|
|
29
|
+
if (supportsPathOutlineClipping) {
|
|
30
|
+
ReactViewGroup(context)
|
|
31
|
+
} else {
|
|
32
|
+
object : ReactViewGroup(context) {
|
|
33
|
+
override fun dispatchDraw(canvas: Canvas) {
|
|
34
|
+
val saveCount = canvas.save()
|
|
35
|
+
canvas.clipPath(clipPath)
|
|
36
|
+
try {
|
|
37
|
+
super.dispatchDraw(canvas)
|
|
38
|
+
} finally {
|
|
39
|
+
canvas.restoreToCount(saveCount)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
16
44
|
internal val contentContainer = ReactViewGroup(context)
|
|
17
45
|
private var requestedX = 0f
|
|
18
46
|
private var requestedY = 0f
|
|
19
47
|
private var requestedWidth = 0f
|
|
20
48
|
private var requestedHeight = 0f
|
|
21
|
-
private var requestedRadius = 0f
|
|
22
49
|
private var requestedTopLeftRadius = 0f
|
|
23
50
|
private var requestedTopRightRadius = 0f
|
|
24
51
|
private var requestedBottomRightRadius = 0f
|
|
@@ -27,43 +54,35 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
|
|
|
27
54
|
private var requestedContentTranslateX = 0f
|
|
28
55
|
private var requestedContentTranslateY = 0f
|
|
29
56
|
private var requestedContentScale = 1f
|
|
30
|
-
private var
|
|
57
|
+
private var requestedShadowEnabled = false
|
|
58
|
+
private var requestedShadowRed = 0f
|
|
59
|
+
private var requestedShadowGreen = 0f
|
|
60
|
+
private var requestedShadowBlue = 0f
|
|
61
|
+
private var requestedShadowAlpha = 1f
|
|
62
|
+
private var requestedShadowOffsetX = 0f
|
|
63
|
+
private var requestedShadowOffsetY = 0f
|
|
64
|
+
private var requestedShadowBlurRadius = 0f
|
|
65
|
+
private var requestedShadowSpreadDistance = 0f
|
|
31
66
|
private var clipLeft = 0f
|
|
32
67
|
private var clipTop = 0f
|
|
33
68
|
private var clipRight = 0f
|
|
34
69
|
private var clipBottom = 0f
|
|
35
|
-
private var clipRadius = 0f
|
|
36
70
|
private var clipTopLeftRadius = 0f
|
|
37
71
|
private var clipTopRightRadius = 0f
|
|
38
72
|
private var clipBottomRightRadius = 0f
|
|
39
73
|
private var clipBottomLeftRadius = 0f
|
|
40
74
|
private var clipCurveCode = CLIP_CURVE_CIRCULAR
|
|
41
|
-
private
|
|
42
|
-
private var
|
|
43
|
-
private var outlineUsesFloatRoundRect = false
|
|
44
|
-
// Rounded edges actually emitted to the outline provider. Sub-pixel changes
|
|
45
|
-
// that round to the same integers must not invalidate the outline.
|
|
46
|
-
private var outlineLeft = 0
|
|
47
|
-
private var outlineTop = 0
|
|
48
|
-
private var outlineRight = 0
|
|
49
|
-
private var outlineBottom = 0
|
|
75
|
+
private var boxShadowPath: Path? = null
|
|
76
|
+
private var boxShadowPaint: Paint? = null
|
|
50
77
|
private var clipIsEmpty = true
|
|
51
|
-
// Sub-pixel remainder of the clip origin, carried on this view's own
|
|
52
|
-
// translation and subtracted back out of the content container.
|
|
53
|
-
private var clipResidualX = 0f
|
|
54
|
-
private var clipResidualY = 0f
|
|
55
|
-
// The consumer's `transform` translation, kept apart from the residual so
|
|
56
|
-
// the two compose instead of clobbering each other.
|
|
57
|
-
private var userTranslationX = 0f
|
|
58
|
-
private var userTranslationY = 0f
|
|
59
78
|
private var requestedImportantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_AUTO
|
|
79
|
+
private var autonomousMotion = false
|
|
60
80
|
private var acceptingTouchStream = false
|
|
61
|
-
private var retainedTouchEvent: MotionEvent? = null
|
|
62
81
|
|
|
63
82
|
/** Driver this view is registered with in the native registry (0 = none). */
|
|
64
83
|
internal var boundDriverId: Double = 0.0
|
|
65
84
|
|
|
66
|
-
/** Set once a
|
|
85
|
+
/** Set once a command arrives; later initial presentation props are ignored. */
|
|
67
86
|
internal var commandIsAuthoritative = false
|
|
68
87
|
|
|
69
88
|
private val clipOutlineProvider = object : ViewOutlineProvider() {
|
|
@@ -73,93 +92,32 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
|
|
|
73
92
|
return
|
|
74
93
|
}
|
|
75
94
|
|
|
76
|
-
if (
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
// Android's public Outline.setRoundRect overload accepts only
|
|
80
|
-
// integer edges. Path.addRoundRect is the platform float
|
|
81
|
-
// round-rect primitive; Outline.setPath preserves those edges
|
|
82
|
-
// without falling back to V1's integer/residual placement.
|
|
95
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
96
|
+
// Outline.setRoundRect only accepts integer edges. The raw path
|
|
97
|
+
// keeps sub-pixel coordinates and off-host geometry intact.
|
|
83
98
|
outline.setPath(clipPath)
|
|
84
99
|
} else {
|
|
85
|
-
outline.
|
|
86
|
-
outlineLeft,
|
|
87
|
-
outlineTop,
|
|
88
|
-
outlineRight,
|
|
89
|
-
outlineBottom,
|
|
90
|
-
clipRadius,
|
|
91
|
-
)
|
|
100
|
+
outline.setEmpty()
|
|
92
101
|
}
|
|
93
102
|
}
|
|
94
103
|
}
|
|
95
104
|
|
|
96
105
|
init {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
106
|
+
clipContainer.addView(contentContainer)
|
|
107
|
+
super.addView(clipContainer)
|
|
108
|
+
if (supportsPathOutlineClipping) {
|
|
109
|
+
clipContainer.outlineProvider = clipOutlineProvider
|
|
110
|
+
clipContainer.clipToOutline = true
|
|
111
|
+
}
|
|
112
|
+
clipToOutline = false
|
|
113
|
+
setWillNotDraw(false)
|
|
100
114
|
visibility = INVISIBLE
|
|
101
115
|
importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
|
|
102
116
|
}
|
|
103
117
|
|
|
104
|
-
|
|
105
|
-
x: Double,
|
|
106
|
-
y: Double,
|
|
107
|
-
width: Double,
|
|
108
|
-
height: Double,
|
|
109
|
-
radius: Double,
|
|
110
|
-
) = setClipPresentationDip(x, y, width, height, radius, 0.0, 0.0)
|
|
111
|
-
|
|
118
|
+
/** DIP fallback used until a registered view has pushed host metrics. */
|
|
112
119
|
@DoNotStrip
|
|
113
120
|
fun setClipPresentationDip(
|
|
114
|
-
x: Double,
|
|
115
|
-
y: Double,
|
|
116
|
-
width: Double,
|
|
117
|
-
height: Double,
|
|
118
|
-
radius: Double,
|
|
119
|
-
contentTranslateX: Double,
|
|
120
|
-
contentTranslateY: Double,
|
|
121
|
-
) {
|
|
122
|
-
if (!x.isFinite() || !y.isFinite() || !width.isFinite() ||
|
|
123
|
-
!height.isFinite() || !radius.isFinite() ||
|
|
124
|
-
!contentTranslateX.isFinite() || !contentTranslateY.isFinite()
|
|
125
|
-
) {
|
|
126
|
-
return
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
val nextX = PixelUtil.toPixelFromDIP(x).toFloat()
|
|
130
|
-
val nextY = PixelUtil.toPixelFromDIP(y).toFloat()
|
|
131
|
-
val nextWidth = PixelUtil.toPixelFromDIP(width).toFloat()
|
|
132
|
-
val nextHeight = PixelUtil.toPixelFromDIP(height).toFloat()
|
|
133
|
-
val nextRadius = PixelUtil.toPixelFromDIP(radius).toFloat()
|
|
134
|
-
val nextContentTranslateX = PixelUtil.toPixelFromDIP(contentTranslateX).toFloat()
|
|
135
|
-
val nextContentTranslateY = PixelUtil.toPixelFromDIP(contentTranslateY).toFloat()
|
|
136
|
-
if (!nextX.isFinite() || !nextY.isFinite() || !nextWidth.isFinite() ||
|
|
137
|
-
!nextHeight.isFinite() || !nextRadius.isFinite() ||
|
|
138
|
-
!nextContentTranslateX.isFinite() || !nextContentTranslateY.isFinite()
|
|
139
|
-
) {
|
|
140
|
-
return
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
requestedX = nextX
|
|
144
|
-
requestedY = nextY
|
|
145
|
-
requestedWidth = nextWidth
|
|
146
|
-
requestedHeight = nextHeight
|
|
147
|
-
requestedRadius = nextRadius
|
|
148
|
-
requestedTopLeftRadius = nextRadius
|
|
149
|
-
requestedTopRightRadius = nextRadius
|
|
150
|
-
requestedBottomRightRadius = nextRadius
|
|
151
|
-
requestedBottomLeftRadius = nextRadius
|
|
152
|
-
requestedCurveCode = CLIP_CURVE_CIRCULAR
|
|
153
|
-
requestedContentTranslateX = nextContentTranslateX
|
|
154
|
-
requestedContentTranslateY = nextContentTranslateY
|
|
155
|
-
requestedContentScale = 1f
|
|
156
|
-
requestedUsesV2Geometry = false
|
|
157
|
-
applyRequestedGeometry()
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/** V2 DIP fallback used until a registered view has pushed host metrics. */
|
|
161
|
-
@DoNotStrip
|
|
162
|
-
fun setClipPresentationV2Dip(
|
|
163
121
|
x: Double,
|
|
164
122
|
y: Double,
|
|
165
123
|
width: Double,
|
|
@@ -172,13 +130,29 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
|
|
|
172
130
|
contentTranslateX: Double,
|
|
173
131
|
contentTranslateY: Double,
|
|
174
132
|
contentScale: Double,
|
|
133
|
+
shadowEnabled: Boolean = false,
|
|
134
|
+
shadowRed: Double = 0.0,
|
|
135
|
+
shadowGreen: Double = 0.0,
|
|
136
|
+
shadowBlue: Double = 0.0,
|
|
137
|
+
shadowAlpha: Double = 1.0,
|
|
138
|
+
shadowOffsetX: Double = 0.0,
|
|
139
|
+
shadowOffsetY: Double = 0.0,
|
|
140
|
+
shadowBlurRadius: Double = 0.0,
|
|
141
|
+
shadowSpreadDistance: Double = 0.0,
|
|
175
142
|
) {
|
|
176
143
|
if (!x.isFinite() || !y.isFinite() || !width.isFinite() ||
|
|
177
144
|
!height.isFinite() || !topLeftRadius.isFinite() ||
|
|
178
145
|
!topRightRadius.isFinite() || !bottomRightRadius.isFinite() ||
|
|
179
146
|
!bottomLeftRadius.isFinite() || !contentTranslateX.isFinite() ||
|
|
180
147
|
!contentTranslateY.isFinite() || !contentScale.isFinite() ||
|
|
181
|
-
|
|
148
|
+
!shadowRed.isFinite() || !shadowGreen.isFinite() ||
|
|
149
|
+
!shadowBlue.isFinite() || !shadowAlpha.isFinite() ||
|
|
150
|
+
!shadowOffsetX.isFinite() ||
|
|
151
|
+
!shadowOffsetY.isFinite() || !shadowBlurRadius.isFinite() ||
|
|
152
|
+
!shadowSpreadDistance.isFinite() || contentScale <= 0.0 ||
|
|
153
|
+
shadowRed !in 0.0..1.0 || shadowGreen !in 0.0..1.0 ||
|
|
154
|
+
shadowBlue !in 0.0..1.0 || shadowAlpha !in 0.0..1.0 ||
|
|
155
|
+
shadowBlurRadius < 0.0 ||
|
|
182
156
|
(curveCode != CLIP_CURVE_CIRCULAR && curveCode != CLIP_CURVE_CONTINUOUS)
|
|
183
157
|
) {
|
|
184
158
|
return
|
|
@@ -195,12 +169,19 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
|
|
|
195
169
|
val nextContentTranslateX = PixelUtil.toPixelFromDIP(contentTranslateX).toFloat()
|
|
196
170
|
val nextContentTranslateY = PixelUtil.toPixelFromDIP(contentTranslateY).toFloat()
|
|
197
171
|
val nextContentScale = contentScale.toFloat()
|
|
172
|
+
val nextShadowOffsetX = PixelUtil.toPixelFromDIP(shadowOffsetX).toFloat()
|
|
173
|
+
val nextShadowOffsetY = PixelUtil.toPixelFromDIP(shadowOffsetY).toFloat()
|
|
174
|
+
val nextShadowBlurRadius = PixelUtil.toPixelFromDIP(shadowBlurRadius).toFloat()
|
|
175
|
+
val nextShadowSpreadDistance =
|
|
176
|
+
PixelUtil.toPixelFromDIP(shadowSpreadDistance).toFloat()
|
|
198
177
|
if (!nextX.isFinite() || !nextY.isFinite() || !nextWidth.isFinite() ||
|
|
199
178
|
!nextHeight.isFinite() || !nextTopLeftRadius.isFinite() ||
|
|
200
179
|
!nextTopRightRadius.isFinite() || !nextBottomRightRadius.isFinite() ||
|
|
201
180
|
!nextBottomLeftRadius.isFinite() || !nextContentTranslateX.isFinite() ||
|
|
202
181
|
!nextContentTranslateY.isFinite() || !nextContentScale.isFinite() ||
|
|
203
|
-
|
|
182
|
+
!nextShadowOffsetX.isFinite() || !nextShadowOffsetY.isFinite() ||
|
|
183
|
+
!nextShadowBlurRadius.isFinite() ||
|
|
184
|
+
!nextShadowSpreadDistance.isFinite() || nextContentScale <= 0f
|
|
204
185
|
) {
|
|
205
186
|
return
|
|
206
187
|
}
|
|
@@ -209,7 +190,6 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
|
|
|
209
190
|
requestedY = nextY
|
|
210
191
|
requestedWidth = nextWidth
|
|
211
192
|
requestedHeight = nextHeight
|
|
212
|
-
requestedRadius = 0f
|
|
213
193
|
requestedTopLeftRadius = nextTopLeftRadius
|
|
214
194
|
requestedTopRightRadius = nextTopRightRadius
|
|
215
195
|
requestedBottomRightRadius = nextBottomRightRadius
|
|
@@ -218,47 +198,23 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
|
|
|
218
198
|
requestedContentTranslateX = nextContentTranslateX
|
|
219
199
|
requestedContentTranslateY = nextContentTranslateY
|
|
220
200
|
requestedContentScale = nextContentScale
|
|
221
|
-
|
|
201
|
+
storeShadow(
|
|
202
|
+
shadowEnabled,
|
|
203
|
+
shadowRed.toFloat(),
|
|
204
|
+
shadowGreen.toFloat(),
|
|
205
|
+
shadowBlue.toFloat(),
|
|
206
|
+
shadowAlpha.toFloat(),
|
|
207
|
+
nextShadowOffsetX,
|
|
208
|
+
nextShadowOffsetY,
|
|
209
|
+
nextShadowBlurRadius,
|
|
210
|
+
nextShadowSpreadDistance,
|
|
211
|
+
)
|
|
222
212
|
applyRequestedGeometry()
|
|
223
213
|
}
|
|
224
214
|
|
|
225
|
-
/**
|
|
226
|
-
* Driver hot path: called from the C++ registry with final pixel-space,
|
|
227
|
-
* pre-normalized values (see deliverToView in SmoothClipRegistry.cpp).
|
|
228
|
-
* No validation, scaling or normalization happens here.
|
|
229
|
-
*/
|
|
215
|
+
/** Driver hot path. Geometry is canonical, raw, and host-independent. */
|
|
230
216
|
@DoNotStrip
|
|
231
217
|
fun setClipPresentationPx(
|
|
232
|
-
left: Float,
|
|
233
|
-
top: Float,
|
|
234
|
-
right: Float,
|
|
235
|
-
bottom: Float,
|
|
236
|
-
radius: Float,
|
|
237
|
-
contentTranslateX: Float,
|
|
238
|
-
contentTranslateY: Float,
|
|
239
|
-
) {
|
|
240
|
-
// Debug-only Perfetto slice for the Kotlin share of the C++
|
|
241
|
-
// SmoothClip.setPresentation section (see SmoothClipTrace.h).
|
|
242
|
-
if (BuildConfig.DEBUG) Trace.beginSection("SmoothClip.applyPx")
|
|
243
|
-
try {
|
|
244
|
-
// Stored only; applyNormalizedClipPx computes the clip residual and
|
|
245
|
-
// applyClipPlacement writes both translations together, because the
|
|
246
|
-
// content's final offset depends on the residual as well.
|
|
247
|
-
requestedContentTranslateX = contentTranslateX
|
|
248
|
-
requestedContentTranslateY = contentTranslateY
|
|
249
|
-
requestedContentScale = 1f
|
|
250
|
-
applyNormalizedClipPx(left, top, right, bottom, radius)
|
|
251
|
-
} finally {
|
|
252
|
-
if (BuildConfig.DEBUG) Trace.endSection()
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
/**
|
|
257
|
-
* V2 driver hot path. Geometry is already intersected with the host and
|
|
258
|
-
* its four radii have already had the CSS overlap factor applied in C++.
|
|
259
|
-
*/
|
|
260
|
-
@DoNotStrip
|
|
261
|
-
fun setClipPresentationV2Px(
|
|
262
218
|
left: Float,
|
|
263
219
|
top: Float,
|
|
264
220
|
right: Float,
|
|
@@ -271,24 +227,51 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
|
|
|
271
227
|
contentTranslateX: Float,
|
|
272
228
|
contentTranslateY: Float,
|
|
273
229
|
contentScale: Float,
|
|
230
|
+
shadowEnabled: Boolean = false,
|
|
231
|
+
shadowRed: Float = 0f,
|
|
232
|
+
shadowGreen: Float = 0f,
|
|
233
|
+
shadowBlue: Float = 0f,
|
|
234
|
+
shadowAlpha: Float = 1f,
|
|
235
|
+
shadowOffsetX: Float = 0f,
|
|
236
|
+
shadowOffsetY: Float = 0f,
|
|
237
|
+
shadowBlurRadius: Float = 0f,
|
|
238
|
+
shadowSpreadDistance: Float = 0f,
|
|
274
239
|
) {
|
|
275
240
|
if (!left.isFinite() || !top.isFinite() || !right.isFinite() ||
|
|
276
241
|
!bottom.isFinite() || !topLeftRadius.isFinite() ||
|
|
277
242
|
!topRightRadius.isFinite() || !bottomRightRadius.isFinite() ||
|
|
278
243
|
!bottomLeftRadius.isFinite() || !contentTranslateX.isFinite() ||
|
|
279
244
|
!contentTranslateY.isFinite() || !contentScale.isFinite() ||
|
|
280
|
-
|
|
245
|
+
!shadowRed.isFinite() || !shadowGreen.isFinite() ||
|
|
246
|
+
!shadowBlue.isFinite() || !shadowAlpha.isFinite() ||
|
|
247
|
+
!shadowOffsetX.isFinite() ||
|
|
248
|
+
!shadowOffsetY.isFinite() || !shadowBlurRadius.isFinite() ||
|
|
249
|
+
!shadowSpreadDistance.isFinite() || contentScale <= 0f ||
|
|
250
|
+
shadowRed !in 0f..1f || shadowGreen !in 0f..1f ||
|
|
251
|
+
shadowBlue !in 0f..1f || shadowAlpha !in 0f..1f ||
|
|
252
|
+
shadowBlurRadius < 0f ||
|
|
281
253
|
(curveCode != CLIP_CURVE_CIRCULAR && curveCode != CLIP_CURVE_CONTINUOUS)
|
|
282
254
|
) {
|
|
283
255
|
return
|
|
284
256
|
}
|
|
285
257
|
|
|
286
|
-
if (BuildConfig.DEBUG) Trace.beginSection("SmoothClip.
|
|
258
|
+
if (BuildConfig.DEBUG) Trace.beginSection("SmoothClip.applyPresentationPx")
|
|
287
259
|
try {
|
|
288
260
|
requestedContentTranslateX = contentTranslateX
|
|
289
261
|
requestedContentTranslateY = contentTranslateY
|
|
290
262
|
requestedContentScale = contentScale
|
|
291
|
-
|
|
263
|
+
storeShadow(
|
|
264
|
+
shadowEnabled,
|
|
265
|
+
shadowRed,
|
|
266
|
+
shadowGreen,
|
|
267
|
+
shadowBlue,
|
|
268
|
+
shadowAlpha,
|
|
269
|
+
shadowOffsetX,
|
|
270
|
+
shadowOffsetY,
|
|
271
|
+
shadowBlurRadius,
|
|
272
|
+
shadowSpreadDistance,
|
|
273
|
+
)
|
|
274
|
+
applyCanonicalClipPx(
|
|
292
275
|
left,
|
|
293
276
|
top,
|
|
294
277
|
right,
|
|
@@ -305,135 +288,155 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
|
|
|
305
288
|
}
|
|
306
289
|
|
|
307
290
|
private fun applyRequestedGeometry() {
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
bottomLeft,
|
|
331
|
-
curve,
|
|
332
|
-
)
|
|
333
|
-
}
|
|
334
|
-
} else {
|
|
335
|
-
normalizeClipGeometryPx(
|
|
336
|
-
requestedX,
|
|
337
|
-
requestedY,
|
|
338
|
-
requestedWidth,
|
|
339
|
-
requestedHeight,
|
|
340
|
-
requestedRadius,
|
|
341
|
-
width.toFloat(),
|
|
342
|
-
height.toFloat(),
|
|
343
|
-
) { left, top, right, bottom, radius ->
|
|
344
|
-
applyNormalizedClipPx(left, top, right, bottom, radius)
|
|
345
|
-
}
|
|
291
|
+
canonicalizeClipGeometryPx(
|
|
292
|
+
requestedX,
|
|
293
|
+
requestedY,
|
|
294
|
+
requestedWidth,
|
|
295
|
+
requestedHeight,
|
|
296
|
+
requestedTopLeftRadius,
|
|
297
|
+
requestedTopRightRadius,
|
|
298
|
+
requestedBottomRightRadius,
|
|
299
|
+
requestedBottomLeftRadius,
|
|
300
|
+
requestedCurveCode,
|
|
301
|
+
) { left, top, right, bottom, topLeft, topRight, bottomRight, bottomLeft, curve ->
|
|
302
|
+
applyCanonicalClipPx(
|
|
303
|
+
left,
|
|
304
|
+
top,
|
|
305
|
+
right,
|
|
306
|
+
bottom,
|
|
307
|
+
topLeft,
|
|
308
|
+
topRight,
|
|
309
|
+
bottomRight,
|
|
310
|
+
bottomLeft,
|
|
311
|
+
curve,
|
|
312
|
+
)
|
|
346
313
|
}
|
|
347
314
|
}
|
|
348
315
|
|
|
349
|
-
private fun
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
316
|
+
private fun storeShadow(
|
|
317
|
+
enabled: Boolean,
|
|
318
|
+
red: Float,
|
|
319
|
+
green: Float,
|
|
320
|
+
blue: Float,
|
|
321
|
+
alpha: Float,
|
|
322
|
+
offsetX: Float,
|
|
323
|
+
offsetY: Float,
|
|
324
|
+
blurRadius: Float,
|
|
325
|
+
spreadDistance: Float,
|
|
355
326
|
) {
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
val
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
327
|
+
val wasVisible = requestedShadowEnabled && requestedShadowAlpha > 0f
|
|
328
|
+
val isVisible = enabled && alpha > 0f
|
|
329
|
+
val paintChanged = red != requestedShadowRed ||
|
|
330
|
+
green != requestedShadowGreen || blue != requestedShadowBlue ||
|
|
331
|
+
alpha != requestedShadowAlpha || blurRadius != requestedShadowBlurRadius
|
|
332
|
+
val pathChanged = offsetX != requestedShadowOffsetX ||
|
|
333
|
+
offsetY != requestedShadowOffsetY ||
|
|
334
|
+
spreadDistance != requestedShadowSpreadDistance
|
|
335
|
+
requestedShadowEnabled = enabled
|
|
336
|
+
requestedShadowRed = red
|
|
337
|
+
requestedShadowGreen = green
|
|
338
|
+
requestedShadowBlue = blue
|
|
339
|
+
requestedShadowAlpha = alpha
|
|
340
|
+
requestedShadowOffsetX = offsetX
|
|
341
|
+
requestedShadowOffsetY = offsetY
|
|
342
|
+
requestedShadowBlurRadius = blurRadius
|
|
343
|
+
requestedShadowSpreadDistance = spreadDistance
|
|
344
|
+
if (!isVisible) {
|
|
345
|
+
if (wasVisible) invalidate()
|
|
346
|
+
return
|
|
347
|
+
}
|
|
348
|
+
if (paintChanged || boxShadowPaint == null) updateBoxShadowPaint()
|
|
349
|
+
if (pathChanged || !wasVisible || boxShadowPath == null) {
|
|
350
|
+
rebuildBoxShadowPath()
|
|
351
|
+
}
|
|
352
|
+
if (paintChanged || pathChanged || !wasVisible) invalidate()
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
private fun updateBoxShadowPaint() {
|
|
356
|
+
val paint = boxShadowPaint ?: Paint(Paint.ANTI_ALIAS_FLAG).also {
|
|
357
|
+
boxShadowPaint = it
|
|
358
|
+
}
|
|
359
|
+
paint.color = Color.argb(
|
|
360
|
+
(requestedShadowAlpha * 255f).roundToInt(),
|
|
361
|
+
(requestedShadowRed * 255f).roundToInt(),
|
|
362
|
+
(requestedShadowGreen * 255f).roundToInt(),
|
|
363
|
+
(requestedShadowBlue * 255f).roundToInt(),
|
|
384
364
|
)
|
|
365
|
+
val sigmaPx = requestedShadowBlurRadius * 0.5f
|
|
366
|
+
val maskRadius = if (sigmaPx > 0.5f) {
|
|
367
|
+
(sigmaPx - 0.5f) / 0.57735f
|
|
368
|
+
} else {
|
|
369
|
+
0f
|
|
370
|
+
}
|
|
371
|
+
paint.maskFilter = if (maskRadius > 0f) {
|
|
372
|
+
BlurMaskFilter(maskRadius, BlurMaskFilter.Blur.NORMAL)
|
|
373
|
+
} else {
|
|
374
|
+
null
|
|
375
|
+
}
|
|
376
|
+
}
|
|
385
377
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
clipTopRightRadius = radius
|
|
393
|
-
clipBottomRightRadius = radius
|
|
394
|
-
clipBottomLeftRadius = radius
|
|
395
|
-
clipCurveCode = CLIP_CURVE_CIRCULAR
|
|
396
|
-
// Whatever integer rounding threw away, carried on this view's own
|
|
397
|
-
// translation so the clip edge still lands where the driver asked
|
|
398
|
-
// instead of snapping to the pixel grid while the content it clips
|
|
399
|
-
// slides in floats. It is a single scalar per axis only because the far
|
|
400
|
-
// edges are derived from the origin — with independently rounded edges
|
|
401
|
-
// no one translation could place both.
|
|
402
|
-
clipResidualX = left - nextOutlineLeft
|
|
403
|
-
clipResidualY = top - nextOutlineTop
|
|
404
|
-
applyClipPlacement()
|
|
405
|
-
|
|
406
|
-
if (isEmpty != clipIsEmpty) {
|
|
407
|
-
if (isEmpty) cancelAcceptedTouchStream()
|
|
408
|
-
clipIsEmpty = isEmpty
|
|
409
|
-
visibility = clipVisibility(isEmpty)
|
|
410
|
-
importantForAccessibility = clipAccessibility(
|
|
411
|
-
isEmpty,
|
|
412
|
-
requestedImportantForAccessibility,
|
|
413
|
-
)
|
|
378
|
+
private fun adjustedRadiusForSpread(radius: Float, spread: Float): Float {
|
|
379
|
+
val magnitude = abs(spread)
|
|
380
|
+
val multiplier = if (magnitude > 0f && radius < magnitude) {
|
|
381
|
+
1f + (radius / magnitude - 1f).pow(3)
|
|
382
|
+
} else {
|
|
383
|
+
1f
|
|
414
384
|
}
|
|
385
|
+
return (radius + spread * multiplier).coerceAtLeast(0f)
|
|
386
|
+
}
|
|
415
387
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
388
|
+
private fun rebuildBoxShadowPath() {
|
|
389
|
+
if (!requestedShadowEnabled || requestedShadowAlpha <= 0f) return
|
|
390
|
+
val path = boxShadowPath ?: Path().also { boxShadowPath = it }
|
|
391
|
+
path.reset()
|
|
392
|
+
if (clipIsEmpty) return
|
|
393
|
+
val spread = requestedShadowSpreadDistance
|
|
394
|
+
val left = clipLeft - spread + requestedShadowOffsetX
|
|
395
|
+
val top = clipTop - spread + requestedShadowOffsetY
|
|
396
|
+
val right = clipRight + spread + requestedShadowOffsetX
|
|
397
|
+
val bottom = clipBottom + spread + requestedShadowOffsetY
|
|
398
|
+
if (right <= left || bottom <= top) return
|
|
399
|
+
appendRoundedRectPath(
|
|
400
|
+
path,
|
|
401
|
+
left,
|
|
402
|
+
top,
|
|
403
|
+
right,
|
|
404
|
+
bottom,
|
|
405
|
+
adjustedRadiusForSpread(clipTopLeftRadius, spread),
|
|
406
|
+
adjustedRadiusForSpread(clipTopRightRadius, spread),
|
|
407
|
+
adjustedRadiusForSpread(clipBottomRightRadius, spread),
|
|
408
|
+
adjustedRadiusForSpread(clipBottomLeftRadius, spread),
|
|
409
|
+
clipCurveCode,
|
|
410
|
+
)
|
|
411
|
+
}
|
|
419
412
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
413
|
+
override fun dispatchDraw(canvas: Canvas) {
|
|
414
|
+
val hostSaveCount = canvas.save()
|
|
415
|
+
canvas.clipRect(0f, 0f, width.toFloat(), height.toFloat())
|
|
416
|
+
try {
|
|
417
|
+
val shadowPath = boxShadowPath
|
|
418
|
+
val shadowPaint = boxShadowPaint
|
|
419
|
+
if (requestedShadowEnabled && !clipIsEmpty &&
|
|
420
|
+
requestedShadowAlpha > 0f && shadowPath != null &&
|
|
421
|
+
shadowPaint != null && !shadowPath.isEmpty
|
|
422
|
+
) {
|
|
423
|
+
val apertureSaveCount = canvas.save()
|
|
424
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
425
|
+
canvas.clipOutPath(clipPath)
|
|
426
|
+
} else {
|
|
427
|
+
@Suppress("DEPRECATION")
|
|
428
|
+
canvas.clipPath(clipPath, Region.Op.DIFFERENCE)
|
|
429
|
+
}
|
|
430
|
+
canvas.drawPath(shadowPath, shadowPaint)
|
|
431
|
+
canvas.restoreToCount(apertureSaveCount)
|
|
432
|
+
}
|
|
433
|
+
super.dispatchDraw(canvas)
|
|
434
|
+
} finally {
|
|
435
|
+
canvas.restoreToCount(hostSaveCount)
|
|
436
|
+
}
|
|
434
437
|
}
|
|
435
438
|
|
|
436
|
-
private fun
|
|
439
|
+
private fun applyCanonicalClipPx(
|
|
437
440
|
left: Float,
|
|
438
441
|
top: Float,
|
|
439
442
|
right: Float,
|
|
@@ -444,24 +447,9 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
|
|
|
444
447
|
bottomLeftRadius: Float,
|
|
445
448
|
curveCode: Int,
|
|
446
449
|
) {
|
|
447
|
-
val radiiAreUniform = topLeftRadius == topRightRadius &&
|
|
448
|
-
topLeftRadius == bottomRightRadius &&
|
|
449
|
-
topLeftRadius == bottomLeftRadius
|
|
450
|
-
val needsPath = curveCode == CLIP_CURVE_CONTINUOUS || !radiiAreUniform
|
|
451
|
-
if (!needsPath) {
|
|
452
|
-
applyNormalizedFloatRoundRectV2Px(
|
|
453
|
-
left,
|
|
454
|
-
top,
|
|
455
|
-
right,
|
|
456
|
-
bottom,
|
|
457
|
-
topLeftRadius,
|
|
458
|
-
)
|
|
459
|
-
return
|
|
460
|
-
}
|
|
461
|
-
|
|
462
450
|
val isEmpty = right <= left || bottom <= top
|
|
463
|
-
val
|
|
464
|
-
|
|
451
|
+
val geometryChanged = left != clipLeft || top != clipTop ||
|
|
452
|
+
right != clipRight ||
|
|
465
453
|
bottom != clipBottom || topLeftRadius != clipTopLeftRadius ||
|
|
466
454
|
topRightRadius != clipTopRightRadius ||
|
|
467
455
|
bottomRightRadius != clipBottomRightRadius ||
|
|
@@ -471,122 +459,60 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
|
|
|
471
459
|
clipTop = top
|
|
472
460
|
clipRight = right
|
|
473
461
|
clipBottom = bottom
|
|
474
|
-
|
|
462
|
+
val radiiAreUniform = topLeftRadius == topRightRadius &&
|
|
463
|
+
topLeftRadius == bottomRightRadius &&
|
|
464
|
+
topLeftRadius == bottomLeftRadius
|
|
475
465
|
clipTopLeftRadius = topLeftRadius
|
|
476
466
|
clipTopRightRadius = topRightRadius
|
|
477
467
|
clipBottomRightRadius = bottomRightRadius
|
|
478
468
|
clipBottomLeftRadius = bottomLeftRadius
|
|
479
469
|
clipCurveCode = curveCode
|
|
480
|
-
|
|
481
|
-
// integer round rect they need no residual translation compensation.
|
|
482
|
-
clipResidualX = 0f
|
|
483
|
-
clipResidualY = 0f
|
|
484
|
-
applyClipPlacement()
|
|
485
|
-
|
|
486
|
-
if (isEmpty != clipIsEmpty) {
|
|
487
|
-
if (isEmpty) cancelAcceptedTouchStream()
|
|
488
|
-
clipIsEmpty = isEmpty
|
|
489
|
-
visibility = clipVisibility(isEmpty)
|
|
490
|
-
importantForAccessibility = clipAccessibility(
|
|
491
|
-
isEmpty,
|
|
492
|
-
requestedImportantForAccessibility,
|
|
493
|
-
)
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
if (!outlineGeometryChanged) return
|
|
497
|
-
|
|
498
|
-
outlineUsesPath = true
|
|
499
|
-
outlineUsesFloatRoundRect = false
|
|
500
|
-
clipPath.reset()
|
|
501
|
-
if (!isEmpty) {
|
|
502
|
-
appendRoundedRectPath(
|
|
503
|
-
clipPath,
|
|
504
|
-
left,
|
|
505
|
-
top,
|
|
506
|
-
right,
|
|
507
|
-
bottom,
|
|
508
|
-
topLeftRadius,
|
|
509
|
-
topRightRadius,
|
|
510
|
-
bottomRightRadius,
|
|
511
|
-
bottomLeftRadius,
|
|
512
|
-
curveCode,
|
|
513
|
-
)
|
|
514
|
-
}
|
|
515
|
-
invalidateOutline()
|
|
516
|
-
}
|
|
470
|
+
applyContentTransform()
|
|
517
471
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
* pixels. V1 intentionally continues through applyNormalizedClipPx so its
|
|
521
|
-
* integer outline and residual placement remain byte-for-byte compatible.
|
|
522
|
-
*/
|
|
523
|
-
private fun applyNormalizedFloatRoundRectV2Px(
|
|
524
|
-
left: Float,
|
|
525
|
-
top: Float,
|
|
526
|
-
right: Float,
|
|
527
|
-
bottom: Float,
|
|
528
|
-
radius: Float,
|
|
529
|
-
) {
|
|
530
|
-
val isEmpty = right <= left || bottom <= top
|
|
531
|
-
val outlineGeometryChanged = outlineUsesPath ||
|
|
532
|
-
!outlineUsesFloatRoundRect ||
|
|
533
|
-
left != clipLeft || top != clipTop || right != clipRight ||
|
|
534
|
-
bottom != clipBottom || radius != clipRadius
|
|
472
|
+
clipIsEmpty = isEmpty
|
|
473
|
+
reapplyClipPresentation()
|
|
535
474
|
|
|
536
|
-
|
|
537
|
-
clipTop = top
|
|
538
|
-
clipRight = right
|
|
539
|
-
clipBottom = bottom
|
|
540
|
-
clipRadius = radius
|
|
541
|
-
clipTopLeftRadius = radius
|
|
542
|
-
clipTopRightRadius = radius
|
|
543
|
-
clipBottomRightRadius = radius
|
|
544
|
-
clipBottomLeftRadius = radius
|
|
545
|
-
clipCurveCode = CLIP_CURVE_CIRCULAR
|
|
546
|
-
clipResidualX = 0f
|
|
547
|
-
clipResidualY = 0f
|
|
548
|
-
applyClipPlacement()
|
|
549
|
-
|
|
550
|
-
if (isEmpty != clipIsEmpty) {
|
|
551
|
-
if (isEmpty) cancelAcceptedTouchStream()
|
|
552
|
-
clipIsEmpty = isEmpty
|
|
553
|
-
visibility = clipVisibility(isEmpty)
|
|
554
|
-
importantForAccessibility = clipAccessibility(
|
|
555
|
-
isEmpty,
|
|
556
|
-
requestedImportantForAccessibility,
|
|
557
|
-
)
|
|
558
|
-
}
|
|
475
|
+
if (!geometryChanged) return
|
|
559
476
|
|
|
560
|
-
if (!outlineGeometryChanged) return
|
|
561
|
-
outlineUsesPath = false
|
|
562
|
-
outlineUsesFloatRoundRect = true
|
|
563
477
|
clipPath.reset()
|
|
564
478
|
if (!isEmpty) {
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
479
|
+
if (curveCode == CLIP_CURVE_CIRCULAR && radiiAreUniform) {
|
|
480
|
+
clipPath.addRoundRect(
|
|
481
|
+
left,
|
|
482
|
+
top,
|
|
483
|
+
right,
|
|
484
|
+
bottom,
|
|
485
|
+
topLeftRadius,
|
|
486
|
+
topLeftRadius,
|
|
487
|
+
Path.Direction.CW,
|
|
488
|
+
)
|
|
489
|
+
} else {
|
|
490
|
+
appendRoundedRectPath(
|
|
491
|
+
clipPath,
|
|
492
|
+
left,
|
|
493
|
+
top,
|
|
494
|
+
right,
|
|
495
|
+
bottom,
|
|
496
|
+
topLeftRadius,
|
|
497
|
+
topRightRadius,
|
|
498
|
+
bottomRightRadius,
|
|
499
|
+
bottomLeftRadius,
|
|
500
|
+
curveCode,
|
|
501
|
+
)
|
|
502
|
+
}
|
|
574
503
|
}
|
|
575
|
-
|
|
504
|
+
rebuildBoxShadowPath()
|
|
505
|
+
if (supportsPathOutlineClipping) {
|
|
506
|
+
clipContainer.invalidateOutline()
|
|
507
|
+
} else {
|
|
508
|
+
clipContainer.invalidate()
|
|
509
|
+
}
|
|
510
|
+
if (requestedShadowEnabled) invalidate()
|
|
576
511
|
}
|
|
577
512
|
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
* so only the clip edge moves and the content stays exactly where the
|
|
582
|
-
* driver put it. Every write self-dedupes inside View.setTranslationX, so
|
|
583
|
-
* calling this on an unchanged frame costs four float comparisons.
|
|
584
|
-
*/
|
|
585
|
-
private fun applyClipPlacement() {
|
|
586
|
-
super.setTranslationX(userTranslationX + clipResidualX)
|
|
587
|
-
super.setTranslationY(userTranslationY + clipResidualY)
|
|
588
|
-
contentContainer.translationX = requestedContentTranslateX - clipResidualX
|
|
589
|
-
contentContainer.translationY = requestedContentTranslateY - clipResidualY
|
|
513
|
+
private fun applyContentTransform() {
|
|
514
|
+
contentContainer.translationX = requestedContentTranslateX
|
|
515
|
+
contentContainer.translationY = requestedContentTranslateY
|
|
590
516
|
// Scale is intentionally centered and lives on the content only. View
|
|
591
517
|
// translation properties are applied independently of scale, so a
|
|
592
518
|
// caller's tx/ty remains a physical-pixel offset rather than scaling
|
|
@@ -597,36 +523,14 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
|
|
|
597
523
|
contentContainer.scaleY = requestedContentScale
|
|
598
524
|
}
|
|
599
525
|
|
|
600
|
-
// The clip's sub-pixel placement and the consumer's `transform` prop share
|
|
601
|
-
// one property, so they are composed rather than allowed to overwrite each
|
|
602
|
-
// other. RN routes every transform write through these setters
|
|
603
|
-
// (BaseViewManager.setTransformProperty), so intercepting them is enough.
|
|
604
|
-
//
|
|
605
|
-
// The getters are deliberately NOT overridden to hide the residual:
|
|
606
|
-
// View.setTranslationX dedupes by calling getTranslationX() virtually, so
|
|
607
|
-
// reporting the consumer's value there would make every write look like a
|
|
608
|
-
// change and re-invalidate the view on frames where nothing moved. The cost
|
|
609
|
-
// of leaving them alone is that a read-modify-write of translationX picks up
|
|
610
|
-
// the residual twice — sub-pixel, and nothing in RN's transform path reads
|
|
611
|
-
// back before writing.
|
|
612
|
-
override fun setTranslationX(translationX: Float) {
|
|
613
|
-
userTranslationX = translationX
|
|
614
|
-
super.setTranslationX(translationX + clipResidualX)
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
override fun setTranslationY(translationY: Float) {
|
|
618
|
-
userTranslationY = translationY
|
|
619
|
-
super.setTranslationY(translationY + clipResidualY)
|
|
620
|
-
}
|
|
621
|
-
|
|
622
526
|
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
|
623
527
|
super.onSizeChanged(w, h, oldw, oldh)
|
|
528
|
+
clipContainer.layout(0, 0, w, h)
|
|
624
529
|
contentContainer.layout(0, 0, w, h)
|
|
625
|
-
|
|
530
|
+
applyContentTransform()
|
|
626
531
|
if (boundDriverId != 0.0) {
|
|
627
|
-
//
|
|
628
|
-
//
|
|
629
|
-
// visible value synchronously.
|
|
532
|
+
// Host metrics only gate lifecycle readiness. Redelivery remains
|
|
533
|
+
// raw and canonical; this view owns the final viewport crop.
|
|
630
534
|
SmoothClipBindings.nativeSetViewHostGeometry(
|
|
631
535
|
boundDriverId,
|
|
632
536
|
this,
|
|
@@ -641,6 +545,7 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
|
|
|
641
545
|
// authoritative command wins over the driver value on resize.
|
|
642
546
|
applyRequestedGeometry()
|
|
643
547
|
}
|
|
548
|
+
reapplyClipPresentation()
|
|
644
549
|
}
|
|
645
550
|
|
|
646
551
|
override fun onAttachedToWindow() {
|
|
@@ -695,89 +600,33 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
|
|
|
695
600
|
}
|
|
696
601
|
|
|
697
602
|
private fun containsRoundedPoint(x: Float, y: Float): Boolean {
|
|
698
|
-
//
|
|
699
|
-
//
|
|
700
|
-
|
|
701
|
-
// so hit testing stays against the geometry the driver delivered — the
|
|
702
|
-
// same test as before the residual existed.
|
|
703
|
-
if (outlineUsesPath) {
|
|
704
|
-
// Reuse the exact cubic path supplied to Outline.setPath: hit
|
|
705
|
-
// testing must follow the rendered aperture, including its
|
|
706
|
-
// portable continuous-curve approximation.
|
|
707
|
-
return !clipIsEmpty && containsPathPoint(clipPath, x, y)
|
|
708
|
-
}
|
|
709
|
-
return containsRoundedPointPx(
|
|
710
|
-
x + clipResidualX,
|
|
711
|
-
y + clipResidualY,
|
|
712
|
-
clipLeft,
|
|
713
|
-
clipTop,
|
|
714
|
-
clipRight,
|
|
715
|
-
clipBottom,
|
|
716
|
-
clipRadius,
|
|
717
|
-
clipIsEmpty,
|
|
718
|
-
)
|
|
603
|
+
// Reuse the exact path supplied to the Outline. Hit testing therefore
|
|
604
|
+
// follows the rendered aperture, including continuous corners.
|
|
605
|
+
return !clipIsEmpty && containsPathPoint(clipPath, x, y)
|
|
719
606
|
}
|
|
720
607
|
|
|
721
608
|
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
retainTouchEvent(event)
|
|
730
|
-
} else {
|
|
731
|
-
clearTouchState()
|
|
732
|
-
}
|
|
733
|
-
return accepted
|
|
734
|
-
}
|
|
735
|
-
MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> {
|
|
736
|
-
val result = if (acceptingTouchStream) {
|
|
737
|
-
super.dispatchTouchEvent(event)
|
|
738
|
-
} else {
|
|
739
|
-
false
|
|
740
|
-
}
|
|
741
|
-
clearTouchState()
|
|
742
|
-
return result
|
|
743
|
-
}
|
|
609
|
+
val action = event.actionMasked
|
|
610
|
+
if (action == MotionEvent.ACTION_DOWN) {
|
|
611
|
+
// The aperture gates admission only. Once accepted, the stream
|
|
612
|
+
// belongs to Android until its real UP or CANCEL arrives.
|
|
613
|
+
clearTouchState()
|
|
614
|
+
if (!containsRoundedPoint(event.x, event.y)) return false
|
|
615
|
+
acceptingTouchStream = true
|
|
744
616
|
}
|
|
745
617
|
|
|
746
618
|
if (!acceptingTouchStream) return false
|
|
747
619
|
val result = super.dispatchTouchEvent(event)
|
|
748
|
-
|
|
620
|
+
if ((action == MotionEvent.ACTION_DOWN && !result) ||
|
|
621
|
+
action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP
|
|
622
|
+
) {
|
|
623
|
+
clearTouchState()
|
|
624
|
+
}
|
|
749
625
|
return result
|
|
750
626
|
}
|
|
751
627
|
|
|
752
|
-
private fun retainTouchEvent(event: MotionEvent) {
|
|
753
|
-
retainedTouchEvent?.recycle()
|
|
754
|
-
retainedTouchEvent = MotionEvent.obtain(event)
|
|
755
|
-
}
|
|
756
|
-
|
|
757
628
|
private fun clearTouchState() {
|
|
758
629
|
acceptingTouchStream = false
|
|
759
|
-
retainedTouchEvent?.recycle()
|
|
760
|
-
retainedTouchEvent = null
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
private fun cancelAcceptedTouchStream() {
|
|
764
|
-
val latest = retainedTouchEvent
|
|
765
|
-
if (!acceptingTouchStream || latest == null) {
|
|
766
|
-
clearTouchState()
|
|
767
|
-
return
|
|
768
|
-
}
|
|
769
|
-
val cancel = MotionEvent.obtain(latest)
|
|
770
|
-
cancel.action = MotionEvent.ACTION_CANCEL
|
|
771
|
-
// Terminal BEFORE dispatch: a child's synchronous cancel handler can
|
|
772
|
-
// re-enter geometry application, and a nested emptiness transition
|
|
773
|
-
// must observe an already-ended stream instead of synthesizing a
|
|
774
|
-
// second cancel for it.
|
|
775
|
-
clearTouchState()
|
|
776
|
-
try {
|
|
777
|
-
super.dispatchTouchEvent(cancel)
|
|
778
|
-
} finally {
|
|
779
|
-
cancel.recycle()
|
|
780
|
-
}
|
|
781
630
|
}
|
|
782
631
|
|
|
783
632
|
fun setRequestedImportantForAccessibility(value: Int) {
|
|
@@ -785,14 +634,24 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
|
|
|
785
634
|
reapplyClipPresentation()
|
|
786
635
|
}
|
|
787
636
|
|
|
637
|
+
@DoNotStrip
|
|
638
|
+
fun setAutonomousMotion(active: Boolean) {
|
|
639
|
+
if (autonomousMotion == active) return
|
|
640
|
+
autonomousMotion = active
|
|
641
|
+
reapplyClipPresentation()
|
|
642
|
+
}
|
|
643
|
+
|
|
788
644
|
fun reapplyClipPresentation() {
|
|
789
|
-
val
|
|
645
|
+
val apertureVisible = apertureIntersectsHost()
|
|
646
|
+
val expectedVisibility = renderVisibility(
|
|
647
|
+
apertureVisible || shadowIntersectsHost(),
|
|
648
|
+
)
|
|
790
649
|
if (visibility != expectedVisibility) {
|
|
791
650
|
visibility = expectedVisibility
|
|
792
651
|
}
|
|
793
652
|
|
|
794
653
|
val expectedAccessibility = clipAccessibility(
|
|
795
|
-
|
|
654
|
+
autonomousMotion || !apertureVisible,
|
|
796
655
|
requestedImportantForAccessibility,
|
|
797
656
|
)
|
|
798
657
|
if (importantForAccessibility != expectedAccessibility) {
|
|
@@ -800,6 +659,29 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
|
|
|
800
659
|
}
|
|
801
660
|
}
|
|
802
661
|
|
|
662
|
+
private fun apertureIntersectsHost(): Boolean =
|
|
663
|
+
!clipIsEmpty && clipRight > 0f && clipBottom > 0f &&
|
|
664
|
+
clipLeft < width.toFloat() && clipTop < height.toFloat()
|
|
665
|
+
|
|
666
|
+
private fun shadowIntersectsHost(): Boolean {
|
|
667
|
+
if (clipIsEmpty || !requestedShadowEnabled || requestedShadowAlpha <= 0f) {
|
|
668
|
+
return false
|
|
669
|
+
}
|
|
670
|
+
val spread = requestedShadowSpreadDistance
|
|
671
|
+
val pathLeft = clipLeft - spread + requestedShadowOffsetX
|
|
672
|
+
val pathTop = clipTop - spread + requestedShadowOffsetY
|
|
673
|
+
val pathRight = clipRight + spread + requestedShadowOffsetX
|
|
674
|
+
val pathBottom = clipBottom + spread + requestedShadowOffsetY
|
|
675
|
+
if (pathRight <= pathLeft || pathBottom <= pathTop) return false
|
|
676
|
+
|
|
677
|
+
// CSS blur is specified as a diameter-like radius. Expanding by the
|
|
678
|
+
// full value is conservative and prevents culling a faint blur tail.
|
|
679
|
+
val blurOutset = requestedShadowBlurRadius
|
|
680
|
+
return pathRight + blurOutset > 0f && pathBottom + blurOutset > 0f &&
|
|
681
|
+
pathLeft - blurOutset < width.toFloat() &&
|
|
682
|
+
pathTop - blurOutset < height.toFloat()
|
|
683
|
+
}
|
|
684
|
+
|
|
803
685
|
fun resetClipState() {
|
|
804
686
|
clearTouchState()
|
|
805
687
|
commandIsAuthoritative = false
|
|
@@ -807,7 +689,6 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
|
|
|
807
689
|
requestedY = 0f
|
|
808
690
|
requestedWidth = 0f
|
|
809
691
|
requestedHeight = 0f
|
|
810
|
-
requestedRadius = 0f
|
|
811
692
|
requestedTopLeftRadius = 0f
|
|
812
693
|
requestedTopRightRadius = 0f
|
|
813
694
|
requestedBottomRightRadius = 0f
|
|
@@ -816,17 +697,19 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
|
|
|
816
697
|
requestedContentTranslateX = 0f
|
|
817
698
|
requestedContentTranslateY = 0f
|
|
818
699
|
requestedContentScale = 1f
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
700
|
+
requestedShadowEnabled = false
|
|
701
|
+
requestedShadowRed = 0f
|
|
702
|
+
requestedShadowGreen = 0f
|
|
703
|
+
requestedShadowBlue = 0f
|
|
704
|
+
requestedShadowAlpha = 1f
|
|
705
|
+
requestedShadowOffsetX = 0f
|
|
706
|
+
requestedShadowOffsetY = 0f
|
|
707
|
+
requestedShadowBlurRadius = 0f
|
|
708
|
+
requestedShadowSpreadDistance = 0f
|
|
709
|
+
boxShadowPaint = null
|
|
710
|
+
boxShadowPath = null
|
|
711
|
+
applyContentTransform()
|
|
825
712
|
clipPath.reset()
|
|
826
|
-
outlineLeft = 0
|
|
827
|
-
outlineTop = 0
|
|
828
|
-
outlineRight = 0
|
|
829
|
-
outlineBottom = 0
|
|
830
713
|
requestedImportantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_AUTO
|
|
831
714
|
applyRequestedGeometry()
|
|
832
715
|
}
|