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.
- package/README.md +68 -26
- package/android/CMakeLists.txt +0 -1
- package/android/build.gradle +15 -0
- package/android/src/main/cpp/SmoothClipAndroid.h +12 -8
- package/android/src/main/cpp/SmoothClipBindings.cpp +39 -15
- package/android/src/main/cpp/SmoothClipRegistry.cpp +288 -285
- package/android/src/main/java/com/smoothclipview/ClipGeometryNormalizer.kt +61 -11
- package/android/src/main/java/com/smoothclipview/SmoothClipBindings.kt +28 -4
- package/android/src/main/java/com/smoothclipview/SmoothClipModule.kt +1 -0
- package/android/src/main/java/com/smoothclipview/SmoothClipView.kt +180 -42
- package/android/src/main/java/com/smoothclipview/SmoothClipViewManager.kt +1 -0
- package/cpp/SmoothClipAnimationCurve.h +560 -0
- package/cpp/SmoothClipAnimationId.h +32 -0
- package/cpp/SmoothClipRegistry.h +11 -1
- package/cpp/SmoothClipVelocityTracker.h +30 -10
- package/ios/SmoothClipRegistry.mm +398 -150
- package/ios/SmoothClipTurboModule.cpp +4 -2
- package/ios/SmoothClipTurboModule.h +2 -1
- package/ios/SmoothClipView.mm +85 -140
- package/ios/SmoothClipViewRegistry.h +4 -1
- package/lib/module/NativeSmoothClipModule.js.map +1 -1
- package/lib/module/driverState.js +8 -0
- package/lib/module/driverState.js.map +1 -1
- package/lib/module/drivers.native.js +122 -32
- package/lib/module/drivers.native.js.map +1 -1
- package/lib/module/reactRequests.js +9 -3
- package/lib/module/reactRequests.js.map +1 -1
- package/lib/typescript/src/NativeSmoothClipModule.d.ts +1 -1
- package/lib/typescript/src/NativeSmoothClipModule.d.ts.map +1 -1
- package/lib/typescript/src/driverState.d.ts.map +1 -1
- package/lib/typescript/src/driverTypes.d.ts +3 -2
- package/lib/typescript/src/driverTypes.d.ts.map +1 -1
- package/lib/typescript/src/drivers.native.d.ts.map +1 -1
- package/lib/typescript/src/reactRequests.d.ts +3 -1
- package/lib/typescript/src/reactRequests.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/NativeSmoothClipModule.ts +2 -1
- package/src/driverState.ts +8 -0
- package/src/driverTypes.ts +3 -2
- package/src/drivers.native.ts +160 -29
- 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.
|
|
60
|
-
*
|
|
61
|
-
*
|
|
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:
|
|
65
|
-
top:
|
|
66
|
-
right:
|
|
67
|
-
bottom:
|
|
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
|
|
76
|
-
top
|
|
77
|
-
right
|
|
78
|
-
bottom
|
|
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
|
-
*
|
|
52
|
-
*
|
|
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
|
|
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
|
-
|
|
114
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
-
|
|
390
|
+
clearTouchState()
|
|
288
391
|
return result
|
|
289
392
|
}
|
|
290
393
|
}
|
|
291
394
|
|
|
292
|
-
|
|
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
|
-
|
|
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
|
-
|
|
326
|
-
|
|
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
|