@nativescript/core 9.1.0-alpha.6 → 9.1.0-alpha.8
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/package.json +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 +17 -4
- 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-base/index.d.ts +19 -9
- package/ui/core/view-base/index.js +41 -28
- 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-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/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/page/index.ios.js +13 -2
- package/ui/page/index.ios.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 -8
- package/ui/tab-view/index.ios.js +23 -17
- 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/modal-transition.ios.js +15 -1
- package/ui/transition/modal-transition.ios.js.map +1 -1
- package/ui/transition/page-transition.ios.js +72 -5
- package/ui/transition/page-transition.ios.js.map +1 -1
- package/ui/transition/shared-transition-helper.android.d.ts +3 -0
- package/ui/transition/shared-transition-helper.android.js +9 -0
- package/ui/transition/shared-transition-helper.android.js.map +1 -1
- package/ui/transition/shared-transition-helper.d.ts +7 -0
- package/ui/transition/shared-transition-helper.ios.d.ts +36 -0
- package/ui/transition/shared-transition-helper.ios.js +336 -96
- package/ui/transition/shared-transition-helper.ios.js.map +1 -1
- package/ui/transition/shared-transition.d.ts +43 -0
- package/ui/transition/shared-transition.js +57 -2
- package/ui/transition/shared-transition.js.map +1 -1
|
@@ -3,6 +3,115 @@ import { isNumber } from '../../utils/types';
|
|
|
3
3
|
import { Screen } from '../../platform';
|
|
4
4
|
import { CORE_ANIMATION_DEFAULTS } from '../../utils/animation-helpers';
|
|
5
5
|
import { ios as iOSUtils } from '../../utils/native-helper';
|
|
6
|
+
import { Color } from '../../color';
|
|
7
|
+
/**
|
|
8
|
+
* Apply a drop shadow behind the destination view during an interactive
|
|
9
|
+
* dismiss so it looks elevated above the source (Apple Music–style).
|
|
10
|
+
*
|
|
11
|
+
* Shadow + rounded corners can't coexist on the same CALayer on iOS — both
|
|
12
|
+
* `masksToBounds = true` and `layer.mask` clip the shadow as part of the
|
|
13
|
+
* compositing pipeline. So the shadow lives on a SIBLING CALayer inserted
|
|
14
|
+
* below the modal's layer in its superlayer (the same pattern NS uses for
|
|
15
|
+
* box-shadow via `outerShadowContainerLayer`). The modal keeps its own
|
|
16
|
+
* `cornerRadius` + `masksToBounds` so subviews still clip to the rounded
|
|
17
|
+
* shape.
|
|
18
|
+
*
|
|
19
|
+
* Because the shadow layer is a sibling (not a child), it doesn't inherit
|
|
20
|
+
* the modal's transform automatically — `syncInteractiveDismissShadow`
|
|
21
|
+
* mirrors transform/position/bounds during the drag.
|
|
22
|
+
*
|
|
23
|
+
* Falsy `shadowConfig` is a no-op.
|
|
24
|
+
*/
|
|
25
|
+
export function applyInteractiveDismissShadow(presentedView, shadowConfig) {
|
|
26
|
+
if (!presentedView?.layer || !shadowConfig)
|
|
27
|
+
return;
|
|
28
|
+
const layer = presentedView.layer;
|
|
29
|
+
const superlayer = layer.superlayer;
|
|
30
|
+
if (!superlayer)
|
|
31
|
+
return;
|
|
32
|
+
const bounds = layer.bounds;
|
|
33
|
+
if (bounds.size.width <= 0 || bounds.size.height <= 0)
|
|
34
|
+
return;
|
|
35
|
+
// Idempotent.
|
|
36
|
+
if (presentedView.__sharedTransitionShadowLayer)
|
|
37
|
+
return;
|
|
38
|
+
const cfg = shadowConfig === true ? {} : shadowConfig;
|
|
39
|
+
const colorInput = cfg.color ?? '#000';
|
|
40
|
+
let cgColor;
|
|
41
|
+
try {
|
|
42
|
+
const nsColor = typeof colorInput === 'string' ? new Color(colorInput) : colorInput;
|
|
43
|
+
cgColor = nsColor.ios?.CGColor || nsColor.CGColor || UIColor.blackColor.CGColor;
|
|
44
|
+
}
|
|
45
|
+
catch (_) {
|
|
46
|
+
cgColor = UIColor.blackColor.CGColor;
|
|
47
|
+
}
|
|
48
|
+
const opacity = isNumber(cfg.opacity) ? cfg.opacity : 0.3;
|
|
49
|
+
const radius = isNumber(cfg.radius) ? cfg.radius : 30;
|
|
50
|
+
const offsetX = isNumber(cfg.offset?.x) ? cfg.offset.x : 0;
|
|
51
|
+
const offsetY = isNumber(cfg.offset?.y) ? cfg.offset.y : 8;
|
|
52
|
+
const cornerRadius = layer.cornerRadius || 0;
|
|
53
|
+
const roundedRectPath = UIBezierPath.bezierPathWithRoundedRectCornerRadius(CGRectMake(0, 0, bounds.size.width, bounds.size.height), cornerRadius).CGPath;
|
|
54
|
+
const shadowLayer = CALayer.layer();
|
|
55
|
+
// Match the modal's geometry so shadow tracks 1:1.
|
|
56
|
+
shadowLayer.anchorPoint = layer.anchorPoint;
|
|
57
|
+
shadowLayer.bounds = bounds;
|
|
58
|
+
shadowLayer.position = layer.position;
|
|
59
|
+
shadowLayer.transform = layer.transform;
|
|
60
|
+
shadowLayer.shadowColor = cgColor;
|
|
61
|
+
shadowLayer.shadowOpacity = opacity;
|
|
62
|
+
shadowLayer.shadowRadius = radius;
|
|
63
|
+
shadowLayer.shadowOffset = CGSizeMake(offsetX, offsetY);
|
|
64
|
+
// Explicit shadowPath = shadow renders from this shape outward;
|
|
65
|
+
// the layer itself stays fully transparent.
|
|
66
|
+
shadowLayer.shadowPath = roundedRectPath;
|
|
67
|
+
CATransaction.begin();
|
|
68
|
+
CATransaction.setDisableActions(true);
|
|
69
|
+
superlayer.insertSublayerBelow(shadowLayer, layer);
|
|
70
|
+
CATransaction.commit();
|
|
71
|
+
presentedView.__sharedTransitionShadowLayer = shadowLayer;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Mirror the modal layer's geometry onto the sibling shadow layer so the
|
|
75
|
+
* shadow tracks the interactive drag. Called from the gesture handlers
|
|
76
|
+
* after the modal's state has been updated (either via direct transform
|
|
77
|
+
* in morph mode, or via `interactiveController.updateInteractiveTransition`
|
|
78
|
+
* for the non-morph modal/page case driven by UIViewPropertyAnimator).
|
|
79
|
+
*
|
|
80
|
+
* Reads the layer's **presentation** values when an animation is in flight
|
|
81
|
+
* — model values would point at the animation's end state and snap the
|
|
82
|
+
* shadow there. Falls back to model values when no animation is running.
|
|
83
|
+
*/
|
|
84
|
+
export function syncInteractiveDismissShadow(presentedView) {
|
|
85
|
+
const shadowLayer = presentedView?.__sharedTransitionShadowLayer;
|
|
86
|
+
if (!shadowLayer || !presentedView?.layer)
|
|
87
|
+
return;
|
|
88
|
+
const layer = presentedView.layer;
|
|
89
|
+
const pres = typeof layer.presentationLayer === 'function' ? layer.presentationLayer() : null;
|
|
90
|
+
const src = pres || layer;
|
|
91
|
+
const b = src.bounds;
|
|
92
|
+
CATransaction.begin();
|
|
93
|
+
CATransaction.setDisableActions(true);
|
|
94
|
+
shadowLayer.bounds = b;
|
|
95
|
+
shadowLayer.position = src.position;
|
|
96
|
+
shadowLayer.anchorPoint = src.anchorPoint;
|
|
97
|
+
shadowLayer.transform = src.transform;
|
|
98
|
+
shadowLayer.shadowPath = UIBezierPath.bezierPathWithRoundedRectCornerRadius(CGRectMake(0, 0, b.size.width, b.size.height), src.cornerRadius || 0).CGPath;
|
|
99
|
+
CATransaction.commit();
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Remove the sibling shadow layer. Safe to call repeatedly / when no
|
|
103
|
+
* shadow was applied.
|
|
104
|
+
*/
|
|
105
|
+
export function removeInteractiveDismissShadow(presentedView) {
|
|
106
|
+
const shadowLayer = presentedView?.__sharedTransitionShadowLayer;
|
|
107
|
+
if (!shadowLayer)
|
|
108
|
+
return;
|
|
109
|
+
CATransaction.begin();
|
|
110
|
+
CATransaction.setDisableActions(true);
|
|
111
|
+
shadowLayer.removeFromSuperlayer();
|
|
112
|
+
CATransaction.commit();
|
|
113
|
+
presentedView.__sharedTransitionShadowLayer = null;
|
|
114
|
+
}
|
|
6
115
|
export class SharedTransitionHelper {
|
|
7
116
|
static animate(state, transitionContext, type) {
|
|
8
117
|
const transition = state.instance;
|
|
@@ -136,78 +245,99 @@ export class SharedTransitionHelper {
|
|
|
136
245
|
}
|
|
137
246
|
};
|
|
138
247
|
const positionIndependentTags = async () => {
|
|
139
|
-
//
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
248
|
+
// "Independent" tags appear on only one of the two pages (or were
|
|
249
|
+
// explicitly enumerated in pageEnd.sharedTransitionTags). We auto-discover
|
|
250
|
+
// them so authors don't have to enumerate every tag manually:
|
|
251
|
+
// - source-only (orphan on presenting): fade *out* on present, fade
|
|
252
|
+
// back in on dismiss. e.g., a "FEATURED ALBUM" label on a hero card
|
|
253
|
+
// that has no counterpart on the destination page.
|
|
254
|
+
// - destination-only (orphan on presented): fade *in* on present, fade
|
|
255
|
+
// back out on dismiss. e.g., metadata that only exists on the detail
|
|
256
|
+
// page.
|
|
257
|
+
// Authors can override per-tag via pageStart / pageEnd `sharedTransitionTags`
|
|
258
|
+
// (e.g. add `y: -20` to slide while fading), or opt a view out entirely
|
|
259
|
+
// with `sharedTransitionIgnore`.
|
|
260
|
+
const orphanTags = [];
|
|
261
|
+
const seenOrphan = new Set();
|
|
262
|
+
const addOrphan = (tag) => {
|
|
263
|
+
if (!tag || seenOrphan.has(tag) || sharedElementTags.includes(tag))
|
|
264
|
+
return;
|
|
265
|
+
seenOrphan.add(tag);
|
|
266
|
+
orphanTags.push(tag);
|
|
267
|
+
};
|
|
268
|
+
for (const v of presenting)
|
|
269
|
+
addOrphan(v.sharedTransitionTag);
|
|
270
|
+
for (const v of presented)
|
|
271
|
+
addOrphan(v.sharedTransitionTag);
|
|
272
|
+
// Tags the user listed in pageEnd.sharedTransitionTags but that don't
|
|
273
|
+
// match a real view are silently skipped further down — keep them in
|
|
274
|
+
// the iteration so per-tag config still applies to existing orphans.
|
|
275
|
+
for (const tag in pageEndTags)
|
|
276
|
+
addOrphan(tag);
|
|
277
|
+
for (const tag of orphanTags) {
|
|
278
|
+
const pageStartIndependentProps = pageStart?.sharedTransitionTags ? pageStart?.sharedTransitionTags[tag] : null;
|
|
279
|
+
const pageEndProps = pageEndTags[tag];
|
|
280
|
+
let independentView = presenting.find((v) => v.sharedTransitionTag === tag);
|
|
281
|
+
let isPresented = false;
|
|
282
|
+
if (!independentView) {
|
|
283
|
+
independentView = presented.find((v) => v.sharedTransitionTag === tag);
|
|
149
284
|
if (!independentView) {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
154
|
-
isPresented = true;
|
|
155
|
-
}
|
|
156
|
-
const independentSharedElement = independentView.ios;
|
|
157
|
-
if (pageEndProps?.callback) {
|
|
158
|
-
await pageEndProps?.callback(independentView, 'present');
|
|
159
|
-
}
|
|
160
|
-
// let snapshot: UIImageView;
|
|
161
|
-
// if (isPresented) {
|
|
162
|
-
// snapshot = UIImageView.alloc().init();
|
|
163
|
-
// } else {
|
|
164
|
-
const snapshot = UIImageView.alloc().initWithImage(iOSUtils.snapshotView(independentSharedElement, Screen.mainScreen.scale));
|
|
165
|
-
// }
|
|
166
|
-
if (independentSharedElement instanceof UIImageView) {
|
|
167
|
-
// in case the image is loaded async, we need to update the snapshot when it changes
|
|
168
|
-
// todo: remove listener on transition end
|
|
169
|
-
// if (isPresented) {
|
|
170
|
-
// independentView.on('imageSourceChange', () => {
|
|
171
|
-
// snapshot.image = iOSNativeHelper.snapshotView(independentSharedElement, Screen.mainScreen.scale);
|
|
172
|
-
// snapshot.tintColor = independentSharedElement.tintColor;
|
|
173
|
-
// });
|
|
174
|
-
// }
|
|
175
|
-
snapshot.tintColor = independentSharedElement.tintColor;
|
|
176
|
-
snapshot.contentMode = independentSharedElement.contentMode;
|
|
285
|
+
// Tag declared in config but no matching view; skip (don't
|
|
286
|
+
// break — later tags might still resolve).
|
|
287
|
+
continue;
|
|
177
288
|
}
|
|
178
|
-
|
|
179
|
-
const startFrame = independentSharedElement.convertRectToView(independentSharedElement.bounds, transitionContext.containerView);
|
|
180
|
-
const startFrameRect = getRectFromProps(pageStartIndependentProps);
|
|
181
|
-
// adjust for any specified start positions
|
|
182
|
-
const startFrameAdjusted = CGRectMake(startFrame.origin.x + startFrameRect.x, startFrame.origin.y + startFrameRect.y, startFrame.size.width, startFrame.size.height);
|
|
183
|
-
// console.log('startFrameAdjusted:', tag, iOSNativeHelper.printCGRect(startFrameAdjusted));
|
|
184
|
-
// if (pageStartIndependentProps?.scale) {
|
|
185
|
-
// snapshot.transform = CGAffineTransformConcat(CGAffineTransformMakeTranslation(startFrameAdjusted.origin.x, startFrameAdjusted.origin.y), CGAffineTransformMakeScale(pageStartIndependentProps.scale.x, pageStartIndependentProps.scale.y))
|
|
186
|
-
// } else {
|
|
187
|
-
snapshot.frame = startFrame; //startFrameAdjusted;
|
|
188
|
-
// }
|
|
189
|
-
if (SharedTransition.DEBUG) {
|
|
190
|
-
console.log('---> ', independentView.sharedTransitionTag, ' frame:', iOSUtils.printCGRect(snapshot.frame));
|
|
191
|
-
}
|
|
192
|
-
const endFrameRect = getRectFromProps(pageEndProps);
|
|
193
|
-
const endFrame = CGRectMake(startFrame.origin.x + endFrameRect.x, startFrame.origin.y + endFrameRect.y, startFrame.size.width, startFrame.size.height);
|
|
194
|
-
// console.log('endFrame:', tag, iOSNativeHelper.printCGRect(endFrame));
|
|
195
|
-
transition.sharedElements.independent.push({
|
|
196
|
-
view: independentView,
|
|
197
|
-
isPresented,
|
|
198
|
-
startFrame,
|
|
199
|
-
snapshot,
|
|
200
|
-
endFrame,
|
|
201
|
-
startTransform: independentSharedElement.transform,
|
|
202
|
-
scale: pageEndProps.scale,
|
|
203
|
-
startOpacity: independentView.opacity,
|
|
204
|
-
endOpacity: isNumber(pageEndProps.opacity) ? pageEndProps.opacity : 0,
|
|
205
|
-
propertiesToMatch: pageEndProps?.propertiesToMatch,
|
|
206
|
-
zIndex: isNumber(pageEndProps?.zIndex) ? pageEndProps.zIndex : 0,
|
|
207
|
-
});
|
|
208
|
-
// Native alpha; see comment in positionSharedTags.
|
|
209
|
-
independentSharedElement.alpha = 0;
|
|
289
|
+
isPresented = true;
|
|
210
290
|
}
|
|
291
|
+
const independentSharedElement = independentView.ios;
|
|
292
|
+
if (pageEndProps?.callback) {
|
|
293
|
+
await pageEndProps?.callback(independentView, 'present');
|
|
294
|
+
}
|
|
295
|
+
const snapshot = UIImageView.alloc().initWithImage(iOSUtils.snapshotView(independentSharedElement, Screen.mainScreen.scale));
|
|
296
|
+
if (independentSharedElement instanceof UIImageView) {
|
|
297
|
+
snapshot.tintColor = independentSharedElement.tintColor;
|
|
298
|
+
snapshot.contentMode = independentSharedElement.contentMode;
|
|
299
|
+
}
|
|
300
|
+
snapshot.clipsToBounds = true;
|
|
301
|
+
// Copy layer properties (cornerRadius, borderWidth/Color)
|
|
302
|
+
// from the source view so the snapshot renders with the
|
|
303
|
+
// same rounded corners. Without this, source-side
|
|
304
|
+
// thumbnails/buttons appear with sharp square corners
|
|
305
|
+
// during a non-morph interactive dismiss — the snapshot
|
|
306
|
+
// is what the user sees through the dismissing modal,
|
|
307
|
+
// and its layer is freshly created with cornerRadius 0.
|
|
308
|
+
iOSUtils.copyLayerProperties(snapshot, independentSharedElement, pageEndProps?.propertiesToMatch);
|
|
309
|
+
const startFrame = independentSharedElement.convertRectToView(independentSharedElement.bounds, transitionContext.containerView);
|
|
310
|
+
snapshot.frame = startFrame;
|
|
311
|
+
if (SharedTransition.DEBUG) {
|
|
312
|
+
console.log('---> ', independentView.sharedTransitionTag, ' frame:', iOSUtils.printCGRect(snapshot.frame));
|
|
313
|
+
}
|
|
314
|
+
const endFrameRect = getRectFromProps(pageEndProps);
|
|
315
|
+
const endFrame = CGRectMake(startFrame.origin.x + (endFrameRect.x || 0), startFrame.origin.y + (endFrameRect.y || 0), startFrame.size.width, startFrame.size.height);
|
|
316
|
+
// Opacity defaults are side-aware: source-only orphans fade out (1 → 0),
|
|
317
|
+
// destination-only orphans fade in (0 → 1). The values are read back
|
|
318
|
+
// during dismiss as `endOpacity → startOpacity` so the return phase is
|
|
319
|
+
// automatically symmetric. Author-supplied opacity always wins.
|
|
320
|
+
const startOpacity = isNumber(pageStartIndependentProps?.opacity) ? pageStartIndependentProps.opacity : isPresented ? 0 : independentView.opacity;
|
|
321
|
+
const endOpacity = isNumber(pageEndProps?.opacity) ? pageEndProps.opacity : isPresented ? independentView.opacity : 0;
|
|
322
|
+
// Snapshot's initial visible alpha. Default UIImageView alpha is 1;
|
|
323
|
+
// destination-only orphans need to start at 0 so the fade-in is
|
|
324
|
+
// visible, hence we always set it explicitly.
|
|
325
|
+
snapshot.alpha = startOpacity;
|
|
326
|
+
transition.sharedElements.independent.push({
|
|
327
|
+
view: independentView,
|
|
328
|
+
isPresented,
|
|
329
|
+
startFrame,
|
|
330
|
+
snapshot,
|
|
331
|
+
endFrame,
|
|
332
|
+
startTransform: independentSharedElement.transform,
|
|
333
|
+
scale: pageEndProps?.scale,
|
|
334
|
+
startOpacity,
|
|
335
|
+
endOpacity,
|
|
336
|
+
propertiesToMatch: pageEndProps?.propertiesToMatch,
|
|
337
|
+
zIndex: isNumber(pageEndProps?.zIndex) ? pageEndProps.zIndex : 0,
|
|
338
|
+
});
|
|
339
|
+
// Native alpha; see comment in positionSharedTags.
|
|
340
|
+
independentSharedElement.alpha = 0;
|
|
211
341
|
}
|
|
212
342
|
};
|
|
213
343
|
// position all sharedTransitionTag elements
|
|
@@ -215,7 +345,7 @@ export class SharedTransitionHelper {
|
|
|
215
345
|
await positionIndependentTags();
|
|
216
346
|
// combine to order by zIndex and add to transition context
|
|
217
347
|
const snapshotData = transition.sharedElements.presenting.concat(transition.sharedElements.independent);
|
|
218
|
-
snapshotData.sort((a, b) => (a.zIndex
|
|
348
|
+
snapshotData.sort((a, b) => (a.zIndex || 0) - (b.zIndex || 0));
|
|
219
349
|
if (SharedTransition.DEBUG) {
|
|
220
350
|
console.log(`zIndex settings:`, snapshotData.map((s) => {
|
|
221
351
|
return {
|
|
@@ -238,8 +368,8 @@ export class SharedTransitionHelper {
|
|
|
238
368
|
// cornerRadius change.
|
|
239
369
|
if (isNumber(pageStart?.cornerRadius) || isNumber(pageEnd?.cornerRadius)) {
|
|
240
370
|
const layer = transition.presented.view.layer;
|
|
241
|
-
// top-left | top-right (CACornerMask bits)
|
|
242
|
-
layer.maskedCorners =
|
|
371
|
+
// top-left | top-right | bottom-left | bottom-right (CACornerMask bits)
|
|
372
|
+
layer.maskedCorners = 15;
|
|
243
373
|
layer.masksToBounds = true;
|
|
244
374
|
if (isNumber(pageStart?.cornerRadius)) {
|
|
245
375
|
layer.cornerRadius = pageStart.cornerRadius;
|
|
@@ -262,7 +392,7 @@ export class SharedTransitionHelper {
|
|
|
262
392
|
// duplicates that share a sharedTransitionTag and were hidden without
|
|
263
393
|
// a snapshot. Even though the source page is being removed, the View
|
|
264
394
|
// instances may be reused on subsequent navigations.
|
|
265
|
-
for (const sourceView of
|
|
395
|
+
for (const sourceView of transition.sharedElements._allPresentingViews || []) {
|
|
266
396
|
if (sourceView?.ios) {
|
|
267
397
|
sourceView.ios.alpha = sourceView.opacity;
|
|
268
398
|
}
|
|
@@ -418,12 +548,12 @@ export class SharedTransitionHelper {
|
|
|
418
548
|
// set these, this is a harmless no-op.
|
|
419
549
|
if (isNumber(pageReturn?.cornerRadius)) {
|
|
420
550
|
const layer = transition.presented.view.layer;
|
|
421
|
-
layer.maskedCorners =
|
|
551
|
+
layer.maskedCorners = 15; // top-left | top-right | bottom-left | bottom-right
|
|
422
552
|
layer.masksToBounds = true;
|
|
423
553
|
}
|
|
424
554
|
// combine to order by zIndex and add to transition context
|
|
425
555
|
const snapshotData = transition.sharedElements.presenting.concat(transition.sharedElements.independent);
|
|
426
|
-
snapshotData.sort((a, b) => (a.zIndex
|
|
556
|
+
snapshotData.sort((a, b) => (a.zIndex || 0) - (b.zIndex || 0));
|
|
427
557
|
if (SharedTransition.DEBUG) {
|
|
428
558
|
console.log(`zIndex settings:`, snapshotData.map((s) => {
|
|
429
559
|
return {
|
|
@@ -468,7 +598,7 @@ export class SharedTransitionHelper {
|
|
|
468
598
|
// animating over it back to its position. Use the full list (not just
|
|
469
599
|
// the deduped `presenting`) so duplicates from sister lists are hidden
|
|
470
600
|
// too. Restored in cleanupDismiss below.
|
|
471
|
-
for (const sourceView of
|
|
601
|
+
for (const sourceView of transition.sharedElements._allPresentingViews || []) {
|
|
472
602
|
if (sourceView?.ios) {
|
|
473
603
|
sourceView.ios.alpha = 0;
|
|
474
604
|
}
|
|
@@ -477,7 +607,7 @@ export class SharedTransitionHelper {
|
|
|
477
607
|
// Restore alpha on every source view we hid — including duplicate
|
|
478
608
|
// sources with the same sharedTransitionTag — using NS opacity as
|
|
479
609
|
// the source of truth.
|
|
480
|
-
for (const sourceView of
|
|
610
|
+
for (const sourceView of transition.sharedElements._allPresentingViews || []) {
|
|
481
611
|
if (sourceView?.ios) {
|
|
482
612
|
sourceView.ios.alpha = sourceView.opacity;
|
|
483
613
|
}
|
|
@@ -593,6 +723,13 @@ export class SharedTransitionHelper {
|
|
|
593
723
|
}
|
|
594
724
|
break;
|
|
595
725
|
}
|
|
726
|
+
// Apply the dismiss shadow here, not from the gesture handler. UIKit
|
|
727
|
+
// reparents the presented view into the transition containerView as
|
|
728
|
+
// part of starting the interactive transition; applying earlier would
|
|
729
|
+
// attach the shadow to the now-orphaned old superlayer.
|
|
730
|
+
if (state.interactive?.dismiss?.shadow && state.instance?.presented?.view) {
|
|
731
|
+
applyInteractiveDismissShadow(state.instance.presented.view, state.interactive.dismiss.shadow);
|
|
732
|
+
}
|
|
596
733
|
}
|
|
597
734
|
static interactiveUpdate(state, interactiveState, type, percent) {
|
|
598
735
|
if (interactiveState) {
|
|
@@ -629,11 +766,20 @@ export class SharedTransitionHelper {
|
|
|
629
766
|
p.snapshot.alpha = p.endOpacity;
|
|
630
767
|
interactiveState.transitionContext.containerView.addSubview(p.snapshot);
|
|
631
768
|
}
|
|
769
|
+
// Re-mount independent (orphan) snapshots so they're available to
|
|
770
|
+
// animate alongside the gesture. They were removed in present cleanup
|
|
771
|
+
// but the records (including their snapshots) live on transition.sharedElements.
|
|
772
|
+
// Source-only orphans were left at alpha 0 since present; their snapshots
|
|
773
|
+
// pick up where the present animation left off (endOpacity).
|
|
774
|
+
for (const ind of state.instance.sharedElements.independent) {
|
|
775
|
+
ind.snapshot.alpha = ind.endOpacity;
|
|
776
|
+
interactiveState.transitionContext.containerView.addSubview(ind.snapshot);
|
|
777
|
+
}
|
|
632
778
|
// Hide every source-side shared element (including duplicates that share
|
|
633
779
|
// a sharedTransitionTag in sister lists) so the user only sees the
|
|
634
780
|
// animating snapshot — not the snapshot AND the real source element
|
|
635
781
|
// simultaneously ("double image" during interactive dismissal).
|
|
636
|
-
for (const sourceView of
|
|
782
|
+
for (const sourceView of state.instance.sharedElements._allPresentingViews || []) {
|
|
637
783
|
if (sourceView?.ios) {
|
|
638
784
|
sourceView.ios.alpha = 0;
|
|
639
785
|
}
|
|
@@ -646,6 +792,18 @@ export class SharedTransitionHelper {
|
|
|
646
792
|
iOSUtils.copyLayerProperties(p.snapshot, p.view.ios, p.propertiesToMatch);
|
|
647
793
|
p.snapshot.alpha = 1;
|
|
648
794
|
}
|
|
795
|
+
// Animate orphan snapshots back toward their start state alongside
|
|
796
|
+
// the rest of the dismiss. For source-only orphans this is the
|
|
797
|
+
// fade-back-in that mirrors the fade-out of present.
|
|
798
|
+
for (const ind of state.instance.sharedElements.independent) {
|
|
799
|
+
ind.snapshot.alpha = ind.startOpacity;
|
|
800
|
+
if (ind.scale) {
|
|
801
|
+
ind.snapshot.transform = ind.startTransform;
|
|
802
|
+
}
|
|
803
|
+
else {
|
|
804
|
+
ind.snapshot.frame = ind.startFrame;
|
|
805
|
+
}
|
|
806
|
+
}
|
|
649
807
|
state.instance.presented.view.alpha = isNumber(state.pageReturn?.opacity) ? state.pageReturn?.opacity : 0;
|
|
650
808
|
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
809
|
// Drive cornerRadius via the same percent the user controls — round
|
|
@@ -676,7 +834,7 @@ export class SharedTransitionHelper {
|
|
|
676
834
|
view.transform = CGAffineTransformIdentity;
|
|
677
835
|
// Restore the source-side element alphas that the gesture hid on
|
|
678
836
|
// engagement so the source page is correct when shown again.
|
|
679
|
-
for (const v of
|
|
837
|
+
for (const v of state.instance.sharedElements?._allPresentingViews || []) {
|
|
680
838
|
if (v?.ios)
|
|
681
839
|
v.ios.alpha = v.opacity;
|
|
682
840
|
}
|
|
@@ -701,7 +859,7 @@ export class SharedTransitionHelper {
|
|
|
701
859
|
// they're visible from frame one of the snap-back. This
|
|
702
860
|
// also makes the source restoration robust to a new
|
|
703
861
|
// gesture interrupting our completion callback.
|
|
704
|
-
for (const v of
|
|
862
|
+
for (const v of state.instance.sharedElements?._allPresentingViews || []) {
|
|
705
863
|
if (v?.ios)
|
|
706
864
|
v.ios.alpha = v.opacity;
|
|
707
865
|
}
|
|
@@ -747,7 +905,7 @@ export class SharedTransitionHelper {
|
|
|
747
905
|
}
|
|
748
906
|
// Restore alpha on every source view we hid during interactiveUpdate
|
|
749
907
|
// (including duplicates from sister lists).
|
|
750
|
-
for (const sourceView of
|
|
908
|
+
for (const sourceView of state.instance.sharedElements._allPresentingViews || []) {
|
|
751
909
|
if (sourceView?.ios) {
|
|
752
910
|
sourceView.ios.alpha = sourceView.opacity;
|
|
753
911
|
}
|
|
@@ -755,6 +913,17 @@ export class SharedTransitionHelper {
|
|
|
755
913
|
for (const p of state.instance.sharedElements.presenting) {
|
|
756
914
|
p.snapshot.removeFromSuperview();
|
|
757
915
|
}
|
|
916
|
+
// Tear down orphan snapshots that interactiveUpdate re-mounted. The
|
|
917
|
+
// source-only orphans' view.alpha stays at 0 — we're returning to the
|
|
918
|
+
// presented (modal-on-top) state, so they should remain hidden until
|
|
919
|
+
// a real dismiss completes. Cancel/finish for the next dismiss owns
|
|
920
|
+
// the eventual restore.
|
|
921
|
+
for (const ind of state.instance.sharedElements.independent) {
|
|
922
|
+
ind.snapshot.removeFromSuperview();
|
|
923
|
+
// Reset snapshot alpha to its post-present end state so a subsequent
|
|
924
|
+
// interactiveUpdate picks up from the right place.
|
|
925
|
+
ind.snapshot.alpha = ind.endOpacity;
|
|
926
|
+
}
|
|
758
927
|
state.instance.presented.view.alpha = 1;
|
|
759
928
|
interactiveState.propertyAnimator = null;
|
|
760
929
|
interactiveState.added = false;
|
|
@@ -776,23 +945,54 @@ export class SharedTransitionHelper {
|
|
|
776
945
|
// Morph mode finish — animate the destination view to the matching
|
|
777
946
|
// source element's frame and fade it out, then complete.
|
|
778
947
|
const view = state.instance.presented?.view;
|
|
779
|
-
const
|
|
948
|
+
const destPresented = state.instance.sharedElements?.presented?.[0];
|
|
949
|
+
const destTag = destPresented?.view?.sharedTransitionTag;
|
|
780
950
|
const matchingSource = state.instance.sharedElements?.presenting?.find?.((p) => p.view.sharedTransitionTag === destTag);
|
|
781
951
|
const srcIos = matchingSource?.view?.ios;
|
|
952
|
+
const destSharedIos = destPresented?.view?.ios;
|
|
782
953
|
const containerView = interactiveState.transitionContext?.containerView;
|
|
783
954
|
let targetTransform = null;
|
|
784
955
|
if (view && srcIos && containerView) {
|
|
785
|
-
const
|
|
956
|
+
const sourceFrame = srcIos.convertRectToView(srcIos.bounds, containerView);
|
|
786
957
|
const bounds = view.bounds;
|
|
787
|
-
if (bounds.size.width > 0 && bounds.size.height > 0) {
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
958
|
+
if (bounds.size.width > 0 && bounds.size.height > 0 && sourceFrame.size.width > 0 && sourceFrame.size.height > 0) {
|
|
959
|
+
// Anchor the modal's transform on the *internal* matched
|
|
960
|
+
// element (e.g. the album art inside the modal) so that the
|
|
961
|
+
// modal and the matched element converge along the same
|
|
962
|
+
// trajectory to the source thumbnail. Without this, the modal
|
|
963
|
+
// scales around its own center while the matched-element
|
|
964
|
+
// snapshot flies separately to source — making the two visibly
|
|
965
|
+
// drift apart mid-spring.
|
|
966
|
+
//
|
|
967
|
+
// Pick a uniform scale that takes the modal's matched element
|
|
968
|
+
// to the source's size. For square album art (both sides) this
|
|
969
|
+
// is exact; for slightly off-square sources (e.g. 360×380) we
|
|
970
|
+
// match width and let the small remainder be hidden by the
|
|
971
|
+
// fade-out.
|
|
972
|
+
const destSharedFrameInModal = destSharedIos ? destSharedIos.convertRectToView(destSharedIos.bounds, view) : null;
|
|
973
|
+
const sx = sourceFrame.size.width / bounds.size.width;
|
|
974
|
+
const sy = sourceFrame.size.height / bounds.size.height;
|
|
975
|
+
const targetCx = sourceFrame.origin.x + sourceFrame.size.width / 2;
|
|
976
|
+
const targetCy = sourceFrame.origin.y + sourceFrame.size.height / 2;
|
|
977
|
+
const modalCx = bounds.size.width / 2;
|
|
978
|
+
const modalCy = bounds.size.height / 2;
|
|
979
|
+
let tx;
|
|
980
|
+
let ty;
|
|
981
|
+
if (destSharedFrameInModal && destSharedFrameInModal.size.width > 0 && destSharedFrameInModal.size.height > 0) {
|
|
982
|
+
// Place the internal album art's center on the source's
|
|
983
|
+
// center. After scaling around modal center, internal art's
|
|
984
|
+
// visual center = modalCenter + (internalArtCenter - modalCenter) * scale + translation.
|
|
985
|
+
const internalCx = destSharedFrameInModal.origin.x + destSharedFrameInModal.size.width / 2;
|
|
986
|
+
const internalCy = destSharedFrameInModal.origin.y + destSharedFrameInModal.size.height / 2;
|
|
987
|
+
tx = targetCx - modalCx - (internalCx - modalCx) * sx;
|
|
988
|
+
ty = targetCy - modalCy - (internalCy - modalCy) * sy;
|
|
989
|
+
}
|
|
990
|
+
else {
|
|
991
|
+
// Fallback: align modal center on source center.
|
|
992
|
+
tx = targetCx - modalCx;
|
|
993
|
+
ty = targetCy - modalCy;
|
|
994
|
+
}
|
|
995
|
+
targetTransform = CGAffineTransformConcat(CGAffineTransformMakeTranslation(tx, ty), CGAffineTransformMakeScale(sx, sy));
|
|
796
996
|
}
|
|
797
997
|
}
|
|
798
998
|
// Build a snapshot fly-back for each shared element so the album
|
|
@@ -855,6 +1055,10 @@ export class SharedTransitionHelper {
|
|
|
855
1055
|
if (entry.srcSharedView)
|
|
856
1056
|
snapshottedSources.add(entry.srcSharedView);
|
|
857
1057
|
}
|
|
1058
|
+
// Source-only orphan views (album thumbnails, badges, etc.) were
|
|
1059
|
+
// already restored to their opacity by the gesture handler at morph
|
|
1060
|
+
// engagement, so we don't need fade-in snapshots here — they're
|
|
1061
|
+
// already visible behind the morphing modal.
|
|
858
1062
|
let didFinalize = false;
|
|
859
1063
|
const finalize = () => {
|
|
860
1064
|
if (didFinalize)
|
|
@@ -862,7 +1066,7 @@ export class SharedTransitionHelper {
|
|
|
862
1066
|
didFinalize = true;
|
|
863
1067
|
// Detach the imageSourceChange listeners that were attached during
|
|
864
1068
|
// PRESENT — the destination View is about to be torn down.
|
|
865
|
-
for (const presented of
|
|
1069
|
+
for (const presented of state.instance.sharedElements?.presented || []) {
|
|
866
1070
|
if (presented.imageSourceChangeListener) {
|
|
867
1071
|
presented.view.off('imageSourceChange', presented.imageSourceChangeListener);
|
|
868
1072
|
presented.imageSourceChangeListener = null;
|
|
@@ -882,10 +1086,23 @@ export class SharedTransitionHelper {
|
|
|
882
1086
|
for (const { snap } of sharedSnapshots) {
|
|
883
1087
|
snap.removeFromSuperview();
|
|
884
1088
|
}
|
|
1089
|
+
// Independent (orphan) views were restored at gesture engagement,
|
|
1090
|
+
// so they're already visible. Just tear down any leftover present-
|
|
1091
|
+
// phase snapshots that weren't removed in present cleanup, and
|
|
1092
|
+
// ensure alpha is set from NS opacity for safety in case the
|
|
1093
|
+
// engagement code path was bypassed.
|
|
1094
|
+
for (const independent of state.instance?.sharedElements?.independent || []) {
|
|
1095
|
+
if (independent.view?.ios) {
|
|
1096
|
+
independent.view.ios.alpha = independent.view.opacity;
|
|
1097
|
+
}
|
|
1098
|
+
independent.snapshot?.removeFromSuperview();
|
|
1099
|
+
}
|
|
885
1100
|
if (view) {
|
|
886
1101
|
view.transform = CGAffineTransformIdentity;
|
|
887
1102
|
view.alpha = 1;
|
|
888
1103
|
}
|
|
1104
|
+
// Remove the sibling shadow view if interactive engagement added one.
|
|
1105
|
+
removeInteractiveDismissShadow(view);
|
|
889
1106
|
SharedTransition.finishState(state.instance.id);
|
|
890
1107
|
if (interactiveState.transitionContext) {
|
|
891
1108
|
interactiveState.transitionContext.finishInteractiveTransition();
|
|
@@ -898,21 +1115,32 @@ export class SharedTransitionHelper {
|
|
|
898
1115
|
});
|
|
899
1116
|
};
|
|
900
1117
|
if (view && targetTransform) {
|
|
1118
|
+
const shadowViewToAnimate = view.__sharedTransitionShadowView;
|
|
901
1119
|
iOSUtils.animateWithSpring({
|
|
902
1120
|
animations: () => {
|
|
903
1121
|
view.transform = targetTransform;
|
|
904
1122
|
view.alpha = 0;
|
|
1123
|
+
// Drive the sibling shadow view through the same spring so it
|
|
1124
|
+
// shrinks + translates with the modal as it morphs to the
|
|
1125
|
+
// source. Also fade it out so it doesn't linger behind the
|
|
1126
|
+
// finalizing snapshot.
|
|
1127
|
+
if (shadowViewToAnimate) {
|
|
1128
|
+
shadowViewToAnimate.transform = targetTransform;
|
|
1129
|
+
shadowViewToAnimate.alpha = 0;
|
|
1130
|
+
}
|
|
905
1131
|
// Restore source-side element alphas IN PARALLEL with the
|
|
906
1132
|
// destination's morph + fade — but skip sources that have
|
|
907
1133
|
// a flying snapshot. Those are held at alpha 0 and
|
|
908
1134
|
// revealed by finalize() right before the snapshot is
|
|
909
1135
|
// removed, so the user never sees both the snapshot AND
|
|
910
1136
|
// the source visible at the same time.
|
|
911
|
-
for (const v of
|
|
1137
|
+
for (const v of state.instance.sharedElements?._allPresentingViews || []) {
|
|
912
1138
|
if (v?.ios && !snapshottedSources.has(v.ios)) {
|
|
913
1139
|
v.ios.alpha = v.opacity;
|
|
914
1140
|
}
|
|
915
1141
|
}
|
|
1142
|
+
// Source-only orphan views were already restored at
|
|
1143
|
+
// engagement; nothing more to do for them here.
|
|
916
1144
|
// Fly each shared-element snapshot to its source frame.
|
|
917
1145
|
// Rides the same spring so the image lands precisely back
|
|
918
1146
|
// at the source position as the destination view fades.
|
|
@@ -929,7 +1157,7 @@ export class SharedTransitionHelper {
|
|
|
929
1157
|
}
|
|
930
1158
|
else {
|
|
931
1159
|
// No target transform; ensure sources are still restored.
|
|
932
|
-
for (const v of
|
|
1160
|
+
for (const v of state.instance.sharedElements?._allPresentingViews || []) {
|
|
933
1161
|
if (v?.ios)
|
|
934
1162
|
v.ios.alpha = v.opacity;
|
|
935
1163
|
}
|
|
@@ -944,7 +1172,7 @@ export class SharedTransitionHelper {
|
|
|
944
1172
|
setTimeout(() => {
|
|
945
1173
|
// Restore alpha on every source view we hid (including duplicates) so
|
|
946
1174
|
// the source page is visible if shown again.
|
|
947
|
-
for (const sourceView of
|
|
1175
|
+
for (const sourceView of state.instance.sharedElements._allPresentingViews || []) {
|
|
948
1176
|
if (sourceView?.ios) {
|
|
949
1177
|
sourceView.ios.alpha = sourceView.opacity;
|
|
950
1178
|
}
|
|
@@ -958,6 +1186,18 @@ export class SharedTransitionHelper {
|
|
|
958
1186
|
presented.imageSourceChangeListener = null;
|
|
959
1187
|
}
|
|
960
1188
|
}
|
|
1189
|
+
// Restore alpha on independent (orphan) source views and tear down
|
|
1190
|
+
// their snapshots. Without this, source-only orphans (any non-matched
|
|
1191
|
+
// sharedTransitionTag on the source page) stay at alpha 0 forever,
|
|
1192
|
+
// leaving the page visibly blank where they used to be. The non-morph
|
|
1193
|
+
// interactive path drives a propertyAnimator instead of running the
|
|
1194
|
+
// dismiss snapshot/animation rig, so the standard cleanup doesn't run.
|
|
1195
|
+
for (const ind of state.instance.sharedElements.independent) {
|
|
1196
|
+
if (ind.view?.ios) {
|
|
1197
|
+
ind.view.ios.alpha = ind.view.opacity;
|
|
1198
|
+
}
|
|
1199
|
+
ind.snapshot?.removeFromSuperview();
|
|
1200
|
+
}
|
|
961
1201
|
SharedTransition.finishState(state.instance.id);
|
|
962
1202
|
interactiveState.propertyAnimator = null;
|
|
963
1203
|
interactiveState.added = false;
|