react-native-smooth-clip-view 0.2.6 → 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 +32 -18
- package/android/build.gradle +15 -0
- package/android/src/main/cpp/SmoothClipAndroid.h +7 -8
- package/android/src/main/cpp/SmoothClipBindings.cpp +11 -7
- package/android/src/main/cpp/SmoothClipRegistry.cpp +139 -137
- package/android/src/main/java/com/smoothclipview/SmoothClipBindings.kt +8 -4
- package/android/src/main/java/com/smoothclipview/SmoothClipView.kt +75 -8
- package/android/src/main/java/com/smoothclipview/SmoothClipViewManager.kt +1 -0
- package/cpp/SmoothClipAnimationCurve.h +92 -0
- package/cpp/SmoothClipVelocityTracker.h +5 -7
- package/ios/SmoothClipRegistry.mm +231 -132
- package/ios/SmoothClipView.mm +38 -150
- package/ios/SmoothClipViewRegistry.h +4 -1
- package/lib/module/driverState.js +8 -0
- package/lib/module/driverState.js.map +1 -1
- package/lib/module/drivers.native.js +27 -6
- 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/driverState.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/driverState.ts +8 -0
- package/src/drivers.native.ts +31 -7
- package/src/reactRequests.ts +14 -2
|
@@ -32,6 +32,7 @@ internal object SmoothClipBindings {
|
|
|
32
32
|
density: Double,
|
|
33
33
|
widthPx: Double,
|
|
34
34
|
heightPx: Double,
|
|
35
|
+
lifecycleVisible: Boolean,
|
|
35
36
|
)
|
|
36
37
|
|
|
37
38
|
/**
|
|
@@ -49,11 +50,14 @@ internal object SmoothClipBindings {
|
|
|
49
50
|
external fun nativeUnregisterView(driverId: Double, view: SmoothClipView)
|
|
50
51
|
|
|
51
52
|
/**
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
* 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.
|
|
55
55
|
*/
|
|
56
|
-
external fun
|
|
56
|
+
external fun nativeSetViewLifecycleVisibility(
|
|
57
|
+
driverId: Double,
|
|
58
|
+
view: SmoothClipView,
|
|
59
|
+
lifecycleVisible: Boolean,
|
|
60
|
+
)
|
|
57
61
|
|
|
58
62
|
/**
|
|
59
63
|
* Releases runtime-bound listener state during host teardown; must run
|
|
@@ -41,6 +41,7 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
|
|
|
41
41
|
private var userTranslationY = 0f
|
|
42
42
|
private var requestedImportantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_AUTO
|
|
43
43
|
private var acceptingTouchStream = false
|
|
44
|
+
private var retainedTouchEvent: MotionEvent? = null
|
|
44
45
|
|
|
45
46
|
/** Driver this view is registered with in the native registry (0 = none). */
|
|
46
47
|
internal var boundDriverId: Double = 0.0
|
|
@@ -212,13 +213,13 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
|
|
|
212
213
|
applyClipPlacement()
|
|
213
214
|
|
|
214
215
|
if (isEmpty != clipIsEmpty) {
|
|
216
|
+
if (isEmpty) cancelAcceptedTouchStream()
|
|
215
217
|
clipIsEmpty = isEmpty
|
|
216
218
|
visibility = clipVisibility(isEmpty)
|
|
217
219
|
importantForAccessibility = clipAccessibility(
|
|
218
220
|
isEmpty,
|
|
219
221
|
requestedImportantForAccessibility,
|
|
220
222
|
)
|
|
221
|
-
if (isEmpty) acceptingTouchStream = false
|
|
222
223
|
}
|
|
223
224
|
|
|
224
225
|
// An emptiness transition necessarily changes at least one emitted
|
|
@@ -299,12 +300,24 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
|
|
|
299
300
|
|
|
300
301
|
override fun onAttachedToWindow() {
|
|
301
302
|
super.onAttachedToWindow()
|
|
303
|
+
pushLifecycleVisibility()
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
override fun onDetachedFromWindow() {
|
|
302
307
|
if (boundDriverId != 0.0) {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
308
|
+
SmoothClipBindings.nativeSetViewLifecycleVisibility(
|
|
309
|
+
boundDriverId,
|
|
310
|
+
this,
|
|
311
|
+
false,
|
|
312
|
+
)
|
|
307
313
|
}
|
|
314
|
+
clearTouchState()
|
|
315
|
+
super.onDetachedFromWindow()
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
override fun onWindowVisibilityChanged(visibility: Int) {
|
|
319
|
+
super.onWindowVisibilityChanged(visibility)
|
|
320
|
+
pushLifecycleVisibility()
|
|
308
321
|
}
|
|
309
322
|
|
|
310
323
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
|
@@ -324,6 +337,18 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
|
|
|
324
337
|
|
|
325
338
|
internal fun densityScale(): Double = PixelUtil.toPixelFromDIP(1f).toDouble()
|
|
326
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
|
+
|
|
327
352
|
private fun containsRoundedPoint(x: Float, y: Float): Boolean {
|
|
328
353
|
// The event arrives in this view's local space, which the parent has
|
|
329
354
|
// already inverse-transformed by the residual translation. Adding it
|
|
@@ -345,8 +370,16 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
|
|
|
345
370
|
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
|
|
346
371
|
when (event.actionMasked) {
|
|
347
372
|
MotionEvent.ACTION_DOWN -> {
|
|
373
|
+
clearTouchState()
|
|
348
374
|
acceptingTouchStream = containsRoundedPoint(event.x, event.y)
|
|
349
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
|
|
350
383
|
}
|
|
351
384
|
MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> {
|
|
352
385
|
val result = if (acceptingTouchStream) {
|
|
@@ -354,12 +387,46 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
|
|
|
354
387
|
} else {
|
|
355
388
|
false
|
|
356
389
|
}
|
|
357
|
-
|
|
390
|
+
clearTouchState()
|
|
358
391
|
return result
|
|
359
392
|
}
|
|
360
393
|
}
|
|
361
394
|
|
|
362
|
-
|
|
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
|
+
}
|
|
363
430
|
}
|
|
364
431
|
|
|
365
432
|
fun setRequestedImportantForAccessibility(value: Int) {
|
|
@@ -383,7 +450,7 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
|
|
|
383
450
|
}
|
|
384
451
|
|
|
385
452
|
fun resetClipState() {
|
|
386
|
-
|
|
453
|
+
clearTouchState()
|
|
387
454
|
commandIsAuthoritative = false
|
|
388
455
|
requestedX = 0f
|
|
389
456
|
requestedY = 0f
|
|
@@ -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
|
|
@@ -147,6 +147,11 @@ struct TimingRemainder {
|
|
|
147
147
|
bool representable;
|
|
148
148
|
};
|
|
149
149
|
|
|
150
|
+
struct TimingContinuation {
|
|
151
|
+
Presentation start;
|
|
152
|
+
TimingAnimation animation;
|
|
153
|
+
};
|
|
154
|
+
|
|
150
155
|
// Exact right-hand segment of a CSS/CoreAnimation cubic Bezier after a raw
|
|
151
156
|
// time cutoff. Restarting the original controls from a frozen presentation
|
|
152
157
|
// resets its slope and creates a visible velocity cusp. De Casteljau splitting
|
|
@@ -321,6 +326,39 @@ inline Presentation interpolate(
|
|
|
321
326
|
mix(from.contentTranslateY, to.contentTranslateY)};
|
|
322
327
|
}
|
|
323
328
|
|
|
329
|
+
// Freezes a timing animation at one already-rendered raw-time phase and
|
|
330
|
+
// returns the exact right-hand Bezier segment. Keeping the presentation and
|
|
331
|
+
// residual curve in one helper prevents lifecycle code from sampling one
|
|
332
|
+
// timestamp for the geometry and another for the duration.
|
|
333
|
+
inline TimingContinuation timingContinuation(
|
|
334
|
+
const TimingAnimation &original,
|
|
335
|
+
const Presentation &start,
|
|
336
|
+
const Presentation &target,
|
|
337
|
+
double rawProgress) {
|
|
338
|
+
const TimingRemainder remainder = timingRemainder(original, rawProgress);
|
|
339
|
+
return {
|
|
340
|
+
interpolate(start, target, remainder.easedProgress),
|
|
341
|
+
remainder.animation};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Android lifecycle callbacks can arrive long after the frame whose
|
|
345
|
+
// presentation was actually delivered. Keep the clock inputs at this shared
|
|
346
|
+
// boundary so callers cannot accidentally trim from callback wall time while
|
|
347
|
+
// calculating geometry from the last rendered frame.
|
|
348
|
+
inline TimingContinuation timingContinuationAtFrame(
|
|
349
|
+
const TimingAnimation &original,
|
|
350
|
+
const Presentation &start,
|
|
351
|
+
const Presentation &target,
|
|
352
|
+
double lastRenderedS,
|
|
353
|
+
double startedAtS,
|
|
354
|
+
double durationS) {
|
|
355
|
+
return timingContinuation(
|
|
356
|
+
original,
|
|
357
|
+
start,
|
|
358
|
+
target,
|
|
359
|
+
timingFraction(lastRenderedS, startedAtS, durationS));
|
|
360
|
+
}
|
|
361
|
+
|
|
324
362
|
// --- Keyframe curve -------------------------------------------------------
|
|
325
363
|
|
|
326
364
|
// Keyframes sampled from a smooth path, evaluated with monotone cubic Hermite
|
|
@@ -465,4 +503,58 @@ class KeyframeCurve {
|
|
|
465
503
|
std::size_t cursor_ = 1;
|
|
466
504
|
};
|
|
467
505
|
|
|
506
|
+
struct KeyframeContinuation {
|
|
507
|
+
Presentation start;
|
|
508
|
+
std::vector<Keyframe> frames;
|
|
509
|
+
double durationMs;
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
// Trims keyframes at the same raw-time phase that produced `frozen`. The new
|
|
513
|
+
// zero frame is the rendered presentation, not an analytically re-sampled
|
|
514
|
+
// neighbor, so a pause/re-latch cannot jump at the seam. Strictly increasing
|
|
515
|
+
// remapped offsets avoid duplicate/division-by-zero segments after repeated
|
|
516
|
+
// trims or floating-point collisions near the cutoff.
|
|
517
|
+
inline KeyframeContinuation keyframeContinuation(
|
|
518
|
+
const std::vector<Keyframe> &frames,
|
|
519
|
+
const Presentation &frozen,
|
|
520
|
+
const Presentation &target,
|
|
521
|
+
double durationMs,
|
|
522
|
+
double rawProgress) {
|
|
523
|
+
const double progress = clamp01(rawProgress);
|
|
524
|
+
KeyframeContinuation result{
|
|
525
|
+
frozen, {{0.0, frozen}}, std::max(0.0, durationMs * (1 - progress))};
|
|
526
|
+
if (progress < 1.0) {
|
|
527
|
+
for (const Keyframe &frame : frames) {
|
|
528
|
+
if (frame.offset <= progress) continue;
|
|
529
|
+
const double offset = clamp01(
|
|
530
|
+
(frame.offset - progress) / (1 - progress));
|
|
531
|
+
if (offset <= result.frames.back().offset + 1e-12) continue;
|
|
532
|
+
result.frames.push_back({offset, frame.presentation});
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
if (result.frames.size() == 1 ||
|
|
536
|
+
result.frames.back().offset < 1.0 - 1e-12) {
|
|
537
|
+
result.frames.push_back({1.0, target});
|
|
538
|
+
} else {
|
|
539
|
+
result.frames.back().offset = 1.0;
|
|
540
|
+
result.frames.back().presentation = target;
|
|
541
|
+
}
|
|
542
|
+
return result;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
inline KeyframeContinuation keyframeContinuationAtFrame(
|
|
546
|
+
const std::vector<Keyframe> &frames,
|
|
547
|
+
const Presentation &frozen,
|
|
548
|
+
const Presentation &target,
|
|
549
|
+
double lastRenderedS,
|
|
550
|
+
double startedAtS,
|
|
551
|
+
double durationS) {
|
|
552
|
+
return keyframeContinuation(
|
|
553
|
+
frames,
|
|
554
|
+
frozen,
|
|
555
|
+
target,
|
|
556
|
+
std::max(0.0, durationS * 1000.0),
|
|
557
|
+
timingFraction(lastRenderedS, startedAtS, durationS));
|
|
558
|
+
}
|
|
559
|
+
|
|
468
560
|
} // namespace smoothclip
|
|
@@ -67,13 +67,11 @@ inline void recordVelocitySample(
|
|
|
67
67
|
history.latestTimeS = nowS;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
// the staleness decay below is the forgetting mechanism, and it forgets
|
|
76
|
-
// gradually rather than all at once.
|
|
70
|
+
// Only explicit interactive writes route through this recorder. Native
|
|
71
|
+
// freeze/join/resume/static-finalization paths deliberately do not, so an
|
|
72
|
+
// animation cannot manufacture a later inherited velocity. During ordinary
|
|
73
|
+
// interaction, the staleness decay below is the forgetting mechanism and it
|
|
74
|
+
// forgets gradually rather than all at once.
|
|
77
75
|
|
|
78
76
|
inline void clearVelocitySamples(VelocitySampleHistory &history) {
|
|
79
77
|
history = VelocitySampleHistory{};
|