react-native-smooth-clip-view 0.3.0 → 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 (199) hide show
  1. package/README.md +132 -388
  2. package/android/build.gradle +3 -3
  3. package/android/proguard-rules.pro +2 -2
  4. package/android/src/main/cpp/SmoothClipAndroid.h +9 -12
  5. package/android/src/main/cpp/SmoothClipBindings.cpp +300 -704
  6. package/android/src/main/cpp/SmoothClipRegistry.cpp +348 -594
  7. package/android/src/main/java/com/smoothclipview/ClipGeometryNormalizer.kt +36 -227
  8. package/android/src/main/java/com/smoothclipview/SmoothClipBindings.kt +16 -19
  9. package/android/src/main/java/com/smoothclipview/SmoothClipModule.kt +36 -294
  10. package/android/src/main/java/com/smoothclipview/SmoothClipView.kt +344 -488
  11. package/android/src/main/java/com/smoothclipview/SmoothClipViewManager.kt +75 -77
  12. package/cpp/SmoothClipAnimationCurve.h +237 -346
  13. package/cpp/SmoothClipRegistry.h +24 -52
  14. package/cpp/SmoothClipSharedGeometry.h +64 -94
  15. package/cpp/SmoothClipVelocityTracker.h +5 -5
  16. package/ios/SmoothClipGeometry.h +23 -34
  17. package/ios/SmoothClipRegistry.mm +206 -711
  18. package/ios/SmoothClipTurboModule.cpp +217 -715
  19. package/ios/SmoothClipTurboModule.h +54 -297
  20. package/ios/SmoothClipView.mm +599 -505
  21. package/ios/SmoothClipViewRegistry.h +2 -7
  22. package/lib/module/NativeSmoothClipModule.js.map +1 -1
  23. package/lib/module/SmoothClipView.ios.js +84 -17
  24. package/lib/module/SmoothClipView.ios.js.map +1 -1
  25. package/lib/module/SmoothClipView.js +1 -1
  26. package/lib/module/SmoothClipView.js.map +1 -1
  27. package/lib/module/SmoothClipViewNativeComponent.ts +20 -26
  28. package/lib/module/capabilities.native.js +1 -19
  29. package/lib/module/capabilities.native.js.map +1 -1
  30. package/lib/module/capabilityTypes.js +0 -9
  31. package/lib/module/capabilityTypes.js.map +1 -1
  32. package/lib/module/controllerInternals.js +28 -0
  33. package/lib/module/controllerInternals.js.map +1 -0
  34. package/lib/module/controllerLifecycle.js +9 -0
  35. package/lib/module/controllerLifecycle.js.map +1 -0
  36. package/lib/module/controllerTypes.js +4 -0
  37. package/lib/module/controllerTypes.js.map +1 -0
  38. package/lib/module/controllers.android.js +4 -0
  39. package/lib/module/controllers.android.js.map +1 -0
  40. package/lib/module/controllers.ios.js +4 -0
  41. package/lib/module/controllers.ios.js.map +1 -0
  42. package/lib/module/controllers.js +4 -0
  43. package/lib/module/controllers.js.map +1 -0
  44. package/lib/module/controllers.native.js +89 -0
  45. package/lib/module/controllers.native.js.map +1 -0
  46. package/lib/module/geometry.js +71 -51
  47. package/lib/module/geometry.js.map +1 -1
  48. package/lib/module/groupTypes.js +4 -0
  49. package/lib/module/{groupDriverTypes.js.map → groupTypes.js.map} +1 -1
  50. package/lib/module/groups.android.js +4 -0
  51. package/lib/module/groups.android.js.map +1 -0
  52. package/lib/module/groups.ios.js +4 -0
  53. package/lib/module/groups.ios.js.map +1 -0
  54. package/lib/module/groups.js +4 -0
  55. package/lib/module/groups.js.map +1 -0
  56. package/lib/module/groups.native.js +399 -0
  57. package/lib/module/groups.native.js.map +1 -0
  58. package/lib/module/ids.js +8 -0
  59. package/lib/module/ids.js.map +1 -0
  60. package/lib/module/index.js +3 -3
  61. package/lib/module/index.js.map +1 -1
  62. package/lib/module/nativeCompletion.js +37 -16
  63. package/lib/module/nativeCompletion.js.map +1 -1
  64. package/lib/module/presentationCodec.js +81 -0
  65. package/lib/module/presentationCodec.js.map +1 -0
  66. package/lib/typescript/src/NativeSmoothClipModule.d.ts +14 -23
  67. package/lib/typescript/src/NativeSmoothClipModule.d.ts.map +1 -1
  68. package/lib/typescript/src/SmoothClipView.d.ts +1 -1
  69. package/lib/typescript/src/SmoothClipView.d.ts.map +1 -1
  70. package/lib/typescript/src/SmoothClipView.ios.d.ts +18 -14
  71. package/lib/typescript/src/SmoothClipView.ios.d.ts.map +1 -1
  72. package/lib/typescript/src/SmoothClipViewNativeComponent.d.ts +10 -5
  73. package/lib/typescript/src/SmoothClipViewNativeComponent.d.ts.map +1 -1
  74. package/lib/typescript/src/capabilities.native.d.ts +1 -1
  75. package/lib/typescript/src/capabilities.native.d.ts.map +1 -1
  76. package/lib/typescript/src/capabilityTypes.d.ts +0 -6
  77. package/lib/typescript/src/capabilityTypes.d.ts.map +1 -1
  78. package/lib/typescript/src/controllerInternals.d.ts +14 -0
  79. package/lib/typescript/src/controllerInternals.d.ts.map +1 -0
  80. package/lib/typescript/src/controllerLifecycle.d.ts +3 -0
  81. package/lib/typescript/src/controllerLifecycle.d.ts.map +1 -0
  82. package/lib/typescript/src/controllerTypes.d.ts +62 -0
  83. package/lib/typescript/src/controllerTypes.d.ts.map +1 -0
  84. package/lib/typescript/src/controllers.android.d.ts +2 -0
  85. package/lib/typescript/src/controllers.android.d.ts.map +1 -0
  86. package/lib/typescript/src/controllers.d.ts +2 -0
  87. package/lib/typescript/src/controllers.d.ts.map +1 -0
  88. package/lib/typescript/src/controllers.ios.d.ts +2 -0
  89. package/lib/typescript/src/controllers.ios.d.ts.map +1 -0
  90. package/lib/typescript/src/controllers.native.d.ts +3 -0
  91. package/lib/typescript/src/controllers.native.d.ts.map +1 -0
  92. package/lib/typescript/src/geometry.d.ts +22 -12
  93. package/lib/typescript/src/geometry.d.ts.map +1 -1
  94. package/lib/typescript/src/groupTypes.d.ts +31 -0
  95. package/lib/typescript/src/groupTypes.d.ts.map +1 -0
  96. package/lib/typescript/src/groups.android.d.ts +2 -0
  97. package/lib/typescript/src/groups.android.d.ts.map +1 -0
  98. package/lib/typescript/src/groups.d.ts +2 -0
  99. package/lib/typescript/src/groups.d.ts.map +1 -0
  100. package/lib/typescript/src/groups.ios.d.ts +2 -0
  101. package/lib/typescript/src/groups.ios.d.ts.map +1 -0
  102. package/lib/typescript/src/groups.native.d.ts +3 -0
  103. package/lib/typescript/src/groups.native.d.ts.map +1 -0
  104. package/lib/typescript/src/ids.d.ts +2 -0
  105. package/lib/typescript/src/ids.d.ts.map +1 -0
  106. package/lib/typescript/src/index.d.ts +6 -6
  107. package/lib/typescript/src/index.d.ts.map +1 -1
  108. package/lib/typescript/src/nativeCompletion.d.ts +16 -6
  109. package/lib/typescript/src/nativeCompletion.d.ts.map +1 -1
  110. package/lib/typescript/src/presentationCodec.d.ts +10 -0
  111. package/lib/typescript/src/presentationCodec.d.ts.map +1 -0
  112. package/package.json +8 -3
  113. package/src/NativeSmoothClipModule.ts +41 -292
  114. package/src/SmoothClipView.ios.tsx +130 -23
  115. package/src/SmoothClipView.tsx +5 -1
  116. package/src/SmoothClipViewNativeComponent.ts +20 -26
  117. package/src/capabilities.native.ts +2 -52
  118. package/src/capabilityTypes.ts +0 -14
  119. package/src/controllerInternals.ts +52 -0
  120. package/src/controllerLifecycle.ts +6 -0
  121. package/src/controllerTypes.ts +87 -0
  122. package/src/controllers.android.ts +1 -0
  123. package/src/controllers.ios.ts +1 -0
  124. package/src/controllers.native.ts +104 -0
  125. package/src/controllers.ts +1 -0
  126. package/src/geometry.ts +118 -74
  127. package/src/groupTypes.ts +53 -0
  128. package/src/groups.android.ts +1 -0
  129. package/src/groups.ios.ts +1 -0
  130. package/src/groups.native.ts +604 -0
  131. package/src/groups.ts +1 -0
  132. package/src/ids.ts +6 -0
  133. package/src/index.ts +20 -28
  134. package/src/nativeCompletion.ts +67 -32
  135. package/src/presentationCodec.ts +134 -0
  136. package/lib/module/driverState.js +0 -82
  137. package/lib/module/driverState.js.map +0 -1
  138. package/lib/module/driverTypes.js +0 -4
  139. package/lib/module/driverTypes.js.map +0 -1
  140. package/lib/module/drivers.android.js +0 -6
  141. package/lib/module/drivers.android.js.map +0 -1
  142. package/lib/module/drivers.ios.js +0 -6
  143. package/lib/module/drivers.ios.js.map +0 -1
  144. package/lib/module/drivers.js +0 -4
  145. package/lib/module/drivers.js.map +0 -1
  146. package/lib/module/drivers.native.js +0 -756
  147. package/lib/module/drivers.native.js.map +0 -1
  148. package/lib/module/groupDriverTypes.js +0 -4
  149. package/lib/module/groupDrivers.android.js +0 -4
  150. package/lib/module/groupDrivers.android.js.map +0 -1
  151. package/lib/module/groupDrivers.ios.js +0 -4
  152. package/lib/module/groupDrivers.ios.js.map +0 -1
  153. package/lib/module/groupDrivers.js +0 -4
  154. package/lib/module/groupDrivers.js.map +0 -1
  155. package/lib/module/groupDrivers.native.js +0 -799
  156. package/lib/module/groupDrivers.native.js.map +0 -1
  157. package/lib/module/presentationProtocol.js +0 -15
  158. package/lib/module/presentationProtocol.js.map +0 -1
  159. package/lib/module/reactRequests.js +0 -56
  160. package/lib/module/reactRequests.js.map +0 -1
  161. package/lib/typescript/src/driverState.d.ts +0 -26
  162. package/lib/typescript/src/driverState.d.ts.map +0 -1
  163. package/lib/typescript/src/driverTypes.d.ts +0 -121
  164. package/lib/typescript/src/driverTypes.d.ts.map +0 -1
  165. package/lib/typescript/src/drivers.android.d.ts +0 -3
  166. package/lib/typescript/src/drivers.android.d.ts.map +0 -1
  167. package/lib/typescript/src/drivers.d.ts +0 -3
  168. package/lib/typescript/src/drivers.d.ts.map +0 -1
  169. package/lib/typescript/src/drivers.ios.d.ts +0 -3
  170. package/lib/typescript/src/drivers.ios.d.ts.map +0 -1
  171. package/lib/typescript/src/drivers.native.d.ts +0 -5
  172. package/lib/typescript/src/drivers.native.d.ts.map +0 -1
  173. package/lib/typescript/src/groupDriverTypes.d.ts +0 -66
  174. package/lib/typescript/src/groupDriverTypes.d.ts.map +0 -1
  175. package/lib/typescript/src/groupDrivers.android.d.ts +0 -2
  176. package/lib/typescript/src/groupDrivers.android.d.ts.map +0 -1
  177. package/lib/typescript/src/groupDrivers.d.ts +0 -2
  178. package/lib/typescript/src/groupDrivers.d.ts.map +0 -1
  179. package/lib/typescript/src/groupDrivers.ios.d.ts +0 -2
  180. package/lib/typescript/src/groupDrivers.ios.d.ts.map +0 -1
  181. package/lib/typescript/src/groupDrivers.native.d.ts +0 -4
  182. package/lib/typescript/src/groupDrivers.native.d.ts.map +0 -1
  183. package/lib/typescript/src/presentationProtocol.d.ts +0 -4
  184. package/lib/typescript/src/presentationProtocol.d.ts.map +0 -1
  185. package/lib/typescript/src/reactRequests.d.ts +0 -9
  186. package/lib/typescript/src/reactRequests.d.ts.map +0 -1
  187. package/src/driverState.ts +0 -118
  188. package/src/driverTypes.ts +0 -171
  189. package/src/drivers.android.ts +0 -8
  190. package/src/drivers.ios.ts +0 -8
  191. package/src/drivers.native.ts +0 -1599
  192. package/src/drivers.ts +0 -6
  193. package/src/groupDriverTypes.ts +0 -117
  194. package/src/groupDrivers.android.ts +0 -1
  195. package/src/groupDrivers.ios.ts +0 -1
  196. package/src/groupDrivers.native.ts +0 -1189
  197. package/src/groupDrivers.ts +0 -1
  198. package/src/presentationProtocol.ts +0 -28
  199. 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,17 @@ 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 clipContainer = ReactViewGroup(context)
16
26
  internal val contentContainer = ReactViewGroup(context)
17
27
  private var requestedX = 0f
18
28
  private var requestedY = 0f
19
29
  private var requestedWidth = 0f
20
30
  private var requestedHeight = 0f
21
- private var requestedRadius = 0f
22
31
  private var requestedTopLeftRadius = 0f
23
32
  private var requestedTopRightRadius = 0f
24
33
  private var requestedBottomRightRadius = 0f
@@ -27,43 +36,36 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
27
36
  private var requestedContentTranslateX = 0f
28
37
  private var requestedContentTranslateY = 0f
29
38
  private var requestedContentScale = 1f
30
- private var requestedUsesV2Geometry = false
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
31
48
  private var clipLeft = 0f
32
49
  private var clipTop = 0f
33
50
  private var clipRight = 0f
34
51
  private var clipBottom = 0f
35
- private var clipRadius = 0f
36
52
  private var clipTopLeftRadius = 0f
37
53
  private var clipTopRightRadius = 0f
38
54
  private var clipBottomRightRadius = 0f
39
55
  private var clipBottomLeftRadius = 0f
40
56
  private var clipCurveCode = CLIP_CURVE_CIRCULAR
41
57
  private val clipPath = Path()
42
- private var outlineUsesPath = false
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
58
+ private var boxShadowPath: Path? = null
59
+ private var boxShadowPaint: Paint? = null
50
60
  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
61
  private var requestedImportantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_AUTO
62
+ private var autonomousMotion = false
60
63
  private var acceptingTouchStream = false
61
- private var retainedTouchEvent: MotionEvent? = null
62
64
 
63
65
  /** Driver this view is registered with in the native registry (0 = none). */
64
66
  internal var boundDriverId: Double = 0.0
65
67
 
66
- /** Set once a legacy command arrives; later initialClip* props are ignored. */
68
+ /** Set once a command arrives; later initial presentation props are ignored. */
67
69
  internal var commandIsAuthoritative = false
68
70
 
69
71
  private val clipOutlineProvider = object : ViewOutlineProvider() {
@@ -73,93 +75,26 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
73
75
  return
74
76
  }
75
77
 
76
- if (outlineUsesPath) {
77
- outline.setPath(clipPath)
78
- } else if (outlineUsesFloatRoundRect) {
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.
83
- outline.setPath(clipPath)
84
- } else {
85
- outline.setRoundRect(
86
- outlineLeft,
87
- outlineTop,
88
- outlineRight,
89
- outlineBottom,
90
- clipRadius,
91
- )
92
- }
78
+ // Outline.setRoundRect only accepts integer edges. The raw path
79
+ // keeps sub-pixel coordinates and off-host geometry intact.
80
+ outline.setPath(clipPath)
93
81
  }
94
82
  }
95
83
 
96
84
  init {
97
- super.addView(contentContainer)
98
- outlineProvider = clipOutlineProvider
99
- clipToOutline = true
85
+ clipContainer.addView(contentContainer)
86
+ super.addView(clipContainer)
87
+ clipContainer.outlineProvider = clipOutlineProvider
88
+ clipContainer.clipToOutline = true
89
+ clipToOutline = false
90
+ setWillNotDraw(false)
100
91
  visibility = INVISIBLE
101
92
  importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
102
93
  }
103
94
 
104
- fun setClipGeometryDip(
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
-
95
+ /** DIP fallback used until a registered view has pushed host metrics. */
112
96
  @DoNotStrip
113
97
  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
98
  x: Double,
164
99
  y: Double,
165
100
  width: Double,
@@ -172,13 +107,29 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
172
107
  contentTranslateX: Double,
173
108
  contentTranslateY: Double,
174
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,
175
119
  ) {
176
120
  if (!x.isFinite() || !y.isFinite() || !width.isFinite() ||
177
121
  !height.isFinite() || !topLeftRadius.isFinite() ||
178
122
  !topRightRadius.isFinite() || !bottomRightRadius.isFinite() ||
179
123
  !bottomLeftRadius.isFinite() || !contentTranslateX.isFinite() ||
180
124
  !contentTranslateY.isFinite() || !contentScale.isFinite() ||
181
- contentScale <= 0.0 ||
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 ||
182
133
  (curveCode != CLIP_CURVE_CIRCULAR && curveCode != CLIP_CURVE_CONTINUOUS)
183
134
  ) {
184
135
  return
@@ -195,12 +146,19 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
195
146
  val nextContentTranslateX = PixelUtil.toPixelFromDIP(contentTranslateX).toFloat()
196
147
  val nextContentTranslateY = PixelUtil.toPixelFromDIP(contentTranslateY).toFloat()
197
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()
198
154
  if (!nextX.isFinite() || !nextY.isFinite() || !nextWidth.isFinite() ||
199
155
  !nextHeight.isFinite() || !nextTopLeftRadius.isFinite() ||
200
156
  !nextTopRightRadius.isFinite() || !nextBottomRightRadius.isFinite() ||
201
157
  !nextBottomLeftRadius.isFinite() || !nextContentTranslateX.isFinite() ||
202
158
  !nextContentTranslateY.isFinite() || !nextContentScale.isFinite() ||
203
- nextContentScale <= 0f
159
+ !nextShadowOffsetX.isFinite() || !nextShadowOffsetY.isFinite() ||
160
+ !nextShadowBlurRadius.isFinite() ||
161
+ !nextShadowSpreadDistance.isFinite() || nextContentScale <= 0f
204
162
  ) {
205
163
  return
206
164
  }
@@ -209,7 +167,6 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
209
167
  requestedY = nextY
210
168
  requestedWidth = nextWidth
211
169
  requestedHeight = nextHeight
212
- requestedRadius = 0f
213
170
  requestedTopLeftRadius = nextTopLeftRadius
214
171
  requestedTopRightRadius = nextTopRightRadius
215
172
  requestedBottomRightRadius = nextBottomRightRadius
@@ -218,47 +175,23 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
218
175
  requestedContentTranslateX = nextContentTranslateX
219
176
  requestedContentTranslateY = nextContentTranslateY
220
177
  requestedContentScale = nextContentScale
221
- requestedUsesV2Geometry = true
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
+ )
222
189
  applyRequestedGeometry()
223
190
  }
224
191
 
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
- */
192
+ /** Driver hot path. Geometry is canonical, raw, and host-independent. */
230
193
  @DoNotStrip
231
194
  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
195
  left: Float,
263
196
  top: Float,
264
197
  right: Float,
@@ -271,24 +204,51 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
271
204
  contentTranslateX: Float,
272
205
  contentTranslateY: Float,
273
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,
274
216
  ) {
275
217
  if (!left.isFinite() || !top.isFinite() || !right.isFinite() ||
276
218
  !bottom.isFinite() || !topLeftRadius.isFinite() ||
277
219
  !topRightRadius.isFinite() || !bottomRightRadius.isFinite() ||
278
220
  !bottomLeftRadius.isFinite() || !contentTranslateX.isFinite() ||
279
221
  !contentTranslateY.isFinite() || !contentScale.isFinite() ||
280
- contentScale <= 0f ||
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 ||
281
230
  (curveCode != CLIP_CURVE_CIRCULAR && curveCode != CLIP_CURVE_CONTINUOUS)
282
231
  ) {
283
232
  return
284
233
  }
285
234
 
286
- if (BuildConfig.DEBUG) Trace.beginSection("SmoothClip.applyV2Px")
235
+ if (BuildConfig.DEBUG) Trace.beginSection("SmoothClip.applyPresentationPx")
287
236
  try {
288
237
  requestedContentTranslateX = contentTranslateX
289
238
  requestedContentTranslateY = contentTranslateY
290
239
  requestedContentScale = contentScale
291
- applyNormalizedClipV2Px(
240
+ storeShadow(
241
+ shadowEnabled,
242
+ shadowRed,
243
+ shadowGreen,
244
+ shadowBlue,
245
+ shadowAlpha,
246
+ shadowOffsetX,
247
+ shadowOffsetY,
248
+ shadowBlurRadius,
249
+ shadowSpreadDistance,
250
+ )
251
+ applyCanonicalClipPx(
292
252
  left,
293
253
  top,
294
254
  right,
@@ -305,135 +265,155 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
305
265
  }
306
266
 
307
267
  private fun applyRequestedGeometry() {
308
- if (requestedUsesV2Geometry) {
309
- normalizeClipGeometryV2Px(
310
- requestedX,
311
- requestedY,
312
- requestedWidth,
313
- requestedHeight,
314
- requestedTopLeftRadius,
315
- requestedTopRightRadius,
316
- requestedBottomRightRadius,
317
- requestedBottomLeftRadius,
318
- requestedCurveCode,
319
- width.toFloat(),
320
- height.toFloat(),
321
- ) { left, top, right, bottom, topLeft, topRight, bottomRight, bottomLeft, curve ->
322
- applyNormalizedClipV2Px(
323
- left,
324
- top,
325
- right,
326
- bottom,
327
- topLeft,
328
- topRight,
329
- bottomRight,
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
- }
268
+ canonicalizeClipGeometryPx(
269
+ requestedX,
270
+ requestedY,
271
+ requestedWidth,
272
+ requestedHeight,
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
+ )
346
290
  }
347
291
  }
348
292
 
349
- private fun applyNormalizedClipPx(
350
- left: Float,
351
- top: Float,
352
- right: Float,
353
- bottom: Float,
354
- radius: Float,
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,
355
303
  ) {
356
- // Far edges are derived, not rounded independently: see outlineFarEdge.
357
- // Independent rounding breathes the emitted size by 1 px under pure
358
- // translation, which is the visible artifact once an animation's tail
359
- // slows below a pixel per frame.
360
- val nextOutlineLeft = outlineOrigin(left)
361
- val nextOutlineTop = outlineOrigin(top)
362
- val nextOutlineRight = outlineFarEdge(left, right)
363
- val nextOutlineBottom = outlineFarEdge(top, bottom)
364
- // Outline.setRoundRect collapses a degenerate integer rect to empty.
365
- // Use the exact emitted geometry as the single semantic source for
366
- // rendering, visibility, accessibility and hit testing.
367
- val isEmpty = outlineRectIsEmpty(
368
- nextOutlineLeft,
369
- nextOutlineTop,
370
- nextOutlineRight,
371
- nextOutlineBottom,
372
- )
373
- val outlineGeometryChanged = outlineUsesPath || outlineUsesFloatRoundRect || outlineChanged(
374
- nextOutlineLeft,
375
- nextOutlineTop,
376
- nextOutlineRight,
377
- nextOutlineBottom,
378
- radius,
379
- outlineLeft,
380
- outlineTop,
381
- outlineRight,
382
- outlineBottom,
383
- clipRadius,
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(),
384
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
352
+ }
353
+ }
385
354
 
386
- clipLeft = left
387
- clipTop = top
388
- clipRight = right
389
- clipBottom = bottom
390
- clipRadius = radius
391
- clipTopLeftRadius = radius
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
- )
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
414
361
  }
362
+ return (radius + spread * multiplier).coerceAtLeast(0f)
363
+ }
415
364
 
416
- // An emptiness transition necessarily changes at least one emitted
417
- // edge, so the existing edge/radius key cannot skip invalidateOutline.
418
- if (!outlineGeometryChanged) return
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
+ }
419
389
 
420
- outlineUsesPath = false
421
- outlineUsesFloatRoundRect = false
422
- clipPath.reset()
423
- outlineLeft = nextOutlineLeft
424
- outlineTop = nextOutlineTop
425
- outlineRight = nextOutlineRight
426
- outlineBottom = nextOutlineBottom
427
- // Schedules the traversal too: invalidateOutline() ends in
428
- // invalidateViewProperty(), which damages this view in its parent up
429
- // to ViewRootImpl.scheduleTraversals(). A plain invalidate() on top
430
- // would only add PFLAG_INVALIDATED, forcing a display-list re-record
431
- // every frame to restage an outline the RenderNode applies as a
432
- // property.
433
- invalidateOutline()
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
+ }
434
414
  }
435
415
 
436
- private fun applyNormalizedClipV2Px(
416
+ private fun applyCanonicalClipPx(
437
417
  left: Float,
438
418
  top: Float,
439
419
  right: Float,
@@ -444,24 +424,9 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
444
424
  bottomLeftRadius: Float,
445
425
  curveCode: Int,
446
426
  ) {
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
427
  val isEmpty = right <= left || bottom <= top
463
- val outlineGeometryChanged = !outlineUsesPath ||
464
- left != clipLeft || top != clipTop || right != clipRight ||
428
+ val geometryChanged = left != clipLeft || top != clipTop ||
429
+ right != clipRight ||
465
430
  bottom != clipBottom || topLeftRadius != clipTopLeftRadius ||
466
431
  topRightRadius != clipTopRightRadius ||
467
432
  bottomRightRadius != clipBottomRightRadius ||
@@ -471,122 +436,56 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
471
436
  clipTop = top
472
437
  clipRight = right
473
438
  clipBottom = bottom
474
- clipRadius = if (radiiAreUniform) topLeftRadius else 0f
439
+ val radiiAreUniform = topLeftRadius == topRightRadius &&
440
+ topLeftRadius == bottomRightRadius &&
441
+ topLeftRadius == bottomLeftRadius
475
442
  clipTopLeftRadius = topLeftRadius
476
443
  clipTopRightRadius = topRightRadius
477
444
  clipBottomRightRadius = bottomRightRadius
478
445
  clipBottomLeftRadius = bottomLeftRadius
479
446
  clipCurveCode = curveCode
480
- // Path outlines retain physical-pixel floats, so unlike the legacy
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
- }
447
+ applyContentTransform()
517
448
 
518
- /**
519
- * Protocol V2 uniform circular geometry stays entirely in float physical
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
449
+ clipIsEmpty = isEmpty
450
+ reapplyClipPresentation()
535
451
 
536
- clipLeft = left
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
- }
452
+ if (!geometryChanged) return
559
453
 
560
- if (!outlineGeometryChanged) return
561
- outlineUsesPath = false
562
- outlineUsesFloatRoundRect = true
563
454
  clipPath.reset()
564
455
  if (!isEmpty) {
565
- clipPath.addRoundRect(
566
- left,
567
- top,
568
- right,
569
- bottom,
570
- radius,
571
- radius,
572
- Path.Direction.CW,
573
- )
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
+ }
574
480
  }
575
- invalidateOutline()
481
+ rebuildBoxShadowPath()
482
+ clipContainer.invalidateOutline()
483
+ if (requestedShadowEnabled) invalidate()
576
484
  }
577
485
 
578
- /**
579
- * Single writer for both translations. The view carries the sub-pixel
580
- * remainder of the clip origin; the content container subtracts it back out
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
486
+ private fun applyContentTransform() {
487
+ contentContainer.translationX = requestedContentTranslateX
488
+ contentContainer.translationY = requestedContentTranslateY
590
489
  // Scale is intentionally centered and lives on the content only. View
591
490
  // translation properties are applied independently of scale, so a
592
491
  // caller's tx/ty remains a physical-pixel offset rather than scaling
@@ -597,36 +496,14 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
597
496
  contentContainer.scaleY = requestedContentScale
598
497
  }
599
498
 
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
499
  override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
623
500
  super.onSizeChanged(w, h, oldw, oldh)
501
+ clipContainer.layout(0, 0, w, h)
624
502
  contentContainer.layout(0, 0, w, h)
625
- applyClipPlacement()
503
+ applyContentTransform()
626
504
  if (boundDriverId != 0.0) {
627
- // Driver deliveries are pre-normalized in C++ against the pushed
628
- // host metrics; refresh them and let the registry redeliver the
629
- // visible value synchronously.
505
+ // Host metrics only gate lifecycle readiness. Redelivery remains
506
+ // raw and canonical; this view owns the final viewport crop.
630
507
  SmoothClipBindings.nativeSetViewHostGeometry(
631
508
  boundDriverId,
632
509
  this,
@@ -641,6 +518,7 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
641
518
  // authoritative command wins over the driver value on resize.
642
519
  applyRequestedGeometry()
643
520
  }
521
+ reapplyClipPresentation()
644
522
  }
645
523
 
646
524
  override fun onAttachedToWindow() {
@@ -695,89 +573,33 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
695
573
  }
696
574
 
697
575
  private fun containsRoundedPoint(x: Float, y: Float): Boolean {
698
- // The event arrives in this view's local space, which the parent has
699
- // already inverse-transformed by the residual translation. Adding it
700
- // back maps the point into the space clipLeft/clipTop are expressed in,
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
- )
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)
719
579
  }
720
580
 
721
581
  override fun dispatchTouchEvent(event: MotionEvent): Boolean {
722
- when (event.actionMasked) {
723
- MotionEvent.ACTION_DOWN -> {
724
- clearTouchState()
725
- acceptingTouchStream = containsRoundedPoint(event.x, event.y)
726
- if (!acceptingTouchStream) return false
727
- val accepted = super.dispatchTouchEvent(event)
728
- if (accepted) {
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
- }
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
744
589
  }
745
590
 
746
591
  if (!acceptingTouchStream) return false
747
592
  val result = super.dispatchTouchEvent(event)
748
- retainTouchEvent(event)
593
+ if ((action == MotionEvent.ACTION_DOWN && !result) ||
594
+ action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP
595
+ ) {
596
+ clearTouchState()
597
+ }
749
598
  return result
750
599
  }
751
600
 
752
- private fun retainTouchEvent(event: MotionEvent) {
753
- retainedTouchEvent?.recycle()
754
- retainedTouchEvent = MotionEvent.obtain(event)
755
- }
756
-
757
601
  private fun clearTouchState() {
758
602
  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
603
  }
782
604
 
783
605
  fun setRequestedImportantForAccessibility(value: Int) {
@@ -785,14 +607,24 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
785
607
  reapplyClipPresentation()
786
608
  }
787
609
 
610
+ @DoNotStrip
611
+ fun setAutonomousMotion(active: Boolean) {
612
+ if (autonomousMotion == active) return
613
+ autonomousMotion = active
614
+ reapplyClipPresentation()
615
+ }
616
+
788
617
  fun reapplyClipPresentation() {
789
- val expectedVisibility = clipVisibility(clipIsEmpty)
618
+ val apertureVisible = apertureIntersectsHost()
619
+ val expectedVisibility = renderVisibility(
620
+ apertureVisible || shadowIntersectsHost(),
621
+ )
790
622
  if (visibility != expectedVisibility) {
791
623
  visibility = expectedVisibility
792
624
  }
793
625
 
794
626
  val expectedAccessibility = clipAccessibility(
795
- clipIsEmpty,
627
+ autonomousMotion || !apertureVisible,
796
628
  requestedImportantForAccessibility,
797
629
  )
798
630
  if (importantForAccessibility != expectedAccessibility) {
@@ -800,6 +632,29 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
800
632
  }
801
633
  }
802
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
+
803
658
  fun resetClipState() {
804
659
  clearTouchState()
805
660
  commandIsAuthoritative = false
@@ -807,7 +662,6 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
807
662
  requestedY = 0f
808
663
  requestedWidth = 0f
809
664
  requestedHeight = 0f
810
- requestedRadius = 0f
811
665
  requestedTopLeftRadius = 0f
812
666
  requestedTopRightRadius = 0f
813
667
  requestedBottomRightRadius = 0f
@@ -816,17 +670,19 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
816
670
  requestedContentTranslateX = 0f
817
671
  requestedContentTranslateY = 0f
818
672
  requestedContentScale = 1f
819
- requestedUsesV2Geometry = false
820
- clipResidualX = 0f
821
- clipResidualY = 0f
822
- applyClipPlacement()
823
- outlineUsesPath = false
824
- outlineUsesFloatRoundRect = false
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()
825
685
  clipPath.reset()
826
- outlineLeft = 0
827
- outlineTop = 0
828
- outlineRight = 0
829
- outlineBottom = 0
830
686
  requestedImportantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_AUTO
831
687
  applyRequestedGeometry()
832
688
  }