react-native-smooth-clip-view 0.2.9 → 0.4.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.
Files changed (193) hide show
  1. package/README.md +132 -349
  2. package/android/build.gradle +8 -4
  3. package/android/proguard-rules.pro +3 -3
  4. package/android/src/main/cpp/SmoothClipAndroid.h +13 -6
  5. package/android/src/main/cpp/SmoothClipBindings.cpp +605 -123
  6. package/android/src/main/cpp/SmoothClipRegistry.cpp +975 -209
  7. package/android/src/main/java/com/smoothclipview/ClipGeometryNormalizer.kt +192 -118
  8. package/android/src/main/java/com/smoothclipview/SmoothClipBindings.kt +22 -4
  9. package/android/src/main/java/com/smoothclipview/SmoothClipModule.kt +81 -56
  10. package/android/src/main/java/com/smoothclipview/SmoothClipView.kt +449 -240
  11. package/android/src/main/java/com/smoothclipview/SmoothClipViewManager.kt +150 -38
  12. package/cpp/SmoothClipAnimationCurve.h +317 -397
  13. package/cpp/SmoothClipRegistry.h +84 -20
  14. package/cpp/SmoothClipSharedGeometry.h +131 -22
  15. package/cpp/SmoothClipVelocityTracker.h +16 -12
  16. package/ios/SmoothClipGeometry.h +188 -21
  17. package/ios/SmoothClipRegistry.mm +869 -368
  18. package/ios/SmoothClipRegistrySnapshot.h +13 -0
  19. package/ios/SmoothClipTurboModule.cpp +513 -156
  20. package/ios/SmoothClipTurboModule.h +123 -56
  21. package/ios/SmoothClipView.mm +1074 -449
  22. package/ios/SmoothClipViewRegistry.h +3 -7
  23. package/ios/reactnativesmoothclipview.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
  24. package/ios/reactnativesmoothclipview.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  25. package/lib/module/NativeSmoothClipModule.js.map +1 -1
  26. package/lib/module/SmoothClipView.ios.js +94 -11
  27. package/lib/module/SmoothClipView.ios.js.map +1 -1
  28. package/lib/module/SmoothClipView.js +1 -46
  29. package/lib/module/SmoothClipView.js.map +1 -1
  30. package/lib/module/SmoothClipViewNativeComponent.ts +32 -12
  31. package/lib/module/capabilities.js +4 -0
  32. package/lib/module/capabilities.js.map +1 -0
  33. package/lib/module/capabilities.native.js +18 -0
  34. package/lib/module/capabilities.native.js.map +1 -0
  35. package/lib/module/capabilityTypes.js +2 -0
  36. package/lib/module/capabilityTypes.js.map +1 -0
  37. package/lib/module/controllerInternals.js +28 -0
  38. package/lib/module/controllerInternals.js.map +1 -0
  39. package/lib/module/controllerLifecycle.js +9 -0
  40. package/lib/module/controllerLifecycle.js.map +1 -0
  41. package/lib/module/controllerTypes.js +4 -0
  42. package/lib/module/controllerTypes.js.map +1 -0
  43. package/lib/module/controllers.android.js +4 -0
  44. package/lib/module/controllers.android.js.map +1 -0
  45. package/lib/module/controllers.ios.js +4 -0
  46. package/lib/module/controllers.ios.js.map +1 -0
  47. package/lib/module/controllers.js +4 -0
  48. package/lib/module/controllers.js.map +1 -0
  49. package/lib/module/controllers.native.js +89 -0
  50. package/lib/module/controllers.native.js.map +1 -0
  51. package/lib/module/geometry.js +180 -30
  52. package/lib/module/geometry.js.map +1 -1
  53. package/lib/module/groupTypes.js +4 -0
  54. package/lib/module/groupTypes.js.map +1 -0
  55. package/lib/module/groups.android.js +4 -0
  56. package/lib/module/groups.android.js.map +1 -0
  57. package/lib/module/groups.ios.js +4 -0
  58. package/lib/module/groups.ios.js.map +1 -0
  59. package/lib/module/groups.js +4 -0
  60. package/lib/module/groups.js.map +1 -0
  61. package/lib/module/groups.native.js +399 -0
  62. package/lib/module/groups.native.js.map +1 -0
  63. package/lib/module/ids.js +8 -0
  64. package/lib/module/ids.js.map +1 -0
  65. package/lib/module/index.js +4 -3
  66. package/lib/module/index.js.map +1 -1
  67. package/lib/module/nativeCompletion.js +37 -16
  68. package/lib/module/nativeCompletion.js.map +1 -1
  69. package/lib/module/presentationCodec.js +81 -0
  70. package/lib/module/presentationCodec.js.map +1 -0
  71. package/lib/typescript/src/NativeSmoothClipModule.d.ts +20 -4
  72. package/lib/typescript/src/NativeSmoothClipModule.d.ts.map +1 -1
  73. package/lib/typescript/src/SmoothClipView.d.ts +2 -10
  74. package/lib/typescript/src/SmoothClipView.d.ts.map +1 -1
  75. package/lib/typescript/src/SmoothClipView.ios.d.ts +139 -4
  76. package/lib/typescript/src/SmoothClipView.ios.d.ts.map +1 -1
  77. package/lib/typescript/src/SmoothClipViewNativeComponent.d.ts +16 -3
  78. package/lib/typescript/src/SmoothClipViewNativeComponent.d.ts.map +1 -1
  79. package/lib/typescript/src/capabilities.d.ts +3 -0
  80. package/lib/typescript/src/capabilities.d.ts.map +1 -0
  81. package/lib/typescript/src/capabilities.native.d.ts +3 -0
  82. package/lib/typescript/src/capabilities.native.d.ts.map +1 -0
  83. package/lib/typescript/src/capabilityTypes.d.ts +4 -0
  84. package/lib/typescript/src/capabilityTypes.d.ts.map +1 -0
  85. package/lib/typescript/src/controllerInternals.d.ts +14 -0
  86. package/lib/typescript/src/controllerInternals.d.ts.map +1 -0
  87. package/lib/typescript/src/controllerLifecycle.d.ts +3 -0
  88. package/lib/typescript/src/controllerLifecycle.d.ts.map +1 -0
  89. package/lib/typescript/src/controllerTypes.d.ts +62 -0
  90. package/lib/typescript/src/controllerTypes.d.ts.map +1 -0
  91. package/lib/typescript/src/controllers.android.d.ts +2 -0
  92. package/lib/typescript/src/controllers.android.d.ts.map +1 -0
  93. package/lib/typescript/src/controllers.d.ts +2 -0
  94. package/lib/typescript/src/controllers.d.ts.map +1 -0
  95. package/lib/typescript/src/controllers.ios.d.ts +2 -0
  96. package/lib/typescript/src/controllers.ios.d.ts.map +1 -0
  97. package/lib/typescript/src/controllers.native.d.ts +3 -0
  98. package/lib/typescript/src/controllers.native.d.ts.map +1 -0
  99. package/lib/typescript/src/geometry.d.ts +49 -5
  100. package/lib/typescript/src/geometry.d.ts.map +1 -1
  101. package/lib/typescript/src/groupTypes.d.ts +31 -0
  102. package/lib/typescript/src/groupTypes.d.ts.map +1 -0
  103. package/lib/typescript/src/groups.android.d.ts +2 -0
  104. package/lib/typescript/src/groups.android.d.ts.map +1 -0
  105. package/lib/typescript/src/groups.d.ts +2 -0
  106. package/lib/typescript/src/groups.d.ts.map +1 -0
  107. package/lib/typescript/src/groups.ios.d.ts +2 -0
  108. package/lib/typescript/src/groups.ios.d.ts.map +1 -0
  109. package/lib/typescript/src/groups.native.d.ts +3 -0
  110. package/lib/typescript/src/groups.native.d.ts.map +1 -0
  111. package/lib/typescript/src/ids.d.ts +2 -0
  112. package/lib/typescript/src/ids.d.ts.map +1 -0
  113. package/lib/typescript/src/index.d.ts +8 -5
  114. package/lib/typescript/src/index.d.ts.map +1 -1
  115. package/lib/typescript/src/nativeCompletion.d.ts +16 -6
  116. package/lib/typescript/src/nativeCompletion.d.ts.map +1 -1
  117. package/lib/typescript/src/presentationCodec.d.ts +10 -0
  118. package/lib/typescript/src/presentationCodec.d.ts.map +1 -0
  119. package/package.json +17 -13
  120. package/src/NativeSmoothClipModule.ts +80 -56
  121. package/src/SmoothClipView.ios.tsx +142 -16
  122. package/src/SmoothClipView.tsx +6 -76
  123. package/src/SmoothClipViewNativeComponent.ts +32 -12
  124. package/src/capabilities.native.ts +19 -0
  125. package/src/capabilities.ts +2 -0
  126. package/src/capabilityTypes.ts +3 -0
  127. package/src/controllerInternals.ts +52 -0
  128. package/src/controllerLifecycle.ts +6 -0
  129. package/src/controllerTypes.ts +87 -0
  130. package/src/controllers.android.ts +1 -0
  131. package/src/controllers.ios.ts +1 -0
  132. package/src/controllers.native.ts +104 -0
  133. package/src/controllers.ts +1 -0
  134. package/src/geometry.ts +293 -45
  135. package/src/groupTypes.ts +53 -0
  136. package/src/groups.android.ts +1 -0
  137. package/src/groups.ios.ts +1 -0
  138. package/src/groups.native.ts +604 -0
  139. package/src/groups.ts +1 -0
  140. package/src/ids.ts +6 -0
  141. package/src/index.ts +32 -12
  142. package/src/nativeCompletion.ts +67 -32
  143. package/src/presentationCodec.ts +134 -0
  144. package/lib/module/SmoothClipView.web.js +0 -54
  145. package/lib/module/SmoothClipView.web.js.map +0 -1
  146. package/lib/module/driverState.js +0 -110
  147. package/lib/module/driverState.js.map +0 -1
  148. package/lib/module/driverTypes.js +0 -4
  149. package/lib/module/driverTypes.js.map +0 -1
  150. package/lib/module/drivers.android.js +0 -6
  151. package/lib/module/drivers.android.js.map +0 -1
  152. package/lib/module/drivers.ios.js +0 -6
  153. package/lib/module/drivers.ios.js.map +0 -1
  154. package/lib/module/drivers.js +0 -284
  155. package/lib/module/drivers.js.map +0 -1
  156. package/lib/module/drivers.native.js +0 -556
  157. package/lib/module/drivers.native.js.map +0 -1
  158. package/lib/module/index.web.js +0 -8
  159. package/lib/module/index.web.js.map +0 -1
  160. package/lib/module/reactRequests.js +0 -56
  161. package/lib/module/reactRequests.js.map +0 -1
  162. package/lib/module/webClipPath.js +0 -25
  163. package/lib/module/webClipPath.js.map +0 -1
  164. package/lib/typescript/src/SmoothClipView.web.d.ts +0 -10
  165. package/lib/typescript/src/SmoothClipView.web.d.ts.map +0 -1
  166. package/lib/typescript/src/driverState.d.ts +0 -31
  167. package/lib/typescript/src/driverState.d.ts.map +0 -1
  168. package/lib/typescript/src/driverTypes.d.ts +0 -107
  169. package/lib/typescript/src/driverTypes.d.ts.map +0 -1
  170. package/lib/typescript/src/drivers.android.d.ts +0 -3
  171. package/lib/typescript/src/drivers.android.d.ts.map +0 -1
  172. package/lib/typescript/src/drivers.d.ts +0 -5
  173. package/lib/typescript/src/drivers.d.ts.map +0 -1
  174. package/lib/typescript/src/drivers.ios.d.ts +0 -3
  175. package/lib/typescript/src/drivers.ios.d.ts.map +0 -1
  176. package/lib/typescript/src/drivers.native.d.ts +0 -5
  177. package/lib/typescript/src/drivers.native.d.ts.map +0 -1
  178. package/lib/typescript/src/index.web.d.ts +0 -9
  179. package/lib/typescript/src/index.web.d.ts.map +0 -1
  180. package/lib/typescript/src/reactRequests.d.ts +0 -9
  181. package/lib/typescript/src/reactRequests.d.ts.map +0 -1
  182. package/lib/typescript/src/webClipPath.d.ts +0 -7
  183. package/lib/typescript/src/webClipPath.d.ts.map +0 -1
  184. package/src/SmoothClipView.web.tsx +0 -66
  185. package/src/driverState.ts +0 -158
  186. package/src/driverTypes.ts +0 -140
  187. package/src/drivers.android.ts +0 -8
  188. package/src/drivers.ios.ts +0 -8
  189. package/src/drivers.native.ts +0 -899
  190. package/src/drivers.ts +0 -427
  191. package/src/index.web.ts +0 -23
  192. package/src/reactRequests.ts +0 -76
  193. package/src/webClipPath.ts +0 -30
@@ -1,7 +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
9
+ import android.graphics.Path
10
+ import android.graphics.Region
11
+ import android.os.Build
5
12
  import android.os.Trace
6
13
  import android.view.MotionEvent
7
14
  import android.view.View
@@ -10,44 +17,55 @@ import com.facebook.proguard.annotations.DoNotStrip
10
17
  import com.facebook.react.uimanager.PixelUtil
11
18
  import com.facebook.react.uimanager.ThemedReactContext
12
19
  import com.facebook.react.views.view.ReactViewGroup
20
+ import kotlin.math.roundToInt
21
+ import kotlin.math.abs
22
+ import kotlin.math.pow
13
23
 
14
24
  class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
25
+ private val clipContainer = ReactViewGroup(context)
15
26
  internal val contentContainer = ReactViewGroup(context)
16
27
  private var requestedX = 0f
17
28
  private var requestedY = 0f
18
29
  private var requestedWidth = 0f
19
30
  private var requestedHeight = 0f
20
- private var requestedRadius = 0f
31
+ private var requestedTopLeftRadius = 0f
32
+ private var requestedTopRightRadius = 0f
33
+ private var requestedBottomRightRadius = 0f
34
+ private var requestedBottomLeftRadius = 0f
35
+ private var requestedCurveCode = CLIP_CURVE_CIRCULAR
21
36
  private var requestedContentTranslateX = 0f
22
37
  private var requestedContentTranslateY = 0f
38
+ private var requestedContentScale = 1f
39
+ private var requestedShadowEnabled = false
40
+ private var requestedShadowRed = 0f
41
+ private var requestedShadowGreen = 0f
42
+ private var requestedShadowBlue = 0f
43
+ private var requestedShadowAlpha = 1f
44
+ private var requestedShadowOffsetX = 0f
45
+ private var requestedShadowOffsetY = 0f
46
+ private var requestedShadowBlurRadius = 0f
47
+ private var requestedShadowSpreadDistance = 0f
23
48
  private var clipLeft = 0f
24
49
  private var clipTop = 0f
25
50
  private var clipRight = 0f
26
51
  private var clipBottom = 0f
27
- private var clipRadius = 0f
28
- // Rounded edges actually emitted to the outline provider. Sub-pixel changes
29
- // that round to the same integers must not invalidate the outline.
30
- private var outlineLeft = 0
31
- private var outlineTop = 0
32
- private var outlineRight = 0
33
- private var outlineBottom = 0
52
+ private var clipTopLeftRadius = 0f
53
+ private var clipTopRightRadius = 0f
54
+ private var clipBottomRightRadius = 0f
55
+ private var clipBottomLeftRadius = 0f
56
+ private var clipCurveCode = CLIP_CURVE_CIRCULAR
57
+ private val clipPath = Path()
58
+ private var boxShadowPath: Path? = null
59
+ private var boxShadowPaint: Paint? = null
34
60
  private var clipIsEmpty = true
35
- // Sub-pixel remainder of the clip origin, carried on this view's own
36
- // translation and subtracted back out of the content container.
37
- private var clipResidualX = 0f
38
- private var clipResidualY = 0f
39
- // The consumer's `transform` translation, kept apart from the residual so
40
- // the two compose instead of clobbering each other.
41
- private var userTranslationX = 0f
42
- private var userTranslationY = 0f
43
61
  private var requestedImportantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_AUTO
62
+ private var autonomousMotion = false
44
63
  private var acceptingTouchStream = false
45
- private var retainedTouchEvent: MotionEvent? = null
46
64
 
47
65
  /** Driver this view is registered with in the native registry (0 = none). */
48
66
  internal var boundDriverId: Double = 0.0
49
67
 
50
- /** Set once a legacy command arrives; later initialClip* props are ignored. */
68
+ /** Set once a command arrives; later initial presentation props are ignored. */
51
69
  internal var commandIsAuthoritative = false
52
70
 
53
71
  private val clipOutlineProvider = object : ViewOutlineProvider() {
@@ -57,45 +75,62 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
57
75
  return
58
76
  }
59
77
 
60
- outline.setRoundRect(
61
- outlineLeft,
62
- outlineTop,
63
- outlineRight,
64
- outlineBottom,
65
- clipRadius,
66
- )
78
+ // Outline.setRoundRect only accepts integer edges. The raw path
79
+ // keeps sub-pixel coordinates and off-host geometry intact.
80
+ outline.setPath(clipPath)
67
81
  }
68
82
  }
69
83
 
70
84
  init {
71
- super.addView(contentContainer)
72
- outlineProvider = clipOutlineProvider
73
- clipToOutline = true
85
+ clipContainer.addView(contentContainer)
86
+ super.addView(clipContainer)
87
+ clipContainer.outlineProvider = clipOutlineProvider
88
+ clipContainer.clipToOutline = true
89
+ clipToOutline = false
90
+ setWillNotDraw(false)
74
91
  visibility = INVISIBLE
75
92
  importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
76
93
  }
77
94
 
78
- fun setClipGeometryDip(
79
- x: Double,
80
- y: Double,
81
- width: Double,
82
- height: Double,
83
- radius: Double,
84
- ) = setClipPresentationDip(x, y, width, height, radius, 0.0, 0.0)
85
-
95
+ /** DIP fallback used until a registered view has pushed host metrics. */
86
96
  @DoNotStrip
87
97
  fun setClipPresentationDip(
88
98
  x: Double,
89
99
  y: Double,
90
100
  width: Double,
91
101
  height: Double,
92
- radius: Double,
102
+ topLeftRadius: Double,
103
+ topRightRadius: Double,
104
+ bottomRightRadius: Double,
105
+ bottomLeftRadius: Double,
106
+ curveCode: Int,
93
107
  contentTranslateX: Double,
94
108
  contentTranslateY: Double,
109
+ contentScale: Double,
110
+ shadowEnabled: Boolean = false,
111
+ shadowRed: Double = 0.0,
112
+ shadowGreen: Double = 0.0,
113
+ shadowBlue: Double = 0.0,
114
+ shadowAlpha: Double = 1.0,
115
+ shadowOffsetX: Double = 0.0,
116
+ shadowOffsetY: Double = 0.0,
117
+ shadowBlurRadius: Double = 0.0,
118
+ shadowSpreadDistance: Double = 0.0,
95
119
  ) {
96
120
  if (!x.isFinite() || !y.isFinite() || !width.isFinite() ||
97
- !height.isFinite() || !radius.isFinite() ||
98
- !contentTranslateX.isFinite() || !contentTranslateY.isFinite()
121
+ !height.isFinite() || !topLeftRadius.isFinite() ||
122
+ !topRightRadius.isFinite() || !bottomRightRadius.isFinite() ||
123
+ !bottomLeftRadius.isFinite() || !contentTranslateX.isFinite() ||
124
+ !contentTranslateY.isFinite() || !contentScale.isFinite() ||
125
+ !shadowRed.isFinite() || !shadowGreen.isFinite() ||
126
+ !shadowBlue.isFinite() || !shadowAlpha.isFinite() ||
127
+ !shadowOffsetX.isFinite() ||
128
+ !shadowOffsetY.isFinite() || !shadowBlurRadius.isFinite() ||
129
+ !shadowSpreadDistance.isFinite() || contentScale <= 0.0 ||
130
+ shadowRed !in 0.0..1.0 || shadowGreen !in 0.0..1.0 ||
131
+ shadowBlue !in 0.0..1.0 || shadowAlpha !in 0.0..1.0 ||
132
+ shadowBlurRadius < 0.0 ||
133
+ (curveCode != CLIP_CURVE_CIRCULAR && curveCode != CLIP_CURVE_CONTINUOUS)
99
134
  ) {
100
135
  return
101
136
  }
@@ -104,12 +139,26 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
104
139
  val nextY = PixelUtil.toPixelFromDIP(y).toFloat()
105
140
  val nextWidth = PixelUtil.toPixelFromDIP(width).toFloat()
106
141
  val nextHeight = PixelUtil.toPixelFromDIP(height).toFloat()
107
- val nextRadius = PixelUtil.toPixelFromDIP(radius).toFloat()
142
+ val nextTopLeftRadius = PixelUtil.toPixelFromDIP(topLeftRadius).toFloat()
143
+ val nextTopRightRadius = PixelUtil.toPixelFromDIP(topRightRadius).toFloat()
144
+ val nextBottomRightRadius = PixelUtil.toPixelFromDIP(bottomRightRadius).toFloat()
145
+ val nextBottomLeftRadius = PixelUtil.toPixelFromDIP(bottomLeftRadius).toFloat()
108
146
  val nextContentTranslateX = PixelUtil.toPixelFromDIP(contentTranslateX).toFloat()
109
147
  val nextContentTranslateY = PixelUtil.toPixelFromDIP(contentTranslateY).toFloat()
148
+ val nextContentScale = contentScale.toFloat()
149
+ val nextShadowOffsetX = PixelUtil.toPixelFromDIP(shadowOffsetX).toFloat()
150
+ val nextShadowOffsetY = PixelUtil.toPixelFromDIP(shadowOffsetY).toFloat()
151
+ val nextShadowBlurRadius = PixelUtil.toPixelFromDIP(shadowBlurRadius).toFloat()
152
+ val nextShadowSpreadDistance =
153
+ PixelUtil.toPixelFromDIP(shadowSpreadDistance).toFloat()
110
154
  if (!nextX.isFinite() || !nextY.isFinite() || !nextWidth.isFinite() ||
111
- !nextHeight.isFinite() || !nextRadius.isFinite() ||
112
- !nextContentTranslateX.isFinite() || !nextContentTranslateY.isFinite()
155
+ !nextHeight.isFinite() || !nextTopLeftRadius.isFinite() ||
156
+ !nextTopRightRadius.isFinite() || !nextBottomRightRadius.isFinite() ||
157
+ !nextBottomLeftRadius.isFinite() || !nextContentTranslateX.isFinite() ||
158
+ !nextContentTranslateY.isFinite() || !nextContentScale.isFinite() ||
159
+ !nextShadowOffsetX.isFinite() || !nextShadowOffsetY.isFinite() ||
160
+ !nextShadowBlurRadius.isFinite() ||
161
+ !nextShadowSpreadDistance.isFinite() || nextContentScale <= 0f
113
162
  ) {
114
163
  return
115
164
  }
@@ -118,178 +167,343 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
118
167
  requestedY = nextY
119
168
  requestedWidth = nextWidth
120
169
  requestedHeight = nextHeight
121
- requestedRadius = nextRadius
170
+ requestedTopLeftRadius = nextTopLeftRadius
171
+ requestedTopRightRadius = nextTopRightRadius
172
+ requestedBottomRightRadius = nextBottomRightRadius
173
+ requestedBottomLeftRadius = nextBottomLeftRadius
174
+ requestedCurveCode = curveCode
122
175
  requestedContentTranslateX = nextContentTranslateX
123
176
  requestedContentTranslateY = nextContentTranslateY
177
+ requestedContentScale = nextContentScale
178
+ storeShadow(
179
+ shadowEnabled,
180
+ shadowRed.toFloat(),
181
+ shadowGreen.toFloat(),
182
+ shadowBlue.toFloat(),
183
+ shadowAlpha.toFloat(),
184
+ nextShadowOffsetX,
185
+ nextShadowOffsetY,
186
+ nextShadowBlurRadius,
187
+ nextShadowSpreadDistance,
188
+ )
124
189
  applyRequestedGeometry()
125
190
  }
126
191
 
127
- /**
128
- * Driver hot path: called from the C++ registry with final pixel-space,
129
- * pre-normalized values (see deliverToView in SmoothClipRegistry.cpp).
130
- * No validation, scaling or normalization happens here.
131
- */
192
+ /** Driver hot path. Geometry is canonical, raw, and host-independent. */
132
193
  @DoNotStrip
133
194
  fun setClipPresentationPx(
134
195
  left: Float,
135
196
  top: Float,
136
197
  right: Float,
137
198
  bottom: Float,
138
- radius: Float,
199
+ topLeftRadius: Float,
200
+ topRightRadius: Float,
201
+ bottomRightRadius: Float,
202
+ bottomLeftRadius: Float,
203
+ curveCode: Int,
139
204
  contentTranslateX: Float,
140
205
  contentTranslateY: Float,
206
+ contentScale: Float,
207
+ shadowEnabled: Boolean = false,
208
+ shadowRed: Float = 0f,
209
+ shadowGreen: Float = 0f,
210
+ shadowBlue: Float = 0f,
211
+ shadowAlpha: Float = 1f,
212
+ shadowOffsetX: Float = 0f,
213
+ shadowOffsetY: Float = 0f,
214
+ shadowBlurRadius: Float = 0f,
215
+ shadowSpreadDistance: Float = 0f,
141
216
  ) {
142
- // Debug-only Perfetto slice for the Kotlin share of the C++
143
- // SmoothClip.setPresentation section (see SmoothClipTrace.h).
144
- if (BuildConfig.DEBUG) Trace.beginSection("SmoothClip.applyPx")
217
+ if (!left.isFinite() || !top.isFinite() || !right.isFinite() ||
218
+ !bottom.isFinite() || !topLeftRadius.isFinite() ||
219
+ !topRightRadius.isFinite() || !bottomRightRadius.isFinite() ||
220
+ !bottomLeftRadius.isFinite() || !contentTranslateX.isFinite() ||
221
+ !contentTranslateY.isFinite() || !contentScale.isFinite() ||
222
+ !shadowRed.isFinite() || !shadowGreen.isFinite() ||
223
+ !shadowBlue.isFinite() || !shadowAlpha.isFinite() ||
224
+ !shadowOffsetX.isFinite() ||
225
+ !shadowOffsetY.isFinite() || !shadowBlurRadius.isFinite() ||
226
+ !shadowSpreadDistance.isFinite() || contentScale <= 0f ||
227
+ shadowRed !in 0f..1f || shadowGreen !in 0f..1f ||
228
+ shadowBlue !in 0f..1f || shadowAlpha !in 0f..1f ||
229
+ shadowBlurRadius < 0f ||
230
+ (curveCode != CLIP_CURVE_CIRCULAR && curveCode != CLIP_CURVE_CONTINUOUS)
231
+ ) {
232
+ return
233
+ }
234
+
235
+ if (BuildConfig.DEBUG) Trace.beginSection("SmoothClip.applyPresentationPx")
145
236
  try {
146
- // Stored only; applyNormalizedClipPx computes the clip residual and
147
- // applyClipPlacement writes both translations together, because the
148
- // content's final offset depends on the residual as well.
149
237
  requestedContentTranslateX = contentTranslateX
150
238
  requestedContentTranslateY = contentTranslateY
151
- applyNormalizedClipPx(left, top, right, bottom, radius)
239
+ requestedContentScale = contentScale
240
+ storeShadow(
241
+ shadowEnabled,
242
+ shadowRed,
243
+ shadowGreen,
244
+ shadowBlue,
245
+ shadowAlpha,
246
+ shadowOffsetX,
247
+ shadowOffsetY,
248
+ shadowBlurRadius,
249
+ shadowSpreadDistance,
250
+ )
251
+ applyCanonicalClipPx(
252
+ left,
253
+ top,
254
+ right,
255
+ bottom,
256
+ topLeftRadius,
257
+ topRightRadius,
258
+ bottomRightRadius,
259
+ bottomLeftRadius,
260
+ curveCode,
261
+ )
152
262
  } finally {
153
263
  if (BuildConfig.DEBUG) Trace.endSection()
154
264
  }
155
265
  }
156
266
 
157
267
  private fun applyRequestedGeometry() {
158
- normalizeClipGeometryPx(
268
+ canonicalizeClipGeometryPx(
159
269
  requestedX,
160
270
  requestedY,
161
271
  requestedWidth,
162
272
  requestedHeight,
163
- requestedRadius,
164
- width.toFloat(),
165
- height.toFloat(),
166
- ) { left, top, right, bottom, radius ->
167
- applyNormalizedClipPx(left, top, right, bottom, radius)
273
+ requestedTopLeftRadius,
274
+ requestedTopRightRadius,
275
+ requestedBottomRightRadius,
276
+ requestedBottomLeftRadius,
277
+ requestedCurveCode,
278
+ ) { left, top, right, bottom, topLeft, topRight, bottomRight, bottomLeft, curve ->
279
+ applyCanonicalClipPx(
280
+ left,
281
+ top,
282
+ right,
283
+ bottom,
284
+ topLeft,
285
+ topRight,
286
+ bottomRight,
287
+ bottomLeft,
288
+ curve,
289
+ )
290
+ }
291
+ }
292
+
293
+ private fun storeShadow(
294
+ enabled: Boolean,
295
+ red: Float,
296
+ green: Float,
297
+ blue: Float,
298
+ alpha: Float,
299
+ offsetX: Float,
300
+ offsetY: Float,
301
+ blurRadius: Float,
302
+ spreadDistance: Float,
303
+ ) {
304
+ val wasVisible = requestedShadowEnabled && requestedShadowAlpha > 0f
305
+ val isVisible = enabled && alpha > 0f
306
+ val paintChanged = red != requestedShadowRed ||
307
+ green != requestedShadowGreen || blue != requestedShadowBlue ||
308
+ alpha != requestedShadowAlpha || blurRadius != requestedShadowBlurRadius
309
+ val pathChanged = offsetX != requestedShadowOffsetX ||
310
+ offsetY != requestedShadowOffsetY ||
311
+ spreadDistance != requestedShadowSpreadDistance
312
+ requestedShadowEnabled = enabled
313
+ requestedShadowRed = red
314
+ requestedShadowGreen = green
315
+ requestedShadowBlue = blue
316
+ requestedShadowAlpha = alpha
317
+ requestedShadowOffsetX = offsetX
318
+ requestedShadowOffsetY = offsetY
319
+ requestedShadowBlurRadius = blurRadius
320
+ requestedShadowSpreadDistance = spreadDistance
321
+ if (!isVisible) {
322
+ if (wasVisible) invalidate()
323
+ return
324
+ }
325
+ if (paintChanged || boxShadowPaint == null) updateBoxShadowPaint()
326
+ if (pathChanged || !wasVisible || boxShadowPath == null) {
327
+ rebuildBoxShadowPath()
328
+ }
329
+ if (paintChanged || pathChanged || !wasVisible) invalidate()
330
+ }
331
+
332
+ private fun updateBoxShadowPaint() {
333
+ val paint = boxShadowPaint ?: Paint(Paint.ANTI_ALIAS_FLAG).also {
334
+ boxShadowPaint = it
335
+ }
336
+ paint.color = Color.argb(
337
+ (requestedShadowAlpha * 255f).roundToInt(),
338
+ (requestedShadowRed * 255f).roundToInt(),
339
+ (requestedShadowGreen * 255f).roundToInt(),
340
+ (requestedShadowBlue * 255f).roundToInt(),
341
+ )
342
+ val sigmaPx = requestedShadowBlurRadius * 0.5f
343
+ val maskRadius = if (sigmaPx > 0.5f) {
344
+ (sigmaPx - 0.5f) / 0.57735f
345
+ } else {
346
+ 0f
347
+ }
348
+ paint.maskFilter = if (maskRadius > 0f) {
349
+ BlurMaskFilter(maskRadius, BlurMaskFilter.Blur.NORMAL)
350
+ } else {
351
+ null
168
352
  }
169
353
  }
170
354
 
171
- private fun applyNormalizedClipPx(
355
+ private fun adjustedRadiusForSpread(radius: Float, spread: Float): Float {
356
+ val magnitude = abs(spread)
357
+ val multiplier = if (magnitude > 0f && radius < magnitude) {
358
+ 1f + (radius / magnitude - 1f).pow(3)
359
+ } else {
360
+ 1f
361
+ }
362
+ return (radius + spread * multiplier).coerceAtLeast(0f)
363
+ }
364
+
365
+ private fun rebuildBoxShadowPath() {
366
+ if (!requestedShadowEnabled || requestedShadowAlpha <= 0f) return
367
+ val path = boxShadowPath ?: Path().also { boxShadowPath = it }
368
+ path.reset()
369
+ if (clipIsEmpty) return
370
+ val spread = requestedShadowSpreadDistance
371
+ val left = clipLeft - spread + requestedShadowOffsetX
372
+ val top = clipTop - spread + requestedShadowOffsetY
373
+ val right = clipRight + spread + requestedShadowOffsetX
374
+ val bottom = clipBottom + spread + requestedShadowOffsetY
375
+ if (right <= left || bottom <= top) return
376
+ appendRoundedRectPath(
377
+ path,
378
+ left,
379
+ top,
380
+ right,
381
+ bottom,
382
+ adjustedRadiusForSpread(clipTopLeftRadius, spread),
383
+ adjustedRadiusForSpread(clipTopRightRadius, spread),
384
+ adjustedRadiusForSpread(clipBottomRightRadius, spread),
385
+ adjustedRadiusForSpread(clipBottomLeftRadius, spread),
386
+ clipCurveCode,
387
+ )
388
+ }
389
+
390
+ override fun dispatchDraw(canvas: Canvas) {
391
+ val hostSaveCount = canvas.save()
392
+ canvas.clipRect(0f, 0f, width.toFloat(), height.toFloat())
393
+ try {
394
+ val shadowPath = boxShadowPath
395
+ val shadowPaint = boxShadowPaint
396
+ if (requestedShadowEnabled && !clipIsEmpty &&
397
+ requestedShadowAlpha > 0f && shadowPath != null &&
398
+ shadowPaint != null && !shadowPath.isEmpty
399
+ ) {
400
+ val apertureSaveCount = canvas.save()
401
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
402
+ canvas.clipOutPath(clipPath)
403
+ } else {
404
+ @Suppress("DEPRECATION")
405
+ canvas.clipPath(clipPath, Region.Op.DIFFERENCE)
406
+ }
407
+ canvas.drawPath(shadowPath, shadowPaint)
408
+ canvas.restoreToCount(apertureSaveCount)
409
+ }
410
+ super.dispatchDraw(canvas)
411
+ } finally {
412
+ canvas.restoreToCount(hostSaveCount)
413
+ }
414
+ }
415
+
416
+ private fun applyCanonicalClipPx(
172
417
  left: Float,
173
418
  top: Float,
174
419
  right: Float,
175
420
  bottom: Float,
176
- radius: Float,
421
+ topLeftRadius: Float,
422
+ topRightRadius: Float,
423
+ bottomRightRadius: Float,
424
+ bottomLeftRadius: Float,
425
+ curveCode: Int,
177
426
  ) {
178
- // Far edges are derived, not rounded independently: see outlineFarEdge.
179
- // Independent rounding breathes the emitted size by 1 px under pure
180
- // translation, which is the visible artifact once an animation's tail
181
- // slows below a pixel per frame.
182
- val nextOutlineLeft = outlineOrigin(left)
183
- val nextOutlineTop = outlineOrigin(top)
184
- val nextOutlineRight = outlineFarEdge(left, right)
185
- val nextOutlineBottom = outlineFarEdge(top, bottom)
186
- // Outline.setRoundRect collapses a degenerate integer rect to empty.
187
- // Use the exact emitted geometry as the single semantic source for
188
- // rendering, visibility, accessibility and hit testing.
189
- val isEmpty = outlineRectIsEmpty(
190
- nextOutlineLeft,
191
- nextOutlineTop,
192
- nextOutlineRight,
193
- nextOutlineBottom,
194
- )
195
- val outlineGeometryChanged = outlineChanged(
196
- nextOutlineLeft,
197
- nextOutlineTop,
198
- nextOutlineRight,
199
- nextOutlineBottom,
200
- radius,
201
- outlineLeft,
202
- outlineTop,
203
- outlineRight,
204
- outlineBottom,
205
- clipRadius,
206
- )
427
+ val isEmpty = right <= left || bottom <= top
428
+ val geometryChanged = left != clipLeft || top != clipTop ||
429
+ right != clipRight ||
430
+ bottom != clipBottom || topLeftRadius != clipTopLeftRadius ||
431
+ topRightRadius != clipTopRightRadius ||
432
+ bottomRightRadius != clipBottomRightRadius ||
433
+ bottomLeftRadius != clipBottomLeftRadius || curveCode != clipCurveCode
207
434
 
208
435
  clipLeft = left
209
436
  clipTop = top
210
437
  clipRight = right
211
438
  clipBottom = bottom
212
- clipRadius = radius
213
- // Whatever integer rounding threw away, carried on this view's own
214
- // translation so the clip edge still lands where the driver asked
215
- // instead of snapping to the pixel grid while the content it clips
216
- // slides in floats. It is a single scalar per axis only because the far
217
- // edges are derived from the origin — with independently rounded edges
218
- // no one translation could place both.
219
- clipResidualX = left - nextOutlineLeft
220
- clipResidualY = top - nextOutlineTop
221
- applyClipPlacement()
222
-
223
- if (isEmpty != clipIsEmpty) {
224
- if (isEmpty) cancelAcceptedTouchStream()
225
- clipIsEmpty = isEmpty
226
- visibility = clipVisibility(isEmpty)
227
- importantForAccessibility = clipAccessibility(
228
- isEmpty,
229
- requestedImportantForAccessibility,
230
- )
231
- }
232
-
233
- // An emptiness transition necessarily changes at least one emitted
234
- // edge, so the existing edge/radius key cannot skip invalidateOutline.
235
- if (!outlineGeometryChanged) return
236
-
237
- outlineLeft = nextOutlineLeft
238
- outlineTop = nextOutlineTop
239
- outlineRight = nextOutlineRight
240
- outlineBottom = nextOutlineBottom
241
- // Schedules the traversal too: invalidateOutline() ends in
242
- // invalidateViewProperty(), which damages this view in its parent up
243
- // to ViewRootImpl.scheduleTraversals(). A plain invalidate() on top
244
- // would only add PFLAG_INVALIDATED, forcing a display-list re-record
245
- // every frame to restage an outline the RenderNode applies as a
246
- // property.
247
- invalidateOutline()
248
- }
249
-
250
- /**
251
- * Single writer for both translations. The view carries the sub-pixel
252
- * remainder of the clip origin; the content container subtracts it back out
253
- * so only the clip edge moves and the content stays exactly where the
254
- * driver put it. Every write self-dedupes inside View.setTranslationX, so
255
- * calling this on an unchanged frame costs four float comparisons.
256
- */
257
- private fun applyClipPlacement() {
258
- super.setTranslationX(userTranslationX + clipResidualX)
259
- super.setTranslationY(userTranslationY + clipResidualY)
260
- contentContainer.translationX = requestedContentTranslateX - clipResidualX
261
- contentContainer.translationY = requestedContentTranslateY - clipResidualY
262
- }
439
+ val radiiAreUniform = topLeftRadius == topRightRadius &&
440
+ topLeftRadius == bottomRightRadius &&
441
+ topLeftRadius == bottomLeftRadius
442
+ clipTopLeftRadius = topLeftRadius
443
+ clipTopRightRadius = topRightRadius
444
+ clipBottomRightRadius = bottomRightRadius
445
+ clipBottomLeftRadius = bottomLeftRadius
446
+ clipCurveCode = curveCode
447
+ applyContentTransform()
448
+
449
+ clipIsEmpty = isEmpty
450
+ reapplyClipPresentation()
263
451
 
264
- // The clip's sub-pixel placement and the consumer's `transform` prop share
265
- // one property, so they are composed rather than allowed to overwrite each
266
- // other. RN routes every transform write through these setters
267
- // (BaseViewManager.setTransformProperty), so intercepting them is enough.
268
- //
269
- // The getters are deliberately NOT overridden to hide the residual:
270
- // View.setTranslationX dedupes by calling getTranslationX() virtually, so
271
- // reporting the consumer's value there would make every write look like a
272
- // change and re-invalidate the view on frames where nothing moved. The cost
273
- // of leaving them alone is that a read-modify-write of translationX picks up
274
- // the residual twice — sub-pixel, and nothing in RN's transform path reads
275
- // back before writing.
276
- override fun setTranslationX(translationX: Float) {
277
- userTranslationX = translationX
278
- super.setTranslationX(translationX + clipResidualX)
452
+ if (!geometryChanged) return
453
+
454
+ clipPath.reset()
455
+ if (!isEmpty) {
456
+ if (curveCode == CLIP_CURVE_CIRCULAR && radiiAreUniform) {
457
+ clipPath.addRoundRect(
458
+ left,
459
+ top,
460
+ right,
461
+ bottom,
462
+ topLeftRadius,
463
+ topLeftRadius,
464
+ Path.Direction.CW,
465
+ )
466
+ } else {
467
+ appendRoundedRectPath(
468
+ clipPath,
469
+ left,
470
+ top,
471
+ right,
472
+ bottom,
473
+ topLeftRadius,
474
+ topRightRadius,
475
+ bottomRightRadius,
476
+ bottomLeftRadius,
477
+ curveCode,
478
+ )
479
+ }
480
+ }
481
+ rebuildBoxShadowPath()
482
+ clipContainer.invalidateOutline()
483
+ if (requestedShadowEnabled) invalidate()
279
484
  }
280
485
 
281
- override fun setTranslationY(translationY: Float) {
282
- userTranslationY = translationY
283
- super.setTranslationY(translationY + clipResidualY)
486
+ private fun applyContentTransform() {
487
+ contentContainer.translationX = requestedContentTranslateX
488
+ contentContainer.translationY = requestedContentTranslateY
489
+ // Scale is intentionally centered and lives on the content only. View
490
+ // translation properties are applied independently of scale, so a
491
+ // caller's tx/ty remains a physical-pixel offset rather than scaling
492
+ // around the origin with the content.
493
+ contentContainer.pivotX = contentContainer.width / 2f
494
+ contentContainer.pivotY = contentContainer.height / 2f
495
+ contentContainer.scaleX = requestedContentScale
496
+ contentContainer.scaleY = requestedContentScale
284
497
  }
285
498
 
286
499
  override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
287
500
  super.onSizeChanged(w, h, oldw, oldh)
501
+ clipContainer.layout(0, 0, w, h)
288
502
  contentContainer.layout(0, 0, w, h)
503
+ applyContentTransform()
289
504
  if (boundDriverId != 0.0) {
290
- // Driver deliveries are pre-normalized in C++ against the pushed
291
- // host metrics; refresh them and let the registry redeliver the
292
- // visible value synchronously.
505
+ // Host metrics only gate lifecycle readiness. Redelivery remains
506
+ // raw and canonical; this view owns the final viewport crop.
293
507
  SmoothClipBindings.nativeSetViewHostGeometry(
294
508
  boundDriverId,
295
509
  this,
@@ -304,6 +518,7 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
304
518
  // authoritative command wins over the driver value on resize.
305
519
  applyRequestedGeometry()
306
520
  }
521
+ reapplyClipPresentation()
307
522
  }
308
523
 
309
524
  override fun onAttachedToWindow() {
@@ -358,83 +573,33 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
358
573
  }
359
574
 
360
575
  private fun containsRoundedPoint(x: Float, y: Float): Boolean {
361
- // The event arrives in this view's local space, which the parent has
362
- // already inverse-transformed by the residual translation. Adding it
363
- // back maps the point into the space clipLeft/clipTop are expressed in,
364
- // so hit testing stays against the geometry the driver delivered — the
365
- // same test as before the residual existed.
366
- return containsRoundedPointPx(
367
- x + clipResidualX,
368
- y + clipResidualY,
369
- clipLeft,
370
- clipTop,
371
- clipRight,
372
- clipBottom,
373
- clipRadius,
374
- clipIsEmpty,
375
- )
576
+ // Reuse the exact path supplied to the Outline. Hit testing therefore
577
+ // follows the rendered aperture, including continuous corners.
578
+ return !clipIsEmpty && containsPathPoint(clipPath, x, y)
376
579
  }
377
580
 
378
581
  override fun dispatchTouchEvent(event: MotionEvent): Boolean {
379
- when (event.actionMasked) {
380
- MotionEvent.ACTION_DOWN -> {
381
- clearTouchState()
382
- acceptingTouchStream = containsRoundedPoint(event.x, event.y)
383
- if (!acceptingTouchStream) return false
384
- val accepted = super.dispatchTouchEvent(event)
385
- if (accepted) {
386
- retainTouchEvent(event)
387
- } else {
388
- clearTouchState()
389
- }
390
- return accepted
391
- }
392
- MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> {
393
- val result = if (acceptingTouchStream) {
394
- super.dispatchTouchEvent(event)
395
- } else {
396
- false
397
- }
398
- clearTouchState()
399
- return result
400
- }
582
+ val action = event.actionMasked
583
+ if (action == MotionEvent.ACTION_DOWN) {
584
+ // The aperture gates admission only. Once accepted, the stream
585
+ // belongs to Android until its real UP or CANCEL arrives.
586
+ clearTouchState()
587
+ if (!containsRoundedPoint(event.x, event.y)) return false
588
+ acceptingTouchStream = true
401
589
  }
402
590
 
403
591
  if (!acceptingTouchStream) return false
404
592
  val result = super.dispatchTouchEvent(event)
405
- retainTouchEvent(event)
593
+ if ((action == MotionEvent.ACTION_DOWN && !result) ||
594
+ action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP
595
+ ) {
596
+ clearTouchState()
597
+ }
406
598
  return result
407
599
  }
408
600
 
409
- private fun retainTouchEvent(event: MotionEvent) {
410
- retainedTouchEvent?.recycle()
411
- retainedTouchEvent = MotionEvent.obtain(event)
412
- }
413
-
414
601
  private fun clearTouchState() {
415
602
  acceptingTouchStream = false
416
- retainedTouchEvent?.recycle()
417
- retainedTouchEvent = null
418
- }
419
-
420
- private fun cancelAcceptedTouchStream() {
421
- val latest = retainedTouchEvent
422
- if (!acceptingTouchStream || latest == null) {
423
- clearTouchState()
424
- return
425
- }
426
- val cancel = MotionEvent.obtain(latest)
427
- cancel.action = MotionEvent.ACTION_CANCEL
428
- // Terminal BEFORE dispatch: a child's synchronous cancel handler can
429
- // re-enter geometry application, and a nested emptiness transition
430
- // must observe an already-ended stream instead of synthesizing a
431
- // second cancel for it.
432
- clearTouchState()
433
- try {
434
- super.dispatchTouchEvent(cancel)
435
- } finally {
436
- cancel.recycle()
437
- }
438
603
  }
439
604
 
440
605
  fun setRequestedImportantForAccessibility(value: Int) {
@@ -442,14 +607,24 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
442
607
  reapplyClipPresentation()
443
608
  }
444
609
 
610
+ @DoNotStrip
611
+ fun setAutonomousMotion(active: Boolean) {
612
+ if (autonomousMotion == active) return
613
+ autonomousMotion = active
614
+ reapplyClipPresentation()
615
+ }
616
+
445
617
  fun reapplyClipPresentation() {
446
- val expectedVisibility = clipVisibility(clipIsEmpty)
618
+ val apertureVisible = apertureIntersectsHost()
619
+ val expectedVisibility = renderVisibility(
620
+ apertureVisible || shadowIntersectsHost(),
621
+ )
447
622
  if (visibility != expectedVisibility) {
448
623
  visibility = expectedVisibility
449
624
  }
450
625
 
451
626
  val expectedAccessibility = clipAccessibility(
452
- clipIsEmpty,
627
+ autonomousMotion || !apertureVisible,
453
628
  requestedImportantForAccessibility,
454
629
  )
455
630
  if (importantForAccessibility != expectedAccessibility) {
@@ -457,6 +632,29 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
457
632
  }
458
633
  }
459
634
 
635
+ private fun apertureIntersectsHost(): Boolean =
636
+ !clipIsEmpty && clipRight > 0f && clipBottom > 0f &&
637
+ clipLeft < width.toFloat() && clipTop < height.toFloat()
638
+
639
+ private fun shadowIntersectsHost(): Boolean {
640
+ if (clipIsEmpty || !requestedShadowEnabled || requestedShadowAlpha <= 0f) {
641
+ return false
642
+ }
643
+ val spread = requestedShadowSpreadDistance
644
+ val pathLeft = clipLeft - spread + requestedShadowOffsetX
645
+ val pathTop = clipTop - spread + requestedShadowOffsetY
646
+ val pathRight = clipRight + spread + requestedShadowOffsetX
647
+ val pathBottom = clipBottom + spread + requestedShadowOffsetY
648
+ if (pathRight <= pathLeft || pathBottom <= pathTop) return false
649
+
650
+ // CSS blur is specified as a diameter-like radius. Expanding by the
651
+ // full value is conservative and prevents culling a faint blur tail.
652
+ val blurOutset = requestedShadowBlurRadius
653
+ return pathRight + blurOutset > 0f && pathBottom + blurOutset > 0f &&
654
+ pathLeft - blurOutset < width.toFloat() &&
655
+ pathTop - blurOutset < height.toFloat()
656
+ }
657
+
460
658
  fun resetClipState() {
461
659
  clearTouchState()
462
660
  commandIsAuthoritative = false
@@ -464,16 +662,27 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
464
662
  requestedY = 0f
465
663
  requestedWidth = 0f
466
664
  requestedHeight = 0f
467
- requestedRadius = 0f
665
+ requestedTopLeftRadius = 0f
666
+ requestedTopRightRadius = 0f
667
+ requestedBottomRightRadius = 0f
668
+ requestedBottomLeftRadius = 0f
669
+ requestedCurveCode = CLIP_CURVE_CIRCULAR
468
670
  requestedContentTranslateX = 0f
469
671
  requestedContentTranslateY = 0f
470
- clipResidualX = 0f
471
- clipResidualY = 0f
472
- applyClipPlacement()
473
- outlineLeft = 0
474
- outlineTop = 0
475
- outlineRight = 0
476
- outlineBottom = 0
672
+ requestedContentScale = 1f
673
+ requestedShadowEnabled = false
674
+ requestedShadowRed = 0f
675
+ requestedShadowGreen = 0f
676
+ requestedShadowBlue = 0f
677
+ requestedShadowAlpha = 1f
678
+ requestedShadowOffsetX = 0f
679
+ requestedShadowOffsetY = 0f
680
+ requestedShadowBlurRadius = 0f
681
+ requestedShadowSpreadDistance = 0f
682
+ boxShadowPaint = null
683
+ boxShadowPath = null
684
+ applyContentTransform()
685
+ clipPath.reset()
477
686
  requestedImportantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_AUTO
478
687
  applyRequestedGeometry()
479
688
  }