react-native-smooth-clip-view 0.2.5 → 0.2.7

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 (41) hide show
  1. package/README.md +68 -26
  2. package/android/CMakeLists.txt +0 -1
  3. package/android/build.gradle +15 -0
  4. package/android/src/main/cpp/SmoothClipAndroid.h +12 -8
  5. package/android/src/main/cpp/SmoothClipBindings.cpp +39 -15
  6. package/android/src/main/cpp/SmoothClipRegistry.cpp +288 -285
  7. package/android/src/main/java/com/smoothclipview/ClipGeometryNormalizer.kt +61 -11
  8. package/android/src/main/java/com/smoothclipview/SmoothClipBindings.kt +28 -4
  9. package/android/src/main/java/com/smoothclipview/SmoothClipModule.kt +1 -0
  10. package/android/src/main/java/com/smoothclipview/SmoothClipView.kt +180 -42
  11. package/android/src/main/java/com/smoothclipview/SmoothClipViewManager.kt +1 -0
  12. package/cpp/SmoothClipAnimationCurve.h +560 -0
  13. package/cpp/SmoothClipAnimationId.h +32 -0
  14. package/cpp/SmoothClipRegistry.h +11 -1
  15. package/cpp/SmoothClipVelocityTracker.h +30 -10
  16. package/ios/SmoothClipRegistry.mm +398 -150
  17. package/ios/SmoothClipTurboModule.cpp +4 -2
  18. package/ios/SmoothClipTurboModule.h +2 -1
  19. package/ios/SmoothClipView.mm +85 -140
  20. package/ios/SmoothClipViewRegistry.h +4 -1
  21. package/lib/module/NativeSmoothClipModule.js.map +1 -1
  22. package/lib/module/driverState.js +8 -0
  23. package/lib/module/driverState.js.map +1 -1
  24. package/lib/module/drivers.native.js +122 -32
  25. package/lib/module/drivers.native.js.map +1 -1
  26. package/lib/module/reactRequests.js +9 -3
  27. package/lib/module/reactRequests.js.map +1 -1
  28. package/lib/typescript/src/NativeSmoothClipModule.d.ts +1 -1
  29. package/lib/typescript/src/NativeSmoothClipModule.d.ts.map +1 -1
  30. package/lib/typescript/src/driverState.d.ts.map +1 -1
  31. package/lib/typescript/src/driverTypes.d.ts +3 -2
  32. package/lib/typescript/src/driverTypes.d.ts.map +1 -1
  33. package/lib/typescript/src/drivers.native.d.ts.map +1 -1
  34. package/lib/typescript/src/reactRequests.d.ts +3 -1
  35. package/lib/typescript/src/reactRequests.d.ts.map +1 -1
  36. package/package.json +1 -1
  37. package/src/NativeSmoothClipModule.ts +2 -1
  38. package/src/driverState.ts +8 -0
  39. package/src/driverTypes.ts +3 -2
  40. package/src/drivers.native.ts +160 -29
  41. package/src/reactRequests.ts +14 -2
@@ -54,17 +54,67 @@ internal inline fun normalizeClipGeometryPx(
54
54
  internal fun clipVisibility(isEmpty: Boolean): Int =
55
55
  if (isEmpty) View.INVISIBLE else View.VISIBLE
56
56
 
57
+ /** Rounded integer origin of an outline edge. */
58
+ internal fun outlineOrigin(value: Float): Int = value.roundToInt()
59
+
60
+ /**
61
+ * Rounded far edge, derived from the rounded origin plus the rounded extent.
62
+ *
63
+ * `Outline.setRoundRect` takes an int rect, and rounding the two edges
64
+ * independently makes `round(far) - round(origin)` alternate between the floor
65
+ * and the ceil of a *constant* extent as the origin's fraction sweeps: a pure
66
+ * translation breathes the emitted size by 1 px, frame after frame, while the
67
+ * content inside translates in floats. That oscillation is what reads as
68
+ * shimmer on the slow tail of an animation, where the per-frame delta drops
69
+ * below a pixel and the rounding becomes the motion.
70
+ *
71
+ * An int rect cannot hold origin, far edge and extent within half a pixel at
72
+ * once (extent = far - origin, so bounding both edges at 0.5 bounds the extent
73
+ * at 1.0); the only choice is which error oscillates. Deriving the far edge
74
+ * makes the emitted extent a pure function of the true extent — stable, error
75
+ * <= 0.5 px — and moves the slack onto the far edge, <= 1.0 px and static. A
76
+ * static bias does not read as jank; an alternating one does.
77
+ *
78
+ * Two consequences, both benign:
79
+ * - The far edge can overshoot the host bound by up to 1 px instead of 0.5. The
80
+ * view's own bounds already clip, so an over-large outline just means that
81
+ * edge is not clipped that frame — which independent rounding already
82
+ * produced whenever `round(far)` rounded up.
83
+ * - A sub-pixel extent (0 < extent < 0.5) rounds to a degenerate rect, which
84
+ * `Outline.setRoundRect` collapses to `setEmpty()`. Independent rounding hit
85
+ * that state on some frames and a 1 px rect on others, i.e. it flickered;
86
+ * deriving the far edge makes it consistent. The emitted integer rect also
87
+ * governs visibility, accessibility and hit testing, so a view never remains
88
+ * logically visible while its outline clips every pixel.
89
+ */
90
+ internal fun outlineFarEdge(origin: Float, far: Float): Int =
91
+ outlineOrigin(origin) + (far - origin).roundToInt()
92
+
93
+ /** Exactly mirrors the non-empty requirement of Outline.setRoundRect(Int...). */
94
+ internal fun outlineRectIsEmpty(
95
+ left: Int,
96
+ top: Int,
97
+ right: Int,
98
+ bottom: Int,
99
+ ): Boolean = right <= left || bottom <= top
100
+
57
101
  /**
58
102
  * Outline dedupe on the rounded integer physical-pixel edges actually emitted
59
- * to the outline provider. Sub-pixel motion that rounds to the cached integers
60
- * must not invalidate the outline; the radius stays a float comparison because
61
- * it is emitted to the outline as a float.
103
+ * to the outline provider. Takes the already-derived edges rather than raw
104
+ * floats so the caller rounds once per frame and the predicate can never drift
105
+ * from what the provider emits; the radius stays a float comparison because it
106
+ * is emitted to the outline as a float.
107
+ *
108
+ * Sub-pixel motion that rounds to the cached integers must NOT invalidate the
109
+ * outline — it is carried on the view's translation instead (see
110
+ * SmoothClipView.applyClipPlacement), which is both cheaper and smoother than
111
+ * restaging an identical rect.
62
112
  */
63
113
  internal fun outlineChanged(
64
- left: Float,
65
- top: Float,
66
- right: Float,
67
- bottom: Float,
114
+ left: Int,
115
+ top: Int,
116
+ right: Int,
117
+ bottom: Int,
68
118
  radius: Float,
69
119
  cachedLeft: Int,
70
120
  cachedTop: Int,
@@ -72,10 +122,10 @@ internal fun outlineChanged(
72
122
  cachedBottom: Int,
73
123
  cachedRadius: Float,
74
124
  ): Boolean =
75
- left.roundToInt() != cachedLeft ||
76
- top.roundToInt() != cachedTop ||
77
- right.roundToInt() != cachedRight ||
78
- bottom.roundToInt() != cachedBottom ||
125
+ left != cachedLeft ||
126
+ top != cachedTop ||
127
+ right != cachedRight ||
128
+ bottom != cachedBottom ||
79
129
  radius != cachedRadius
80
130
 
81
131
  internal fun clipAccessibility(
@@ -1,5 +1,6 @@
1
1
  package com.smoothclipview
2
2
 
3
+ import android.view.Choreographer
3
4
  import com.facebook.proguard.annotations.DoNotStrip
4
5
  import com.facebook.react.turbomodule.core.interfaces.BindingsInstallerHolder
5
6
 
@@ -31,6 +32,7 @@ internal object SmoothClipBindings {
31
32
  density: Double,
32
33
  widthPx: Double,
33
34
  heightPx: Double,
35
+ lifecycleVisible: Boolean,
34
36
  )
35
37
 
36
38
  /**
@@ -48,15 +50,37 @@ internal object SmoothClipBindings {
48
50
  external fun nativeUnregisterView(driverId: Double, view: SmoothClipView)
49
51
 
50
52
  /**
51
- * Notifies the registry that a registered view attached to a window; a
52
- * latched animation may only start once a view can produce a visible
53
- * frame (attached and with real host geometry).
53
+ * Pushes both positive and negative attachment/window-visibility state.
54
+ * The registry combines it with real host geometry before a latch can run.
54
55
  */
55
- external fun nativeViewBecameDisplayable(driverId: Double, view: SmoothClipView)
56
+ external fun nativeSetViewLifecycleVisibility(
57
+ driverId: Double,
58
+ view: SmoothClipView,
59
+ lifecycleVisible: Boolean,
60
+ )
56
61
 
57
62
  /**
58
63
  * Releases runtime-bound listener state during host teardown; must run
59
64
  * while the JS runtime is still alive (module invalidate).
60
65
  */
61
66
  external fun nativeInvalidate()
67
+
68
+ private val frameCallback = Choreographer.FrameCallback { frameTimeNanos ->
69
+ nativeOnFrame(frameTimeNanos)
70
+ }
71
+
72
+ /**
73
+ * Posts the registry frame loop into this thread's Java Choreographer.
74
+ * Plain postFrameCallback lands in the animation phase, so the advance
75
+ * runs in the same doFrame pass as Reanimated prop updates — always
76
+ * before the traversal draws. The C++ registry calls this (main thread
77
+ * only) and dedupes posts with its own scheduled flag.
78
+ */
79
+ @JvmStatic
80
+ fun scheduleFrame() {
81
+ Choreographer.getInstance().postFrameCallback(frameCallback)
82
+ }
83
+
84
+ /** Advances the frame loop with the doFrame vsync timestamp. */
85
+ external fun nativeOnFrame(frameTimeNanos: Long)
62
86
  }
@@ -39,6 +39,7 @@ class SmoothClipModule(context: ReactApplicationContext) :
39
39
  contentTranslateX: Double,
40
40
  contentTranslateY: Double,
41
41
  takeOwnership: Boolean,
42
+ overridePendingAnimation: Boolean,
42
43
  ) = Unit
43
44
 
44
45
  override fun beginInteraction(driverId: Double): WritableArray =
@@ -9,7 +9,6 @@ import com.facebook.proguard.annotations.DoNotStrip
9
9
  import com.facebook.react.uimanager.PixelUtil
10
10
  import com.facebook.react.uimanager.ThemedReactContext
11
11
  import com.facebook.react.views.view.ReactViewGroup
12
- import kotlin.math.roundToInt
13
12
 
14
13
  class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
15
14
  internal val contentContainer = ReactViewGroup(context)
@@ -32,8 +31,17 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
32
31
  private var outlineRight = 0
33
32
  private var outlineBottom = 0
34
33
  private var clipIsEmpty = true
34
+ // Sub-pixel remainder of the clip origin, carried on this view's own
35
+ // translation and subtracted back out of the content container.
36
+ private var clipResidualX = 0f
37
+ private var clipResidualY = 0f
38
+ // The consumer's `transform` translation, kept apart from the residual so
39
+ // the two compose instead of clobbering each other.
40
+ private var userTranslationX = 0f
41
+ private var userTranslationY = 0f
35
42
  private var requestedImportantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_AUTO
36
43
  private var acceptingTouchStream = false
44
+ private var retainedTouchEvent: MotionEvent? = null
37
45
 
38
46
  /** Driver this view is registered with in the native registry (0 = none). */
39
47
  internal var boundDriverId: Double = 0.0
@@ -110,14 +118,8 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
110
118
  requestedWidth = nextWidth
111
119
  requestedHeight = nextHeight
112
120
  requestedRadius = nextRadius
113
- if (requestedContentTranslateX != nextContentTranslateX) {
114
- requestedContentTranslateX = nextContentTranslateX
115
- contentContainer.translationX = nextContentTranslateX
116
- }
117
- if (requestedContentTranslateY != nextContentTranslateY) {
118
- requestedContentTranslateY = nextContentTranslateY
119
- contentContainer.translationY = nextContentTranslateY
120
- }
121
+ requestedContentTranslateX = nextContentTranslateX
122
+ requestedContentTranslateY = nextContentTranslateY
121
123
  applyRequestedGeometry()
122
124
  }
123
125
 
@@ -136,14 +138,11 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
136
138
  contentTranslateX: Float,
137
139
  contentTranslateY: Float,
138
140
  ) {
139
- if (requestedContentTranslateX != contentTranslateX) {
140
- requestedContentTranslateX = contentTranslateX
141
- contentContainer.translationX = contentTranslateX
142
- }
143
- if (requestedContentTranslateY != contentTranslateY) {
144
- requestedContentTranslateY = contentTranslateY
145
- contentContainer.translationY = contentTranslateY
146
- }
141
+ // Stored only; applyNormalizedClipPx computes the clip residual and
142
+ // applyClipPlacement writes both translations together, because the
143
+ // content's final offset depends on the residual as well.
144
+ requestedContentTranslateX = contentTranslateX
145
+ requestedContentTranslateY = contentTranslateY
147
146
  applyNormalizedClipPx(left, top, right, bottom, radius)
148
147
  }
149
148
 
@@ -168,12 +167,28 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
168
167
  bottom: Float,
169
168
  radius: Float,
170
169
  ) {
171
- val isEmpty = right <= left || bottom <= top
170
+ // Far edges are derived, not rounded independently: see outlineFarEdge.
171
+ // Independent rounding breathes the emitted size by 1 px under pure
172
+ // translation, which is the visible artifact once an animation's tail
173
+ // slows below a pixel per frame.
174
+ val nextOutlineLeft = outlineOrigin(left)
175
+ val nextOutlineTop = outlineOrigin(top)
176
+ val nextOutlineRight = outlineFarEdge(left, right)
177
+ val nextOutlineBottom = outlineFarEdge(top, bottom)
178
+ // Outline.setRoundRect collapses a degenerate integer rect to empty.
179
+ // Use the exact emitted geometry as the single semantic source for
180
+ // rendering, visibility, accessibility and hit testing.
181
+ val isEmpty = outlineRectIsEmpty(
182
+ nextOutlineLeft,
183
+ nextOutlineTop,
184
+ nextOutlineRight,
185
+ nextOutlineBottom,
186
+ )
172
187
  val outlineGeometryChanged = outlineChanged(
173
- left,
174
- top,
175
- right,
176
- bottom,
188
+ nextOutlineLeft,
189
+ nextOutlineTop,
190
+ nextOutlineRight,
191
+ nextOutlineBottom,
177
192
  radius,
178
193
  outlineLeft,
179
194
  outlineTop,
@@ -182,33 +197,84 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
182
197
  clipRadius,
183
198
  )
184
199
 
185
- if (!outlineGeometryChanged && isEmpty == clipIsEmpty) {
186
- return
187
- }
188
-
189
200
  clipLeft = left
190
201
  clipTop = top
191
202
  clipRight = right
192
203
  clipBottom = bottom
193
204
  clipRadius = radius
194
- outlineLeft = left.roundToInt()
195
- outlineTop = top.roundToInt()
196
- outlineRight = right.roundToInt()
197
- outlineBottom = bottom.roundToInt()
205
+ // Whatever integer rounding threw away, carried on this view's own
206
+ // translation so the clip edge still lands where the driver asked
207
+ // instead of snapping to the pixel grid while the content it clips
208
+ // slides in floats. It is a single scalar per axis only because the far
209
+ // edges are derived from the origin — with independently rounded edges
210
+ // no one translation could place both.
211
+ clipResidualX = left - nextOutlineLeft
212
+ clipResidualY = top - nextOutlineTop
213
+ applyClipPlacement()
198
214
 
199
215
  if (isEmpty != clipIsEmpty) {
216
+ if (isEmpty) cancelAcceptedTouchStream()
200
217
  clipIsEmpty = isEmpty
201
218
  visibility = clipVisibility(isEmpty)
202
219
  importantForAccessibility = clipAccessibility(
203
220
  isEmpty,
204
221
  requestedImportantForAccessibility,
205
222
  )
206
- if (isEmpty) acceptingTouchStream = false
207
223
  }
208
224
 
225
+ // An emptiness transition necessarily changes at least one emitted
226
+ // edge, so the existing edge/radius key cannot skip invalidateOutline.
227
+ if (!outlineGeometryChanged) return
228
+
229
+ outlineLeft = nextOutlineLeft
230
+ outlineTop = nextOutlineTop
231
+ outlineRight = nextOutlineRight
232
+ outlineBottom = nextOutlineBottom
233
+ // Schedules the traversal too: invalidateOutline() ends in
234
+ // invalidateViewProperty(), which damages this view in its parent up
235
+ // to ViewRootImpl.scheduleTraversals(). A plain invalidate() on top
236
+ // would only add PFLAG_INVALIDATED, forcing a display-list re-record
237
+ // every frame to restage an outline the RenderNode applies as a
238
+ // property.
209
239
  invalidateOutline()
210
240
  }
211
241
 
242
+ /**
243
+ * Single writer for both translations. The view carries the sub-pixel
244
+ * remainder of the clip origin; the content container subtracts it back out
245
+ * so only the clip edge moves and the content stays exactly where the
246
+ * driver put it. Every write self-dedupes inside View.setTranslationX, so
247
+ * calling this on an unchanged frame costs four float comparisons.
248
+ */
249
+ private fun applyClipPlacement() {
250
+ super.setTranslationX(userTranslationX + clipResidualX)
251
+ super.setTranslationY(userTranslationY + clipResidualY)
252
+ contentContainer.translationX = requestedContentTranslateX - clipResidualX
253
+ contentContainer.translationY = requestedContentTranslateY - clipResidualY
254
+ }
255
+
256
+ // The clip's sub-pixel placement and the consumer's `transform` prop share
257
+ // one property, so they are composed rather than allowed to overwrite each
258
+ // other. RN routes every transform write through these setters
259
+ // (BaseViewManager.setTransformProperty), so intercepting them is enough.
260
+ //
261
+ // The getters are deliberately NOT overridden to hide the residual:
262
+ // View.setTranslationX dedupes by calling getTranslationX() virtually, so
263
+ // reporting the consumer's value there would make every write look like a
264
+ // change and re-invalidate the view on frames where nothing moved. The cost
265
+ // of leaving them alone is that a read-modify-write of translationX picks up
266
+ // the residual twice — sub-pixel, and nothing in RN's transform path reads
267
+ // back before writing.
268
+ override fun setTranslationX(translationX: Float) {
269
+ userTranslationX = translationX
270
+ super.setTranslationX(translationX + clipResidualX)
271
+ }
272
+
273
+ override fun setTranslationY(translationY: Float) {
274
+ userTranslationY = translationY
275
+ super.setTranslationY(translationY + clipResidualY)
276
+ }
277
+
212
278
  override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
213
279
  super.onSizeChanged(w, h, oldw, oldh)
214
280
  contentContainer.layout(0, 0, w, h)
@@ -234,12 +300,24 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
234
300
 
235
301
  override fun onAttachedToWindow() {
236
302
  super.onAttachedToWindow()
303
+ pushLifecycleVisibility()
304
+ }
305
+
306
+ override fun onDetachedFromWindow() {
237
307
  if (boundDriverId != 0.0) {
238
- // A latched animation may only start once this view can produce a
239
- // visible frame; window attach is that signal for views that
240
- // registered from a detached subtree.
241
- SmoothClipBindings.nativeViewBecameDisplayable(boundDriverId, this)
308
+ SmoothClipBindings.nativeSetViewLifecycleVisibility(
309
+ boundDriverId,
310
+ this,
311
+ false,
312
+ )
242
313
  }
314
+ clearTouchState()
315
+ super.onDetachedFromWindow()
316
+ }
317
+
318
+ override fun onWindowVisibilityChanged(visibility: Int) {
319
+ super.onWindowVisibilityChanged(visibility)
320
+ pushLifecycleVisibility()
243
321
  }
244
322
 
245
323
  override fun onConfigurationChanged(newConfig: Configuration) {
@@ -259,10 +337,27 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
259
337
 
260
338
  internal fun densityScale(): Double = PixelUtil.toPixelFromDIP(1f).toDouble()
261
339
 
340
+ internal fun isHostLifecycleVisible(): Boolean =
341
+ isAttachedToWindow && windowVisibility == VISIBLE
342
+
343
+ private fun pushLifecycleVisibility() {
344
+ if (boundDriverId == 0.0) return
345
+ SmoothClipBindings.nativeSetViewLifecycleVisibility(
346
+ boundDriverId,
347
+ this,
348
+ isHostLifecycleVisible(),
349
+ )
350
+ }
351
+
262
352
  private fun containsRoundedPoint(x: Float, y: Float): Boolean {
353
+ // The event arrives in this view's local space, which the parent has
354
+ // already inverse-transformed by the residual translation. Adding it
355
+ // back maps the point into the space clipLeft/clipTop are expressed in,
356
+ // so hit testing stays against the geometry the driver delivered — the
357
+ // same test as before the residual existed.
263
358
  return containsRoundedPointPx(
264
- x,
265
- y,
359
+ x + clipResidualX,
360
+ y + clipResidualY,
266
361
  clipLeft,
267
362
  clipTop,
268
363
  clipRight,
@@ -275,8 +370,16 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
275
370
  override fun dispatchTouchEvent(event: MotionEvent): Boolean {
276
371
  when (event.actionMasked) {
277
372
  MotionEvent.ACTION_DOWN -> {
373
+ clearTouchState()
278
374
  acceptingTouchStream = containsRoundedPoint(event.x, event.y)
279
375
  if (!acceptingTouchStream) return false
376
+ val accepted = super.dispatchTouchEvent(event)
377
+ if (accepted) {
378
+ retainTouchEvent(event)
379
+ } else {
380
+ clearTouchState()
381
+ }
382
+ return accepted
280
383
  }
281
384
  MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> {
282
385
  val result = if (acceptingTouchStream) {
@@ -284,12 +387,46 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
284
387
  } else {
285
388
  false
286
389
  }
287
- acceptingTouchStream = false
390
+ clearTouchState()
288
391
  return result
289
392
  }
290
393
  }
291
394
 
292
- return acceptingTouchStream && super.dispatchTouchEvent(event)
395
+ if (!acceptingTouchStream) return false
396
+ val result = super.dispatchTouchEvent(event)
397
+ retainTouchEvent(event)
398
+ return result
399
+ }
400
+
401
+ private fun retainTouchEvent(event: MotionEvent) {
402
+ retainedTouchEvent?.recycle()
403
+ retainedTouchEvent = MotionEvent.obtain(event)
404
+ }
405
+
406
+ private fun clearTouchState() {
407
+ acceptingTouchStream = false
408
+ retainedTouchEvent?.recycle()
409
+ retainedTouchEvent = null
410
+ }
411
+
412
+ private fun cancelAcceptedTouchStream() {
413
+ val latest = retainedTouchEvent
414
+ if (!acceptingTouchStream || latest == null) {
415
+ clearTouchState()
416
+ return
417
+ }
418
+ val cancel = MotionEvent.obtain(latest)
419
+ cancel.action = MotionEvent.ACTION_CANCEL
420
+ // Terminal BEFORE dispatch: a child's synchronous cancel handler can
421
+ // re-enter geometry application, and a nested emptiness transition
422
+ // must observe an already-ended stream instead of synthesizing a
423
+ // second cancel for it.
424
+ clearTouchState()
425
+ try {
426
+ super.dispatchTouchEvent(cancel)
427
+ } finally {
428
+ cancel.recycle()
429
+ }
293
430
  }
294
431
 
295
432
  fun setRequestedImportantForAccessibility(value: Int) {
@@ -313,7 +450,7 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
313
450
  }
314
451
 
315
452
  fun resetClipState() {
316
- acceptingTouchStream = false
453
+ clearTouchState()
317
454
  commandIsAuthoritative = false
318
455
  requestedX = 0f
319
456
  requestedY = 0f
@@ -322,8 +459,9 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
322
459
  requestedRadius = 0f
323
460
  requestedContentTranslateX = 0f
324
461
  requestedContentTranslateY = 0f
325
- contentContainer.translationX = 0f
326
- contentContainer.translationY = 0f
462
+ clipResidualX = 0f
463
+ clipResidualY = 0f
464
+ applyClipPlacement()
327
465
  outlineLeft = 0
328
466
  outlineTop = 0
329
467
  outlineRight = 0
@@ -167,6 +167,7 @@ class SmoothClipViewManager : ViewGroupManager<SmoothClipView>(),
167
167
  view.densityScale(),
168
168
  view.width.toDouble(),
169
169
  view.height.toDouble(),
170
+ view.isHostLifecycleVisible(),
170
171
  )
171
172
  } else if (!view.commandIsAuthoritative) {
172
173
  // Command-driven mode (driverId 0): the initial props are the only