@nativescript/core 9.1.0-alpha.5 → 9.1.0-alpha.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/application/application.android.js +2 -0
- package/application/application.android.js.map +1 -1
- package/file-system/file-system-access.ios.js +12 -25
- package/file-system/file-system-access.ios.js.map +1 -1
- package/package.json +1 -1
- package/ui/action-bar/index.ios.js +0 -1
- package/ui/action-bar/index.ios.js.map +1 -1
- package/ui/button/index.android.d.ts +2 -9
- package/ui/button/index.android.js +7 -24
- package/ui/button/index.android.js.map +1 -1
- package/ui/button/index.ios.d.ts +2 -9
- package/ui/button/index.ios.js +17 -72
- package/ui/button/index.ios.js.map +1 -1
- package/ui/core/view/index.android.d.ts +1 -0
- package/ui/core/view/index.android.js +17 -8
- package/ui/core/view/index.android.js.map +1 -1
- package/ui/core/view/index.ios.d.ts +1 -0
- package/ui/core/view/index.ios.js +18 -20
- package/ui/core/view/index.ios.js.map +1 -1
- package/ui/core/view/view-common.d.ts +0 -2
- package/ui/core/view/view-common.js +1 -6
- package/ui/core/view/view-common.js.map +1 -1
- package/ui/core/view/view-helper/index.d.ts +1 -0
- package/ui/core/view/view-helper/index.ios.d.ts +24 -0
- package/ui/core/view/view-helper/index.ios.js +54 -5
- package/ui/core/view/view-helper/index.ios.js.map +1 -1
- package/ui/core/view-base/index.d.ts +19 -9
- package/ui/core/view-base/index.js +42 -29
- package/ui/core/view-base/index.js.map +1 -1
- package/ui/frame/fragment.transitions.android.d.ts +13 -13
- package/ui/frame/fragment.transitions.android.js.map +1 -1
- package/ui/frame/frame-common.d.ts +2 -1
- package/ui/frame/frame-common.js +10 -0
- package/ui/frame/frame-common.js.map +1 -1
- package/ui/frame/frame-helper-for-android.d.ts +4 -4
- package/ui/frame/frame-helper-for-android.js +17 -8
- package/ui/frame/frame-helper-for-android.js.map +1 -1
- package/ui/frame/index.android.d.ts +6 -11
- package/ui/frame/index.android.js +18 -59
- package/ui/frame/index.android.js.map +1 -1
- package/ui/frame/index.d.ts +12 -21
- package/ui/frame/index.ios.d.ts +2 -2
- package/ui/frame/index.ios.js.map +1 -1
- package/ui/label/index.ios.d.ts +2 -5
- package/ui/label/index.ios.js +12 -44
- package/ui/label/index.ios.js.map +1 -1
- package/ui/layouts/absolute-layout/index.ios.js +5 -1
- package/ui/layouts/absolute-layout/index.ios.js.map +1 -1
- package/ui/layouts/dock-layout/index.ios.js +5 -1
- package/ui/layouts/dock-layout/index.ios.js.map +1 -1
- package/ui/layouts/flexbox-layout/index.ios.js +5 -1
- package/ui/layouts/flexbox-layout/index.ios.js.map +1 -1
- package/ui/layouts/grid-layout/index.ios.js +5 -1
- package/ui/layouts/grid-layout/index.ios.js.map +1 -1
- package/ui/layouts/layout-base.android.d.ts +3 -10
- package/ui/layouts/layout-base.android.js +7 -24
- package/ui/layouts/layout-base.android.js.map +1 -1
- package/ui/layouts/layout-base.ios.js.map +1 -1
- package/ui/layouts/stack-layout/index.ios.js +5 -1
- package/ui/layouts/stack-layout/index.ios.js.map +1 -1
- package/ui/layouts/wrap-layout/index.ios.js +5 -1
- package/ui/layouts/wrap-layout/index.ios.js.map +1 -1
- package/ui/list-view/index.d.ts +13 -1
- package/ui/list-view/index.ios.d.ts +3 -1
- package/ui/list-view/index.ios.js +48 -10
- package/ui/list-view/index.ios.js.map +1 -1
- package/ui/list-view/list-view-common.d.ts +14 -0
- package/ui/list-view/list-view-common.js +4 -0
- package/ui/list-view/list-view-common.js.map +1 -1
- package/ui/page/index.ios.js +17 -4
- package/ui/page/index.ios.js.map +1 -1
- package/ui/scroll-view/index.d.ts +13 -0
- package/ui/scroll-view/index.ios.js +21 -7
- package/ui/scroll-view/index.ios.js.map +1 -1
- package/ui/scroll-view/scroll-view-common.d.ts +2 -0
- package/ui/scroll-view/scroll-view-common.js +8 -0
- package/ui/scroll-view/scroll-view-common.js.map +1 -1
- package/ui/styling/style/index.d.ts +1 -0
- package/ui/styling/style/index.js.map +1 -1
- package/ui/styling/style-properties.d.ts +1 -0
- package/ui/styling/style-properties.js +13 -4
- package/ui/styling/style-properties.js.map +1 -1
- package/ui/tab-view/index.ios.d.ts +10 -1
- package/ui/tab-view/index.ios.js +12 -4
- package/ui/tab-view/index.ios.js.map +1 -1
- package/ui/text-base/index.android.d.ts +2 -9
- package/ui/text-base/index.android.js +7 -24
- package/ui/text-base/index.android.js.map +1 -1
- package/ui/text-field/index.ios.d.ts +2 -9
- package/ui/text-field/index.ios.js +2 -23
- package/ui/text-field/index.ios.js.map +1 -1
- package/ui/text-view/index.ios.d.ts +2 -9
- package/ui/text-view/index.ios.js +20 -72
- package/ui/text-view/index.ios.js.map +1 -1
- package/ui/transition/index.d.ts +2 -1
- package/ui/transition/modal-transition.ios.js +18 -4
- package/ui/transition/modal-transition.ios.js.map +1 -1
- package/ui/transition/page-transition.ios.d.ts +5 -0
- package/ui/transition/page-transition.ios.js +227 -31
- package/ui/transition/page-transition.ios.js.map +1 -1
- package/ui/transition/shared-transition-helper.ios.js +456 -20
- package/ui/transition/shared-transition-helper.ios.js.map +1 -1
- package/ui/transition/shared-transition.d.ts +23 -0
- package/ui/transition/shared-transition.js.map +1 -1
- package/utils/native-helper.ios.js +15 -4
- package/utils/native-helper.ios.js.map +1 -1
|
@@ -34,6 +34,10 @@ export class SharedTransitionHelper {
|
|
|
34
34
|
independent: [],
|
|
35
35
|
};
|
|
36
36
|
}
|
|
37
|
+
// Track every matched source view so cleanup can restore alpha,
|
|
38
|
+
// including duplicates (same album surfacing in multiple lists)
|
|
39
|
+
// that we hide but don't create a snapshot for.
|
|
40
|
+
transition.sharedElements._allPresentingViews = sharedElements.slice();
|
|
37
41
|
if (SharedTransition.DEBUG) {
|
|
38
42
|
console.log(` ${type}: Present`);
|
|
39
43
|
console.log(`1. Found sharedTransitionTags to animate:`, sharedElementTags);
|
|
@@ -46,12 +50,22 @@ export class SharedTransitionHelper {
|
|
|
46
50
|
const pageEndTags = pageEnd?.sharedTransitionTags || {};
|
|
47
51
|
// console.log('pageEndIndependentTags:', pageEndIndependentTags);
|
|
48
52
|
const positionSharedTags = async () => {
|
|
53
|
+
const processedTags = new Set();
|
|
49
54
|
for (const presentingView of sharedElements) {
|
|
50
55
|
const presentingSharedElement = presentingView.ios;
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
56
|
+
const tag = presentingView.sharedTransitionTag;
|
|
57
|
+
if (processedTags.has(tag)) {
|
|
58
|
+
// The same item can appear in multiple lists on the source page
|
|
59
|
+
// (e.g., a featured album also surfacing in "Recently Played").
|
|
60
|
+
// Each instance has the same sharedTransitionTag. Only the first
|
|
61
|
+
// one becomes the snapshot origin; the rest must still be hidden
|
|
62
|
+
// so they don't show through behind the animating snapshot.
|
|
63
|
+
if (presentingSharedElement) {
|
|
64
|
+
presentingSharedElement.alpha = 0;
|
|
65
|
+
}
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
processedTags.add(tag);
|
|
55
69
|
const presentedView = presented.find((v) => v.sharedTransitionTag === presentingView.sharedTransitionTag);
|
|
56
70
|
const presentedSharedElement = presentedView.ios;
|
|
57
71
|
const pageEndProps = pageEndTags[presentingView.sharedTransitionTag];
|
|
@@ -60,15 +74,25 @@ export class SharedTransitionHelper {
|
|
|
60
74
|
await pageEndProps?.callback(presentedView, 'present');
|
|
61
75
|
}
|
|
62
76
|
// treat images differently...
|
|
77
|
+
let imageSourceChangeListener;
|
|
63
78
|
if (presentedSharedElement instanceof UIImageView) {
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
|
|
79
|
+
// In case the image is loaded async, keep the snapshot's image in sync with
|
|
80
|
+
// the destination view's image. We hold a ref so we can detach the listener
|
|
81
|
+
// on cleanup (the View would otherwise hold a strong ref via the observers
|
|
82
|
+
// map and the snapshot closure would leak).
|
|
83
|
+
imageSourceChangeListener = () => {
|
|
67
84
|
snapshot.image = iOSUtils.snapshotView(presentedSharedElement, Screen.mainScreen.scale);
|
|
68
85
|
snapshot.tintColor = presentedSharedElement.tintColor;
|
|
69
|
-
}
|
|
86
|
+
};
|
|
87
|
+
presentedView.on('imageSourceChange', imageSourceChangeListener);
|
|
70
88
|
snapshot.tintColor = presentedSharedElement.tintColor;
|
|
71
89
|
snapshot.contentMode = presentedSharedElement.contentMode;
|
|
90
|
+
// Seed the snapshot with the source's already-loaded image so the very
|
|
91
|
+
// first frame of the animation isn't blank if the destination image is
|
|
92
|
+
// still loading.
|
|
93
|
+
if (presentingSharedElement instanceof UIImageView) {
|
|
94
|
+
snapshot.image = presentingSharedElement.image;
|
|
95
|
+
}
|
|
72
96
|
}
|
|
73
97
|
iOSUtils.copyLayerProperties(snapshot, presentingSharedElement, pageEndProps?.propertiesToMatch);
|
|
74
98
|
snapshot.clipsToBounds = true;
|
|
@@ -96,12 +120,19 @@ export class SharedTransitionHelper {
|
|
|
96
120
|
startOpacity: presentedView.opacity,
|
|
97
121
|
endOpacity: presentingView.opacity,
|
|
98
122
|
propertiesToMatch: pageEndProps?.propertiesToMatch,
|
|
123
|
+
imageSourceChangeListener,
|
|
99
124
|
});
|
|
100
125
|
// set initial opacity to match the source view opacity
|
|
101
126
|
snapshot.alpha = presentingView.opacity;
|
|
102
|
-
//
|
|
103
|
-
|
|
104
|
-
|
|
127
|
+
// Hide both while animating in the transition context.
|
|
128
|
+
//
|
|
129
|
+
// We mutate native alpha directly rather than NS `opacity` so the NS-side
|
|
130
|
+
// style remains the user's intended value (typically 1). If we instead set
|
|
131
|
+
// `view.opacity = 0` and then a transition was interrupted before cleanup
|
|
132
|
+
// (or the page was reused), `startOpacity` here could capture 0 from a prior
|
|
133
|
+
// run and the cleanup below would leave the destination invisible.
|
|
134
|
+
presentingSharedElement.alpha = 0;
|
|
135
|
+
presentedSharedElement.alpha = 0;
|
|
105
136
|
}
|
|
106
137
|
};
|
|
107
138
|
const positionIndependentTags = async () => {
|
|
@@ -174,7 +205,8 @@ export class SharedTransitionHelper {
|
|
|
174
205
|
propertiesToMatch: pageEndProps?.propertiesToMatch,
|
|
175
206
|
zIndex: isNumber(pageEndProps?.zIndex) ? pageEndProps.zIndex : 0,
|
|
176
207
|
});
|
|
177
|
-
|
|
208
|
+
// Native alpha; see comment in positionSharedTags.
|
|
209
|
+
independentSharedElement.alpha = 0;
|
|
178
210
|
}
|
|
179
211
|
}
|
|
180
212
|
};
|
|
@@ -199,9 +231,41 @@ export class SharedTransitionHelper {
|
|
|
199
231
|
// Important: always set after above shared element positions have had their start positions set
|
|
200
232
|
transition.presented.view.alpha = isNumber(pageStart?.opacity) ? pageStart?.opacity : 0;
|
|
201
233
|
transition.presented.view.frame = CGRectMake(startFrame.x, startFrame.y, startFrame.width, startFrame.height);
|
|
234
|
+
// Optional top-corner rounding for sheet-style transitions (e.g.
|
|
235
|
+
// a modal that's rounded at the bottom of the screen and flattens
|
|
236
|
+
// as it slides up). Setting maskedCorners and masksToBounds here
|
|
237
|
+
// (before the animation) so they're stable during the animated
|
|
238
|
+
// cornerRadius change.
|
|
239
|
+
if (isNumber(pageStart?.cornerRadius) || isNumber(pageEnd?.cornerRadius)) {
|
|
240
|
+
const layer = transition.presented.view.layer;
|
|
241
|
+
// top-left | top-right | bottom-left | bottom-right (CACornerMask bits)
|
|
242
|
+
layer.maskedCorners = 15;
|
|
243
|
+
layer.masksToBounds = true;
|
|
244
|
+
if (isNumber(pageStart?.cornerRadius)) {
|
|
245
|
+
layer.cornerRadius = pageStart.cornerRadius;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
202
248
|
const cleanupPresent = () => {
|
|
203
249
|
for (const presented of transition.sharedElements.presented) {
|
|
204
|
-
|
|
250
|
+
// Detach the per-transition imageSourceChange listener registered
|
|
251
|
+
// during positionSharedTags so it doesn't leak (the snapshot is gone).
|
|
252
|
+
if (presented.imageSourceChangeListener) {
|
|
253
|
+
presented.view.off('imageSourceChange', presented.imageSourceChangeListener);
|
|
254
|
+
presented.imageSourceChangeListener = null;
|
|
255
|
+
}
|
|
256
|
+
// Restore the destination native alpha from the NS opacity (the source
|
|
257
|
+
// of truth). NS opacity wasn't modified during the transition, so this
|
|
258
|
+
// reflects the user's intended visibility (typically 1).
|
|
259
|
+
presented.view.ios.alpha = presented.view.opacity;
|
|
260
|
+
}
|
|
261
|
+
// Restore native alpha on every source view we touched — including
|
|
262
|
+
// duplicates that share a sharedTransitionTag and were hidden without
|
|
263
|
+
// a snapshot. Even though the source page is being removed, the View
|
|
264
|
+
// instances may be reused on subsequent navigations.
|
|
265
|
+
for (const sourceView of transition.sharedElements._allPresentingViews || []) {
|
|
266
|
+
if (sourceView?.ios) {
|
|
267
|
+
sourceView.ios.alpha = sourceView.opacity;
|
|
268
|
+
}
|
|
205
269
|
}
|
|
206
270
|
for (const presenting of transition.sharedElements.presenting) {
|
|
207
271
|
presenting.snapshot.removeFromSuperview();
|
|
@@ -209,7 +273,7 @@ export class SharedTransitionHelper {
|
|
|
209
273
|
for (const independent of transition.sharedElements.independent) {
|
|
210
274
|
independent.snapshot.removeFromSuperview();
|
|
211
275
|
if (independent.isPresented) {
|
|
212
|
-
independent.view.
|
|
276
|
+
independent.view.ios.alpha = independent.view.opacity;
|
|
213
277
|
}
|
|
214
278
|
}
|
|
215
279
|
SharedTransition.updateState(transition.id, {
|
|
@@ -232,6 +296,12 @@ export class SharedTransitionHelper {
|
|
|
232
296
|
transition.presented.view.alpha = isNumber(pageEnd?.opacity) ? pageEnd?.opacity : 1;
|
|
233
297
|
const endFrame = getRectFromProps(pageEnd);
|
|
234
298
|
transition.presented.view.frame = CGRectMake(endFrame.x, endFrame.y, endFrame.width, endFrame.height);
|
|
299
|
+
// Animate cornerRadius alongside the page frame. Implicit animation
|
|
300
|
+
// of cornerRadius is supported on iOS 11+, both inside
|
|
301
|
+
// UIView.animateWith… and UIViewPropertyAnimator's animations block.
|
|
302
|
+
if (isNumber(pageEnd?.cornerRadius)) {
|
|
303
|
+
transition.presented.view.layer.cornerRadius = pageEnd.cornerRadius;
|
|
304
|
+
}
|
|
235
305
|
if (pageOut) {
|
|
236
306
|
if (isNumber(pageOut.opacity)) {
|
|
237
307
|
transition.presenting.view.alpha = pageOut?.opacity;
|
|
@@ -310,6 +380,23 @@ export class SharedTransitionHelper {
|
|
|
310
380
|
});
|
|
311
381
|
if (type === 'page') {
|
|
312
382
|
transitionContext.containerView.insertSubviewBelowSubview(transition.presenting.view, transition.presented.view);
|
|
383
|
+
// UIKit's standard pop places the incoming source view off to the
|
|
384
|
+
// left so it can slide into position; for custom shared transitions
|
|
385
|
+
// (where the destination either slides down or morphs to source) we
|
|
386
|
+
// don't want that lateral slide. Pin the source to its fullscreen
|
|
387
|
+
// frame so it stays put underneath the animating destination.
|
|
388
|
+
// Users can still opt into a custom source animation via `pageOut`.
|
|
389
|
+
const sourceDefaults = getRectFromProps(null);
|
|
390
|
+
transition.presenting.view.frame = CGRectMake(0, 0, sourceDefaults.width, sourceDefaults.height);
|
|
391
|
+
}
|
|
392
|
+
// Morph + interactive dismiss: the gesture handler drives the
|
|
393
|
+
// animation via transforms and finalizes by calling cancel/finish
|
|
394
|
+
// on the transitionContext. Running the standard snapshot + spring
|
|
395
|
+
// rig here would race that and frequently call completeTransition(true)
|
|
396
|
+
// *before* the user's gesture ended — causing the dismiss to commit
|
|
397
|
+
// even on a cancel. Skip the rest of the dismiss setup entirely.
|
|
398
|
+
if (state.interactive?.dismiss?.morph && state.interactiveBegan) {
|
|
399
|
+
return;
|
|
313
400
|
}
|
|
314
401
|
// console.log('transitionContext.containerView.subviews.count:', transitionContext.containerView.subviews.count);
|
|
315
402
|
if (SharedTransition.DEBUG) {
|
|
@@ -322,7 +409,17 @@ export class SharedTransitionHelper {
|
|
|
322
409
|
const pageEndTags = pageEnd?.sharedTransitionTags || {};
|
|
323
410
|
const pageReturn = state.pageReturn;
|
|
324
411
|
for (const p of transition.sharedElements.presented) {
|
|
325
|
-
|
|
412
|
+
// Use native alpha (not NS opacity) so we don't pollute the NS-side
|
|
413
|
+
// value if the dismiss is interrupted; cleanup restores from NS opacity.
|
|
414
|
+
p.view.ios.alpha = 0;
|
|
415
|
+
}
|
|
416
|
+
// Ensure top-corner masking + clipping is in place before we animate
|
|
417
|
+
// cornerRadius back toward the start value. If the present pass already
|
|
418
|
+
// set these, this is a harmless no-op.
|
|
419
|
+
if (isNumber(pageReturn?.cornerRadius)) {
|
|
420
|
+
const layer = transition.presented.view.layer;
|
|
421
|
+
layer.maskedCorners = 15; // top-left | top-right | bottom-left | bottom-right
|
|
422
|
+
layer.masksToBounds = true;
|
|
326
423
|
}
|
|
327
424
|
// combine to order by zIndex and add to transition context
|
|
328
425
|
const snapshotData = transition.sharedElements.presenting.concat(transition.sharedElements.independent);
|
|
@@ -365,13 +462,43 @@ export class SharedTransitionHelper {
|
|
|
365
462
|
// add snapshot to animate
|
|
366
463
|
transitionContext.containerView.addSubview(data.snapshot);
|
|
367
464
|
}
|
|
465
|
+
// Hide every source-side shared element now that snapshots have been
|
|
466
|
+
// captured. The animating snapshot covers them; without this, the user
|
|
467
|
+
// sees a "double image" — the real source element AND the snapshot
|
|
468
|
+
// animating over it back to its position. Use the full list (not just
|
|
469
|
+
// the deduped `presenting`) so duplicates from sister lists are hidden
|
|
470
|
+
// too. Restored in cleanupDismiss below.
|
|
471
|
+
for (const sourceView of transition.sharedElements._allPresentingViews || []) {
|
|
472
|
+
if (sourceView?.ios) {
|
|
473
|
+
sourceView.ios.alpha = 0;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
368
476
|
const cleanupDismiss = () => {
|
|
477
|
+
// Restore alpha on every source view we hid — including duplicate
|
|
478
|
+
// sources with the same sharedTransitionTag — using NS opacity as
|
|
479
|
+
// the source of truth.
|
|
480
|
+
for (const sourceView of transition.sharedElements._allPresentingViews || []) {
|
|
481
|
+
if (sourceView?.ios) {
|
|
482
|
+
sourceView.ios.alpha = sourceView.opacity;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
369
485
|
for (const presenting of transition.sharedElements.presenting) {
|
|
370
|
-
presenting.view.opacity = presenting.startOpacity;
|
|
371
486
|
presenting.snapshot.removeFromSuperview();
|
|
372
487
|
}
|
|
488
|
+
for (const presented of transition.sharedElements.presented) {
|
|
489
|
+
// Detach the imageSourceChange listener and restore alpha. Even though
|
|
490
|
+
// the destination page is being torn down, NS Views can outlive their
|
|
491
|
+
// nativeView and we shouldn't leak observers.
|
|
492
|
+
if (presented.imageSourceChangeListener) {
|
|
493
|
+
presented.view.off('imageSourceChange', presented.imageSourceChangeListener);
|
|
494
|
+
presented.imageSourceChangeListener = null;
|
|
495
|
+
}
|
|
496
|
+
if (presented.view.ios) {
|
|
497
|
+
presented.view.ios.alpha = presented.view.opacity;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
373
500
|
for (const independent of transition.sharedElements.independent) {
|
|
374
|
-
independent.view.
|
|
501
|
+
independent.view.ios.alpha = independent.view.opacity;
|
|
375
502
|
independent.snapshot.removeFromSuperview();
|
|
376
503
|
}
|
|
377
504
|
SharedTransition.finishState(transition.id);
|
|
@@ -390,6 +517,11 @@ export class SharedTransitionHelper {
|
|
|
390
517
|
transition.presented.view.alpha = isNumber(pageReturn?.opacity) ? pageReturn?.opacity : 0;
|
|
391
518
|
const endFrame = getRectFromProps(pageReturn, getPageStartDefaultsForType(type));
|
|
392
519
|
transition.presented.view.frame = CGRectMake(endFrame.x, endFrame.y, endFrame.width, endFrame.height);
|
|
520
|
+
// Animate cornerRadius back toward the start value (e.g. a sheet
|
|
521
|
+
// re-rounds as it slides offscreen).
|
|
522
|
+
if (isNumber(pageReturn?.cornerRadius)) {
|
|
523
|
+
transition.presented.view.layer.cornerRadius = pageReturn.cornerRadius;
|
|
524
|
+
}
|
|
393
525
|
if (pageOut) {
|
|
394
526
|
// always return to defaults if pageOut had been used
|
|
395
527
|
transition.presenting.view.alpha = 1;
|
|
@@ -450,20 +582,62 @@ export class SharedTransitionHelper {
|
|
|
450
582
|
switch (type) {
|
|
451
583
|
case 'page':
|
|
452
584
|
interactiveState.transitionContext.containerView.insertSubviewBelowSubview(state.instance.presenting.view, state.instance.presented.view);
|
|
585
|
+
// Pin the source view to its fullscreen frame so it doesn't slide
|
|
586
|
+
// in from off-left as the user drags. The default UIKit pop expects
|
|
587
|
+
// to animate the source from off-left; for shared-element / morph
|
|
588
|
+
// transitions we want the source to stay put underneath the
|
|
589
|
+
// animating destination so the visuals feel connected.
|
|
590
|
+
{
|
|
591
|
+
const sourceDefaults = getRectFromProps(null);
|
|
592
|
+
state.instance.presenting.view.frame = CGRectMake(0, 0, sourceDefaults.width, sourceDefaults.height);
|
|
593
|
+
}
|
|
453
594
|
break;
|
|
454
595
|
}
|
|
455
596
|
}
|
|
456
597
|
static interactiveUpdate(state, interactiveState, type, percent) {
|
|
457
598
|
if (interactiveState) {
|
|
599
|
+
if (state.interactive?.dismiss?.morph) {
|
|
600
|
+
// Morph mode: the gesture handler drives the destination view's
|
|
601
|
+
// transform directly. We only forward the event so observers can
|
|
602
|
+
// react to the gesture's percentage.
|
|
603
|
+
SharedTransition.notifyEvent(SharedTransition.interactiveUpdateEvent, {
|
|
604
|
+
id: state?.instance?.id,
|
|
605
|
+
type,
|
|
606
|
+
percent,
|
|
607
|
+
});
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
458
610
|
if (!interactiveState.added) {
|
|
611
|
+
// Defer setup until the gesture has clearly committed to a horizontal
|
|
612
|
+
// dismiss motion. A vertical scroll on a descendant scroll view also
|
|
613
|
+
// triggers the page-level pan gesture (NS pan recognizers don't yield
|
|
614
|
+
// to scroll-view pans by default), producing near-zero percent values.
|
|
615
|
+
// Without this guard we'd mount the dismiss snapshots and hide the
|
|
616
|
+
// destination during a scroll, making the image look "stuck" until the
|
|
617
|
+
// cancel animation runs.
|
|
618
|
+
if (Math.abs(percent) < 0.05) {
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
459
621
|
interactiveState.added = true;
|
|
460
622
|
for (const p of state.instance.sharedElements.presented) {
|
|
461
|
-
|
|
623
|
+
// Native alpha (not NS opacity) for the same reason as the dismiss path:
|
|
624
|
+
// if the user lets go without crossing the threshold, cancel uses NS
|
|
625
|
+
// opacity as source of truth to restore.
|
|
626
|
+
p.view.ios.alpha = 0;
|
|
462
627
|
}
|
|
463
628
|
for (const p of state.instance.sharedElements.presenting) {
|
|
464
629
|
p.snapshot.alpha = p.endOpacity;
|
|
465
630
|
interactiveState.transitionContext.containerView.addSubview(p.snapshot);
|
|
466
631
|
}
|
|
632
|
+
// Hide every source-side shared element (including duplicates that share
|
|
633
|
+
// a sharedTransitionTag in sister lists) so the user only sees the
|
|
634
|
+
// animating snapshot — not the snapshot AND the real source element
|
|
635
|
+
// simultaneously ("double image" during interactive dismissal).
|
|
636
|
+
for (const sourceView of state.instance.sharedElements._allPresentingViews || []) {
|
|
637
|
+
if (sourceView?.ios) {
|
|
638
|
+
sourceView.ios.alpha = 0;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
467
641
|
const pageStart = state.pageStart;
|
|
468
642
|
const startFrame = getRectFromProps(pageStart, getPageStartDefaultsForType(type));
|
|
469
643
|
interactiveState.propertyAnimator = UIViewPropertyAnimator.alloc().initWithDurationDampingRatioAnimations(1, 1, () => {
|
|
@@ -474,6 +648,11 @@ export class SharedTransitionHelper {
|
|
|
474
648
|
}
|
|
475
649
|
state.instance.presented.view.alpha = isNumber(state.pageReturn?.opacity) ? state.pageReturn?.opacity : 0;
|
|
476
650
|
state.instance.presented.view.frame = CGRectMake(startFrame.x, startFrame.y, state.instance.presented.view.bounds.size.width, state.instance.presented.view.bounds.size.height);
|
|
651
|
+
// Drive cornerRadius via the same percent the user controls — round
|
|
652
|
+
// the page back up as they pan it offscreen.
|
|
653
|
+
if (isNumber(state.pageReturn?.cornerRadius)) {
|
|
654
|
+
state.instance.presented.view.layer.cornerRadius = state.pageReturn.cornerRadius;
|
|
655
|
+
}
|
|
477
656
|
});
|
|
478
657
|
}
|
|
479
658
|
interactiveState.propertyAnimator.fractionComplete = percent;
|
|
@@ -485,13 +664,93 @@ export class SharedTransitionHelper {
|
|
|
485
664
|
}
|
|
486
665
|
}
|
|
487
666
|
static interactiveCancel(state, interactiveState, type) {
|
|
667
|
+
if (state?.instance && interactiveState && state.interactive?.dismiss?.morph) {
|
|
668
|
+
// Morph mode cancel — spring the destination view back to identity.
|
|
669
|
+
const view = state.instance.presented?.view;
|
|
670
|
+
let didFinalize = false;
|
|
671
|
+
const finalize = () => {
|
|
672
|
+
if (didFinalize)
|
|
673
|
+
return;
|
|
674
|
+
didFinalize = true;
|
|
675
|
+
if (view)
|
|
676
|
+
view.transform = CGAffineTransformIdentity;
|
|
677
|
+
// Restore the source-side element alphas that the gesture hid on
|
|
678
|
+
// engagement so the source page is correct when shown again.
|
|
679
|
+
for (const v of state.instance.sharedElements?._allPresentingViews || []) {
|
|
680
|
+
if (v?.ios)
|
|
681
|
+
v.ios.alpha = v.opacity;
|
|
682
|
+
}
|
|
683
|
+
if (interactiveState.transitionContext) {
|
|
684
|
+
interactiveState.transitionContext.cancelInteractiveTransition();
|
|
685
|
+
interactiveState.transitionContext.completeTransition(false);
|
|
686
|
+
}
|
|
687
|
+
SharedTransition.updateState(state?.instance?.id, {
|
|
688
|
+
interactiveBegan: false,
|
|
689
|
+
interactiveCancelled: true,
|
|
690
|
+
});
|
|
691
|
+
SharedTransition.notifyEvent(SharedTransition.interactiveCancelledEvent, {
|
|
692
|
+
id: state?.instance?.id,
|
|
693
|
+
type,
|
|
694
|
+
});
|
|
695
|
+
};
|
|
696
|
+
if (view) {
|
|
697
|
+
iOSUtils.animateWithSpring({
|
|
698
|
+
animations: () => {
|
|
699
|
+
view.transform = CGAffineTransformIdentity;
|
|
700
|
+
// Cross-fade sources back IN PARALLEL with the spring so
|
|
701
|
+
// they're visible from frame one of the snap-back. This
|
|
702
|
+
// also makes the source restoration robust to a new
|
|
703
|
+
// gesture interrupting our completion callback.
|
|
704
|
+
for (const v of state.instance.sharedElements?._allPresentingViews || []) {
|
|
705
|
+
if (v?.ios)
|
|
706
|
+
v.ios.alpha = v.opacity;
|
|
707
|
+
}
|
|
708
|
+
},
|
|
709
|
+
completion: () => finalize(),
|
|
710
|
+
});
|
|
711
|
+
// Safety: ensure finalize runs even if the animation completion
|
|
712
|
+
// is dropped (e.g. interrupted by another gesture).
|
|
713
|
+
setTimeout(finalize, 800);
|
|
714
|
+
}
|
|
715
|
+
else {
|
|
716
|
+
finalize();
|
|
717
|
+
}
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
if (state?.instance && interactiveState && !interactiveState.added) {
|
|
721
|
+
// The gesture engaged the interactive transition but never crossed the
|
|
722
|
+
// motion threshold in interactiveUpdate (e.g., the user was scrolling
|
|
723
|
+
// vertically). Nothing was visually set up — just tell UIKit we're
|
|
724
|
+
// cancelling and clear the flags.
|
|
725
|
+
if (interactiveState.transitionContext) {
|
|
726
|
+
interactiveState.transitionContext.cancelInteractiveTransition();
|
|
727
|
+
interactiveState.transitionContext.completeTransition(false);
|
|
728
|
+
}
|
|
729
|
+
SharedTransition.updateState(state?.instance?.id, {
|
|
730
|
+
interactiveBegan: false,
|
|
731
|
+
interactiveCancelled: true,
|
|
732
|
+
});
|
|
733
|
+
SharedTransition.notifyEvent(SharedTransition.interactiveCancelledEvent, {
|
|
734
|
+
id: state?.instance?.id,
|
|
735
|
+
type,
|
|
736
|
+
});
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
488
739
|
if (state?.instance && interactiveState?.added && interactiveState?.propertyAnimator) {
|
|
489
740
|
interactiveState.propertyAnimator.reversed = true;
|
|
490
741
|
const duration = isNumber(state.pageEnd?.duration) ? state.pageEnd?.duration / 1000 : CORE_ANIMATION_DEFAULTS.duration;
|
|
491
742
|
interactiveState.propertyAnimator.continueAnimationWithTimingParametersDurationFactor(null, duration);
|
|
492
743
|
setTimeout(() => {
|
|
493
744
|
for (const p of state.instance.sharedElements.presented) {
|
|
494
|
-
|
|
745
|
+
// Restore native alpha from NS opacity (the user-intended value).
|
|
746
|
+
p.view.ios.alpha = p.view.opacity;
|
|
747
|
+
}
|
|
748
|
+
// Restore alpha on every source view we hid during interactiveUpdate
|
|
749
|
+
// (including duplicates from sister lists).
|
|
750
|
+
for (const sourceView of state.instance.sharedElements._allPresentingViews || []) {
|
|
751
|
+
if (sourceView?.ios) {
|
|
752
|
+
sourceView.ios.alpha = sourceView.opacity;
|
|
753
|
+
}
|
|
495
754
|
}
|
|
496
755
|
for (const p of state.instance.sharedElements.presenting) {
|
|
497
756
|
p.snapshot.removeFromSuperview();
|
|
@@ -513,15 +772,192 @@ export class SharedTransitionHelper {
|
|
|
513
772
|
}
|
|
514
773
|
}
|
|
515
774
|
static interactiveFinish(state, interactiveState, type) {
|
|
775
|
+
if (state?.instance && interactiveState && state.interactive?.dismiss?.morph) {
|
|
776
|
+
// Morph mode finish — animate the destination view to the matching
|
|
777
|
+
// source element's frame and fade it out, then complete.
|
|
778
|
+
const view = state.instance.presented?.view;
|
|
779
|
+
const destTag = state.instance.sharedElements?.presented?.[0]?.view?.sharedTransitionTag;
|
|
780
|
+
const matchingSource = state.instance.sharedElements?.presenting?.find?.((p) => p.view.sharedTransitionTag === destTag);
|
|
781
|
+
const srcIos = matchingSource?.view?.ios;
|
|
782
|
+
const containerView = interactiveState.transitionContext?.containerView;
|
|
783
|
+
let targetTransform = null;
|
|
784
|
+
if (view && srcIos && containerView) {
|
|
785
|
+
const frameInContainer = srcIos.convertRectToView(srcIos.bounds, containerView);
|
|
786
|
+
const bounds = view.bounds;
|
|
787
|
+
if (bounds.size.width > 0 && bounds.size.height > 0) {
|
|
788
|
+
const targetScale = frameInContainer.size.width / bounds.size.width;
|
|
789
|
+
const targetCx = frameInContainer.origin.x + frameInContainer.size.width / 2;
|
|
790
|
+
const targetCy = frameInContainer.origin.y + frameInContainer.size.height / 2;
|
|
791
|
+
const currentCx = bounds.size.width / 2;
|
|
792
|
+
const currentCy = bounds.size.height / 2;
|
|
793
|
+
const tx = targetCx - currentCx;
|
|
794
|
+
const ty = targetCy - currentCy;
|
|
795
|
+
targetTransform = CGAffineTransformConcat(CGAffineTransformMakeTranslation(tx, ty), CGAffineTransformMakeScale(targetScale, targetScale));
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
// Build a snapshot fly-back for each shared element so the album
|
|
799
|
+
// art (etc.) does a real shared-element transition to its source
|
|
800
|
+
// frame in parallel with the destination view's shrink+fade.
|
|
801
|
+
// Without this, the image rides along with the morphing view and
|
|
802
|
+
// lands at a visually offset position rather than precisely back
|
|
803
|
+
// at the source thumbnail.
|
|
804
|
+
const sharedSnapshots = [];
|
|
805
|
+
if (containerView) {
|
|
806
|
+
for (const presented of state.instance.sharedElements?.presented || []) {
|
|
807
|
+
const destSharedView = presented.view?.ios;
|
|
808
|
+
const tag = presented.view?.sharedTransitionTag;
|
|
809
|
+
if (!destSharedView || !tag)
|
|
810
|
+
continue;
|
|
811
|
+
const sourceShared = state.instance.sharedElements?.presenting?.find?.((p) => p.view.sharedTransitionTag === tag);
|
|
812
|
+
const srcSharedView = sourceShared?.view?.ios;
|
|
813
|
+
if (!srcSharedView)
|
|
814
|
+
continue;
|
|
815
|
+
const startFrame = destSharedView.convertRectToView(destSharedView.bounds, containerView);
|
|
816
|
+
const endFrame = srcSharedView.convertRectToView(srcSharedView.bounds, containerView);
|
|
817
|
+
if (startFrame.size.width <= 0 || endFrame.size.width <= 0)
|
|
818
|
+
continue;
|
|
819
|
+
const snap = UIImageView.alloc().init();
|
|
820
|
+
if (destSharedView instanceof UIImageView && destSharedView.image) {
|
|
821
|
+
snap.image = destSharedView.image;
|
|
822
|
+
snap.contentMode = destSharedView.contentMode;
|
|
823
|
+
snap.tintColor = destSharedView.tintColor;
|
|
824
|
+
}
|
|
825
|
+
else {
|
|
826
|
+
try {
|
|
827
|
+
snap.image = iOSUtils.snapshotView(presented.view.ios, Screen.mainScreen.scale);
|
|
828
|
+
}
|
|
829
|
+
catch (e) {
|
|
830
|
+
continue;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
snap.clipsToBounds = true;
|
|
834
|
+
snap.layer.cornerRadius = destSharedView.layer?.cornerRadius || 0;
|
|
835
|
+
snap.frame = startFrame;
|
|
836
|
+
containerView.addSubview(snap);
|
|
837
|
+
// Hide the destination shared element so we don't see it
|
|
838
|
+
// shrinking inside the morphing view at the same time the
|
|
839
|
+
// snapshot is flying back to source — that would double up.
|
|
840
|
+
destSharedView.alpha = 0;
|
|
841
|
+
sharedSnapshots.push({
|
|
842
|
+
snap,
|
|
843
|
+
endFrame,
|
|
844
|
+
endCornerRadius: srcSharedView.layer?.cornerRadius || 0,
|
|
845
|
+
srcSharedView,
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
// Identify which source NS views have a matching fly-back snapshot.
|
|
850
|
+
// Those sources stay at alpha 0 through the spring and are restored
|
|
851
|
+
// inside finalize(), so the snapshot doesn't overlay an already-
|
|
852
|
+
// visible source mid-flight (which looks like two images for a beat).
|
|
853
|
+
const snapshottedSources = new Set();
|
|
854
|
+
for (const entry of sharedSnapshots) {
|
|
855
|
+
if (entry.srcSharedView)
|
|
856
|
+
snapshottedSources.add(entry.srcSharedView);
|
|
857
|
+
}
|
|
858
|
+
let didFinalize = false;
|
|
859
|
+
const finalize = () => {
|
|
860
|
+
if (didFinalize)
|
|
861
|
+
return;
|
|
862
|
+
didFinalize = true;
|
|
863
|
+
// Detach the imageSourceChange listeners that were attached during
|
|
864
|
+
// PRESENT — the destination View is about to be torn down.
|
|
865
|
+
for (const presented of state.instance.sharedElements?.presented || []) {
|
|
866
|
+
if (presented.imageSourceChangeListener) {
|
|
867
|
+
presented.view.off('imageSourceChange', presented.imageSourceChangeListener);
|
|
868
|
+
presented.imageSourceChangeListener = null;
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
// Restore the snapshotted sources (held at alpha 0 through the
|
|
872
|
+
// spring) immediately before removing the snapshot, so the
|
|
873
|
+
// snapshot's last frame and the source's first visible frame
|
|
874
|
+
// coincide exactly — no double-image, no gap.
|
|
875
|
+
for (const entry of sharedSnapshots) {
|
|
876
|
+
const srcSharedView = entry.srcSharedView;
|
|
877
|
+
if (srcSharedView) {
|
|
878
|
+
const nsView = state.instance.sharedElements?.presenting?.find?.((p) => p.view?.ios === srcSharedView)?.view;
|
|
879
|
+
srcSharedView.alpha = nsView ? nsView.opacity : 1;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
for (const { snap } of sharedSnapshots) {
|
|
883
|
+
snap.removeFromSuperview();
|
|
884
|
+
}
|
|
885
|
+
if (view) {
|
|
886
|
+
view.transform = CGAffineTransformIdentity;
|
|
887
|
+
view.alpha = 1;
|
|
888
|
+
}
|
|
889
|
+
SharedTransition.finishState(state.instance.id);
|
|
890
|
+
if (interactiveState.transitionContext) {
|
|
891
|
+
interactiveState.transitionContext.finishInteractiveTransition();
|
|
892
|
+
interactiveState.transitionContext.completeTransition(true);
|
|
893
|
+
}
|
|
894
|
+
SharedTransition.notifyEvent(SharedTransition.finishedEvent, {
|
|
895
|
+
id: state?.instance?.id,
|
|
896
|
+
type,
|
|
897
|
+
action: 'interactiveFinish',
|
|
898
|
+
});
|
|
899
|
+
};
|
|
900
|
+
if (view && targetTransform) {
|
|
901
|
+
iOSUtils.animateWithSpring({
|
|
902
|
+
animations: () => {
|
|
903
|
+
view.transform = targetTransform;
|
|
904
|
+
view.alpha = 0;
|
|
905
|
+
// Restore source-side element alphas IN PARALLEL with the
|
|
906
|
+
// destination's morph + fade — but skip sources that have
|
|
907
|
+
// a flying snapshot. Those are held at alpha 0 and
|
|
908
|
+
// revealed by finalize() right before the snapshot is
|
|
909
|
+
// removed, so the user never sees both the snapshot AND
|
|
910
|
+
// the source visible at the same time.
|
|
911
|
+
for (const v of state.instance.sharedElements?._allPresentingViews || []) {
|
|
912
|
+
if (v?.ios && !snapshottedSources.has(v.ios)) {
|
|
913
|
+
v.ios.alpha = v.opacity;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
// Fly each shared-element snapshot to its source frame.
|
|
917
|
+
// Rides the same spring so the image lands precisely back
|
|
918
|
+
// at the source position as the destination view fades.
|
|
919
|
+
for (const { snap, endFrame, endCornerRadius } of sharedSnapshots) {
|
|
920
|
+
snap.frame = endFrame;
|
|
921
|
+
snap.layer.cornerRadius = endCornerRadius;
|
|
922
|
+
}
|
|
923
|
+
},
|
|
924
|
+
completion: () => finalize(),
|
|
925
|
+
});
|
|
926
|
+
// Safety: ensure finalize runs even if the animation completion
|
|
927
|
+
// is dropped (e.g. interrupted by another gesture).
|
|
928
|
+
setTimeout(finalize, 800);
|
|
929
|
+
}
|
|
930
|
+
else {
|
|
931
|
+
// No target transform; ensure sources are still restored.
|
|
932
|
+
for (const v of state.instance.sharedElements?._allPresentingViews || []) {
|
|
933
|
+
if (v?.ios)
|
|
934
|
+
v.ios.alpha = v.opacity;
|
|
935
|
+
}
|
|
936
|
+
finalize();
|
|
937
|
+
}
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
516
940
|
if (state?.instance && interactiveState?.added && interactiveState?.propertyAnimator) {
|
|
517
941
|
interactiveState.propertyAnimator.reversed = false;
|
|
518
942
|
const duration = isNumber(state.pageReturn?.duration) ? state.pageReturn?.duration / 1000 : CORE_ANIMATION_DEFAULTS.duration;
|
|
519
943
|
interactiveState.propertyAnimator.continueAnimationWithTimingParametersDurationFactor(null, duration);
|
|
520
944
|
setTimeout(() => {
|
|
945
|
+
// Restore alpha on every source view we hid (including duplicates) so
|
|
946
|
+
// the source page is visible if shown again.
|
|
947
|
+
for (const sourceView of state.instance.sharedElements._allPresentingViews || []) {
|
|
948
|
+
if (sourceView?.ios) {
|
|
949
|
+
sourceView.ios.alpha = sourceView.opacity;
|
|
950
|
+
}
|
|
951
|
+
}
|
|
521
952
|
for (const presenting of state.instance.sharedElements.presenting) {
|
|
522
|
-
presenting.view.opacity = presenting.startOpacity;
|
|
523
953
|
presenting.snapshot.removeFromSuperview();
|
|
524
954
|
}
|
|
955
|
+
for (const presented of state.instance.sharedElements.presented) {
|
|
956
|
+
if (presented.imageSourceChangeListener) {
|
|
957
|
+
presented.view.off('imageSourceChange', presented.imageSourceChangeListener);
|
|
958
|
+
presented.imageSourceChangeListener = null;
|
|
959
|
+
}
|
|
960
|
+
}
|
|
525
961
|
SharedTransition.finishState(state.instance.id);
|
|
526
962
|
interactiveState.propertyAnimator = null;
|
|
527
963
|
interactiveState.added = false;
|