@nativescript/core 9.1.0-alpha.1 → 9.1.0-alpha.11
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-common.d.ts +0 -1
- package/application/application-common.js +5 -40
- package/application/application-common.js.map +1 -1
- package/application/application.android.d.ts +17 -1
- package/application/application.android.js +6 -5
- package/application/application.android.js.map +1 -1
- package/application/application.d.ts +6 -0
- package/application/application.ios.d.ts +7 -3
- package/application/application.ios.js +112 -19
- package/application/application.ios.js.map +1 -1
- package/data/observable/index.js +2 -1
- package/data/observable/index.js.map +1 -1
- package/fetch/index.mjs +2 -2
- package/file-system/file-system-access.ios.js +18 -10
- package/file-system/file-system-access.ios.js.map +1 -1
- package/globals/index.js +4 -1
- package/globals/index.js.map +1 -1
- package/http/http-interfaces.d.ts +9 -10
- package/http/http-interfaces.js.map +1 -1
- package/http/http-request/http-request-common.d.ts +1 -0
- package/http/http-request/http-request-common.js +8 -0
- package/http/http-request/http-request-common.js.map +1 -1
- package/http/http-request/index.android.d.ts +1 -2
- package/http/http-request/index.android.js +54 -260
- package/http/http-request/index.android.js.map +1 -1
- package/http/http-request/index.d.ts +1 -1
- package/http/http-request/index.ios.d.ts +1 -2
- package/http/http-request/index.ios.js +35 -214
- package/http/http-request/index.ios.js.map +1 -1
- package/http/http-request-internal/http-request-internal-common.d.ts +2 -0
- package/http/http-request-internal/http-request-internal-common.js +12 -0
- package/http/http-request-internal/http-request-internal-common.js.map +1 -0
- package/http/http-request-internal/index.android.d.ts +10 -0
- package/http/http-request-internal/index.android.js +204 -0
- package/http/http-request-internal/index.android.js.map +1 -0
- package/http/http-request-internal/index.d.ts +28 -0
- package/http/http-request-internal/index.ios.d.ts +10 -0
- package/http/http-request-internal/index.ios.js +181 -0
- package/http/http-request-internal/index.ios.js.map +1 -0
- package/http/index.d.ts +1 -1
- package/http/index.js.map +1 -1
- package/image-source/index.android.js +2 -2
- package/image-source/index.android.js.map +1 -1
- package/image-source/index.ios.js +2 -2
- package/image-source/index.ios.js.map +1 -1
- package/package.json +3 -1
- package/platforms/android/widgets-release.aar +0 -0
- package/platforms/ios/src/NativeScriptEmbedder.h +1 -2
- package/platforms/ios/src/NativeScriptEmbedder.m +0 -2
- package/platforms/ios/src/NativeScriptMainWindow.swift +37 -23
- package/platforms/ios/src/NativeScriptUtils.h +0 -2
- package/platforms/ios/src/NativeScriptUtils.m +0 -2
- package/platforms/ios/src/UIView+NativeScript.h +0 -2
- package/platforms/ios/src/UIView+NativeScript.m +0 -2
- package/references.d.ts +0 -1
- package/ui/action-bar/action-bar-common.js +0 -1
- package/ui/action-bar/action-bar-common.js.map +1 -1
- package/ui/action-bar/index.ios.js +0 -1
- package/ui/action-bar/index.ios.js.map +1 -1
- package/ui/builder/index.js +9 -1
- package/ui/builder/index.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/bindable/index.js +2 -1
- package/ui/core/bindable/index.js.map +1 -1
- package/ui/core/view/index.android.d.ts +1 -0
- package/ui/core/view/index.android.js +39 -30
- package/ui/core/view/index.android.js.map +1 -1
- package/ui/core/view/index.d.ts +18 -2
- package/ui/core/view/index.ios.d.ts +2 -1
- package/ui/core/view/index.ios.js +22 -28
- package/ui/core/view/index.ios.js.map +1 -1
- package/ui/core/view/view-common.d.ts +14 -2
- package/ui/core/view/view-common.js +18 -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 +58 -11
- package/ui/core/view/view-helper/index.ios.js.map +1 -1
- package/ui/core/view/view-interfaces.d.ts +10 -0
- package/ui/core/view-base/index.d.ts +24 -9
- package/ui/core/view-base/index.js +54 -31
- 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 +46 -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/image/image-common.js +1 -1
- package/ui/image/image-common.js.map +1 -1
- package/ui/image/index.android.js +13 -1
- package/ui/image/index.android.js.map +1 -1
- package/ui/index.js +0 -1
- package/ui/index.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/liquid-glass/index.ios.d.ts +3 -1
- package/ui/layouts/liquid-glass/index.ios.js +51 -4
- package/ui/layouts/liquid-glass/index.ios.js.map +1 -1
- package/ui/layouts/liquid-glass-container/index.ios.d.ts +1 -0
- package/ui/layouts/liquid-glass-container/index.ios.js +50 -4
- package/ui/layouts/liquid-glass-container/index.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.android.d.ts +1 -0
- package/ui/page/index.android.js +4 -0
- package/ui/page/index.android.js.map +1 -1
- package/ui/page/index.d.ts +1 -1
- package/ui/page/index.ios.d.ts +1 -0
- package/ui/page/index.ios.js +31 -17
- package/ui/page/index.ios.js.map +1 -1
- package/ui/page/page-common.d.ts +5 -2
- package/ui/page/page-common.js.map +1 -1
- package/ui/repeater/index.d.ts +2 -1
- package/ui/repeater/index.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 +6 -0
- package/ui/scroll-view/scroll-view-common.js +8 -0
- package/ui/scroll-view/scroll-view-common.js.map +1 -1
- package/ui/segmented-bar/segmented-bar-common.d.ts +5 -1
- package/ui/segmented-bar/segmented-bar-common.js.map +1 -1
- package/ui/styling/background.ios.js +4 -0
- package/ui/styling/background.ios.js.map +1 -1
- package/ui/styling/css-selector.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 +41 -32
- package/ui/styling/style-properties.js.map +1 -1
- package/ui/styling/style-scope.js +1 -1
- package/ui/styling/style-scope.js.map +1 -1
- package/ui/switch/index.ios.js +2 -1
- package/ui/switch/index.ios.js.map +1 -1
- package/ui/tab-view/index.android.d.ts +3 -1
- package/ui/tab-view/index.android.js +56 -46
- package/ui/tab-view/index.android.js.map +1 -1
- package/ui/tab-view/index.ios.d.ts +10 -1
- package/ui/tab-view/index.ios.js +22 -4
- package/ui/tab-view/index.ios.js.map +1 -1
- package/ui/tab-view/tab-view-common.d.ts +6 -1
- package/ui/tab-view/tab-view-common.js +12 -3
- package/ui/tab-view/tab-view-common.js.map +1 -1
- package/ui/text-base/index.android.d.ts +4 -11
- package/ui/text-base/index.android.js +11 -25
- package/ui/text-base/index.android.js.map +1 -1
- package/ui/text-field/index.ios.d.ts +3 -9
- package/ui/text-field/index.ios.js +47 -31
- 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 +33 -5
- 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 +296 -33
- 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 +765 -89
- package/ui/transition/shared-transition-helper.ios.js.map +1 -1
- package/ui/transition/shared-transition.d.ts +66 -0
- package/ui/transition/shared-transition.js +57 -2
- package/ui/transition/shared-transition.js.map +1 -1
- package/utils/constants.ios.js.map +1 -1
- package/utils/native-helper.android.d.ts +66 -2
- package/utils/native-helper.android.js.map +1 -1
- package/utils/native-helper.ios.d.ts +107 -4
- package/utils/native-helper.ios.js +17 -6
- package/utils/native-helper.ios.js.map +1 -1
- package/utils/native-helper.types.d.ts +2 -0
- package/utils/platform-check.d.ts +1 -1
- package/utils/platform-check.js.map +1 -1
- package/application-settings/index.macos.d.ts +0 -1
- package/application-settings/index.macos.js +0 -2
- package/application-settings/index.macos.js.map +0 -1
- package/file-system/file-system-access.macos.d.ts +0 -1
- package/file-system/file-system-access.macos.js +0 -2
- package/file-system/file-system-access.macos.js.map +0 -1
- package/http/http-request/index.macos.d.ts +0 -3
- package/http/http-request/index.macos.js +0 -168
- package/http/http-request/index.macos.js.map +0 -1
- package/http/http-shared.d.ts +0 -3
- package/http/http-shared.js +0 -2
- package/http/http-shared.js.map +0 -1
- package/http/index.macos.d.ts +0 -8
- package/http/index.macos.js +0 -70
- package/http/index.macos.js.map +0 -1
- package/index.macos.d.ts +0 -9
- package/index.macos.js +0 -8
- package/index.macos.js.map +0 -1
- package/platform/device/index.macos.d.ts +0 -17
- package/platform/device/index.macos.js +0 -50
- package/platform/device/index.macos.js.map +0 -1
- package/platform/index.macos.d.ts +0 -14
- package/platform/index.macos.js +0 -15
- package/platform/index.macos.js.map +0 -1
- package/platform/screen/index.macos.d.ts +0 -14
- package/platform/screen/index.macos.js +0 -30
- package/platform/screen/index.macos.js.map +0 -1
- package/text/index.macos.d.ts +0 -1
- package/text/index.macos.js +0 -2
- package/text/index.macos.js.map +0 -1
- package/ui/dialogs/index.macos.d.ts +0 -43
- package/ui/dialogs/index.macos.js +0 -134
- package/ui/dialogs/index.macos.js.map +0 -1
- package/utils/constants.macos.d.ts +0 -2
- package/utils/constants.macos.js +0 -6
- package/utils/constants.macos.js.map +0 -1
- package/utils/index.macos.d.ts +0 -23
- package/utils/index.macos.js +0 -62
- package/utils/index.macos.js.map +0 -1
- package/utils/native-helper.macos.d.ts +0 -17
- package/utils/native-helper.macos.js +0 -61
- package/utils/native-helper.macos.js.map +0 -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;
|
|
@@ -34,6 +143,10 @@ export class SharedTransitionHelper {
|
|
|
34
143
|
independent: [],
|
|
35
144
|
};
|
|
36
145
|
}
|
|
146
|
+
// Track every matched source view so cleanup can restore alpha,
|
|
147
|
+
// including duplicates (same album surfacing in multiple lists)
|
|
148
|
+
// that we hide but don't create a snapshot for.
|
|
149
|
+
transition.sharedElements._allPresentingViews = sharedElements.slice();
|
|
37
150
|
if (SharedTransition.DEBUG) {
|
|
38
151
|
console.log(` ${type}: Present`);
|
|
39
152
|
console.log(`1. Found sharedTransitionTags to animate:`, sharedElementTags);
|
|
@@ -46,12 +159,22 @@ export class SharedTransitionHelper {
|
|
|
46
159
|
const pageEndTags = pageEnd?.sharedTransitionTags || {};
|
|
47
160
|
// console.log('pageEndIndependentTags:', pageEndIndependentTags);
|
|
48
161
|
const positionSharedTags = async () => {
|
|
162
|
+
const processedTags = new Set();
|
|
49
163
|
for (const presentingView of sharedElements) {
|
|
50
164
|
const presentingSharedElement = presentingView.ios;
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
165
|
+
const tag = presentingView.sharedTransitionTag;
|
|
166
|
+
if (processedTags.has(tag)) {
|
|
167
|
+
// The same item can appear in multiple lists on the source page
|
|
168
|
+
// (e.g., a featured album also surfacing in "Recently Played").
|
|
169
|
+
// Each instance has the same sharedTransitionTag. Only the first
|
|
170
|
+
// one becomes the snapshot origin; the rest must still be hidden
|
|
171
|
+
// so they don't show through behind the animating snapshot.
|
|
172
|
+
if (presentingSharedElement) {
|
|
173
|
+
presentingSharedElement.alpha = 0;
|
|
174
|
+
}
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
processedTags.add(tag);
|
|
55
178
|
const presentedView = presented.find((v) => v.sharedTransitionTag === presentingView.sharedTransitionTag);
|
|
56
179
|
const presentedSharedElement = presentedView.ios;
|
|
57
180
|
const pageEndProps = pageEndTags[presentingView.sharedTransitionTag];
|
|
@@ -60,15 +183,25 @@ export class SharedTransitionHelper {
|
|
|
60
183
|
await pageEndProps?.callback(presentedView, 'present');
|
|
61
184
|
}
|
|
62
185
|
// treat images differently...
|
|
186
|
+
let imageSourceChangeListener;
|
|
63
187
|
if (presentedSharedElement instanceof UIImageView) {
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
|
|
188
|
+
// In case the image is loaded async, keep the snapshot's image in sync with
|
|
189
|
+
// the destination view's image. We hold a ref so we can detach the listener
|
|
190
|
+
// on cleanup (the View would otherwise hold a strong ref via the observers
|
|
191
|
+
// map and the snapshot closure would leak).
|
|
192
|
+
imageSourceChangeListener = () => {
|
|
67
193
|
snapshot.image = iOSUtils.snapshotView(presentedSharedElement, Screen.mainScreen.scale);
|
|
68
194
|
snapshot.tintColor = presentedSharedElement.tintColor;
|
|
69
|
-
}
|
|
195
|
+
};
|
|
196
|
+
presentedView.on('imageSourceChange', imageSourceChangeListener);
|
|
70
197
|
snapshot.tintColor = presentedSharedElement.tintColor;
|
|
71
198
|
snapshot.contentMode = presentedSharedElement.contentMode;
|
|
199
|
+
// Seed the snapshot with the source's already-loaded image so the very
|
|
200
|
+
// first frame of the animation isn't blank if the destination image is
|
|
201
|
+
// still loading.
|
|
202
|
+
if (presentingSharedElement instanceof UIImageView) {
|
|
203
|
+
snapshot.image = presentingSharedElement.image;
|
|
204
|
+
}
|
|
72
205
|
}
|
|
73
206
|
iOSUtils.copyLayerProperties(snapshot, presentingSharedElement, pageEndProps?.propertiesToMatch);
|
|
74
207
|
snapshot.clipsToBounds = true;
|
|
@@ -96,86 +229,115 @@ export class SharedTransitionHelper {
|
|
|
96
229
|
startOpacity: presentedView.opacity,
|
|
97
230
|
endOpacity: presentingView.opacity,
|
|
98
231
|
propertiesToMatch: pageEndProps?.propertiesToMatch,
|
|
232
|
+
imageSourceChangeListener,
|
|
99
233
|
});
|
|
100
234
|
// set initial opacity to match the source view opacity
|
|
101
235
|
snapshot.alpha = presentingView.opacity;
|
|
102
|
-
//
|
|
103
|
-
|
|
104
|
-
|
|
236
|
+
// Hide both while animating in the transition context.
|
|
237
|
+
//
|
|
238
|
+
// We mutate native alpha directly rather than NS `opacity` so the NS-side
|
|
239
|
+
// style remains the user's intended value (typically 1). If we instead set
|
|
240
|
+
// `view.opacity = 0` and then a transition was interrupted before cleanup
|
|
241
|
+
// (or the page was reused), `startOpacity` here could capture 0 from a prior
|
|
242
|
+
// run and the cleanup below would leave the destination invisible.
|
|
243
|
+
presentingSharedElement.alpha = 0;
|
|
244
|
+
presentedSharedElement.alpha = 0;
|
|
105
245
|
}
|
|
106
246
|
};
|
|
107
247
|
const positionIndependentTags = async () => {
|
|
108
|
-
//
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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);
|
|
118
284
|
if (!independentView) {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
}
|
|
123
|
-
isPresented = true;
|
|
124
|
-
}
|
|
125
|
-
const independentSharedElement = independentView.ios;
|
|
126
|
-
if (pageEndProps?.callback) {
|
|
127
|
-
await pageEndProps?.callback(independentView, 'present');
|
|
128
|
-
}
|
|
129
|
-
// let snapshot: UIImageView;
|
|
130
|
-
// if (isPresented) {
|
|
131
|
-
// snapshot = UIImageView.alloc().init();
|
|
132
|
-
// } else {
|
|
133
|
-
const snapshot = UIImageView.alloc().initWithImage(iOSUtils.snapshotView(independentSharedElement, Screen.mainScreen.scale));
|
|
134
|
-
// }
|
|
135
|
-
if (independentSharedElement instanceof UIImageView) {
|
|
136
|
-
// in case the image is loaded async, we need to update the snapshot when it changes
|
|
137
|
-
// todo: remove listener on transition end
|
|
138
|
-
// if (isPresented) {
|
|
139
|
-
// independentView.on('imageSourceChange', () => {
|
|
140
|
-
// snapshot.image = iOSNativeHelper.snapshotView(independentSharedElement, Screen.mainScreen.scale);
|
|
141
|
-
// snapshot.tintColor = independentSharedElement.tintColor;
|
|
142
|
-
// });
|
|
143
|
-
// }
|
|
144
|
-
snapshot.tintColor = independentSharedElement.tintColor;
|
|
145
|
-
snapshot.contentMode = independentSharedElement.contentMode;
|
|
146
|
-
}
|
|
147
|
-
snapshot.clipsToBounds = true;
|
|
148
|
-
const startFrame = independentSharedElement.convertRectToView(independentSharedElement.bounds, transitionContext.containerView);
|
|
149
|
-
const startFrameRect = getRectFromProps(pageStartIndependentProps);
|
|
150
|
-
// adjust for any specified start positions
|
|
151
|
-
const startFrameAdjusted = CGRectMake(startFrame.origin.x + startFrameRect.x, startFrame.origin.y + startFrameRect.y, startFrame.size.width, startFrame.size.height);
|
|
152
|
-
// console.log('startFrameAdjusted:', tag, iOSNativeHelper.printCGRect(startFrameAdjusted));
|
|
153
|
-
// if (pageStartIndependentProps?.scale) {
|
|
154
|
-
// snapshot.transform = CGAffineTransformConcat(CGAffineTransformMakeTranslation(startFrameAdjusted.origin.x, startFrameAdjusted.origin.y), CGAffineTransformMakeScale(pageStartIndependentProps.scale.x, pageStartIndependentProps.scale.y))
|
|
155
|
-
// } else {
|
|
156
|
-
snapshot.frame = startFrame; //startFrameAdjusted;
|
|
157
|
-
// }
|
|
158
|
-
if (SharedTransition.DEBUG) {
|
|
159
|
-
console.log('---> ', independentView.sharedTransitionTag, ' frame:', iOSUtils.printCGRect(snapshot.frame));
|
|
285
|
+
// Tag declared in config but no matching view; skip (don't
|
|
286
|
+
// break — later tags might still resolve).
|
|
287
|
+
continue;
|
|
160
288
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
289
|
+
isPresented = true;
|
|
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));
|
|
178
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;
|
|
179
341
|
}
|
|
180
342
|
};
|
|
181
343
|
// position all sharedTransitionTag elements
|
|
@@ -183,7 +345,7 @@ export class SharedTransitionHelper {
|
|
|
183
345
|
await positionIndependentTags();
|
|
184
346
|
// combine to order by zIndex and add to transition context
|
|
185
347
|
const snapshotData = transition.sharedElements.presenting.concat(transition.sharedElements.independent);
|
|
186
|
-
snapshotData.sort((a, b) => (a.zIndex
|
|
348
|
+
snapshotData.sort((a, b) => (a.zIndex || 0) - (b.zIndex || 0));
|
|
187
349
|
if (SharedTransition.DEBUG) {
|
|
188
350
|
console.log(`zIndex settings:`, snapshotData.map((s) => {
|
|
189
351
|
return {
|
|
@@ -199,9 +361,41 @@ export class SharedTransitionHelper {
|
|
|
199
361
|
// Important: always set after above shared element positions have had their start positions set
|
|
200
362
|
transition.presented.view.alpha = isNumber(pageStart?.opacity) ? pageStart?.opacity : 0;
|
|
201
363
|
transition.presented.view.frame = CGRectMake(startFrame.x, startFrame.y, startFrame.width, startFrame.height);
|
|
364
|
+
// Optional top-corner rounding for sheet-style transitions (e.g.
|
|
365
|
+
// a modal that's rounded at the bottom of the screen and flattens
|
|
366
|
+
// as it slides up). Setting maskedCorners and masksToBounds here
|
|
367
|
+
// (before the animation) so they're stable during the animated
|
|
368
|
+
// cornerRadius change.
|
|
369
|
+
if (isNumber(pageStart?.cornerRadius) || isNumber(pageEnd?.cornerRadius)) {
|
|
370
|
+
const layer = transition.presented.view.layer;
|
|
371
|
+
// top-left | top-right | bottom-left | bottom-right (CACornerMask bits)
|
|
372
|
+
layer.maskedCorners = 15;
|
|
373
|
+
layer.masksToBounds = true;
|
|
374
|
+
if (isNumber(pageStart?.cornerRadius)) {
|
|
375
|
+
layer.cornerRadius = pageStart.cornerRadius;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
202
378
|
const cleanupPresent = () => {
|
|
203
379
|
for (const presented of transition.sharedElements.presented) {
|
|
204
|
-
|
|
380
|
+
// Detach the per-transition imageSourceChange listener registered
|
|
381
|
+
// during positionSharedTags so it doesn't leak (the snapshot is gone).
|
|
382
|
+
if (presented.imageSourceChangeListener) {
|
|
383
|
+
presented.view.off('imageSourceChange', presented.imageSourceChangeListener);
|
|
384
|
+
presented.imageSourceChangeListener = null;
|
|
385
|
+
}
|
|
386
|
+
// Restore the destination native alpha from the NS opacity (the source
|
|
387
|
+
// of truth). NS opacity wasn't modified during the transition, so this
|
|
388
|
+
// reflects the user's intended visibility (typically 1).
|
|
389
|
+
presented.view.ios.alpha = presented.view.opacity;
|
|
390
|
+
}
|
|
391
|
+
// Restore native alpha on every source view we touched — including
|
|
392
|
+
// duplicates that share a sharedTransitionTag and were hidden without
|
|
393
|
+
// a snapshot. Even though the source page is being removed, the View
|
|
394
|
+
// instances may be reused on subsequent navigations.
|
|
395
|
+
for (const sourceView of transition.sharedElements._allPresentingViews || []) {
|
|
396
|
+
if (sourceView?.ios) {
|
|
397
|
+
sourceView.ios.alpha = sourceView.opacity;
|
|
398
|
+
}
|
|
205
399
|
}
|
|
206
400
|
for (const presenting of transition.sharedElements.presenting) {
|
|
207
401
|
presenting.snapshot.removeFromSuperview();
|
|
@@ -209,7 +403,7 @@ export class SharedTransitionHelper {
|
|
|
209
403
|
for (const independent of transition.sharedElements.independent) {
|
|
210
404
|
independent.snapshot.removeFromSuperview();
|
|
211
405
|
if (independent.isPresented) {
|
|
212
|
-
independent.view.
|
|
406
|
+
independent.view.ios.alpha = independent.view.opacity;
|
|
213
407
|
}
|
|
214
408
|
}
|
|
215
409
|
SharedTransition.updateState(transition.id, {
|
|
@@ -232,6 +426,12 @@ export class SharedTransitionHelper {
|
|
|
232
426
|
transition.presented.view.alpha = isNumber(pageEnd?.opacity) ? pageEnd?.opacity : 1;
|
|
233
427
|
const endFrame = getRectFromProps(pageEnd);
|
|
234
428
|
transition.presented.view.frame = CGRectMake(endFrame.x, endFrame.y, endFrame.width, endFrame.height);
|
|
429
|
+
// Animate cornerRadius alongside the page frame. Implicit animation
|
|
430
|
+
// of cornerRadius is supported on iOS 11+, both inside
|
|
431
|
+
// UIView.animateWith… and UIViewPropertyAnimator's animations block.
|
|
432
|
+
if (isNumber(pageEnd?.cornerRadius)) {
|
|
433
|
+
transition.presented.view.layer.cornerRadius = pageEnd.cornerRadius;
|
|
434
|
+
}
|
|
235
435
|
if (pageOut) {
|
|
236
436
|
if (isNumber(pageOut.opacity)) {
|
|
237
437
|
transition.presenting.view.alpha = pageOut?.opacity;
|
|
@@ -310,6 +510,23 @@ export class SharedTransitionHelper {
|
|
|
310
510
|
});
|
|
311
511
|
if (type === 'page') {
|
|
312
512
|
transitionContext.containerView.insertSubviewBelowSubview(transition.presenting.view, transition.presented.view);
|
|
513
|
+
// UIKit's standard pop places the incoming source view off to the
|
|
514
|
+
// left so it can slide into position; for custom shared transitions
|
|
515
|
+
// (where the destination either slides down or morphs to source) we
|
|
516
|
+
// don't want that lateral slide. Pin the source to its fullscreen
|
|
517
|
+
// frame so it stays put underneath the animating destination.
|
|
518
|
+
// Users can still opt into a custom source animation via `pageOut`.
|
|
519
|
+
const sourceDefaults = getRectFromProps(null);
|
|
520
|
+
transition.presenting.view.frame = CGRectMake(0, 0, sourceDefaults.width, sourceDefaults.height);
|
|
521
|
+
}
|
|
522
|
+
// Morph + interactive dismiss: the gesture handler drives the
|
|
523
|
+
// animation via transforms and finalizes by calling cancel/finish
|
|
524
|
+
// on the transitionContext. Running the standard snapshot + spring
|
|
525
|
+
// rig here would race that and frequently call completeTransition(true)
|
|
526
|
+
// *before* the user's gesture ended — causing the dismiss to commit
|
|
527
|
+
// even on a cancel. Skip the rest of the dismiss setup entirely.
|
|
528
|
+
if (state.interactive?.dismiss?.morph && state.interactiveBegan) {
|
|
529
|
+
return;
|
|
313
530
|
}
|
|
314
531
|
// console.log('transitionContext.containerView.subviews.count:', transitionContext.containerView.subviews.count);
|
|
315
532
|
if (SharedTransition.DEBUG) {
|
|
@@ -322,11 +539,21 @@ export class SharedTransitionHelper {
|
|
|
322
539
|
const pageEndTags = pageEnd?.sharedTransitionTags || {};
|
|
323
540
|
const pageReturn = state.pageReturn;
|
|
324
541
|
for (const p of transition.sharedElements.presented) {
|
|
325
|
-
|
|
542
|
+
// Use native alpha (not NS opacity) so we don't pollute the NS-side
|
|
543
|
+
// value if the dismiss is interrupted; cleanup restores from NS opacity.
|
|
544
|
+
p.view.ios.alpha = 0;
|
|
545
|
+
}
|
|
546
|
+
// Ensure top-corner masking + clipping is in place before we animate
|
|
547
|
+
// cornerRadius back toward the start value. If the present pass already
|
|
548
|
+
// set these, this is a harmless no-op.
|
|
549
|
+
if (isNumber(pageReturn?.cornerRadius)) {
|
|
550
|
+
const layer = transition.presented.view.layer;
|
|
551
|
+
layer.maskedCorners = 15; // top-left | top-right | bottom-left | bottom-right
|
|
552
|
+
layer.masksToBounds = true;
|
|
326
553
|
}
|
|
327
554
|
// combine to order by zIndex and add to transition context
|
|
328
555
|
const snapshotData = transition.sharedElements.presenting.concat(transition.sharedElements.independent);
|
|
329
|
-
snapshotData.sort((a, b) => (a.zIndex
|
|
556
|
+
snapshotData.sort((a, b) => (a.zIndex || 0) - (b.zIndex || 0));
|
|
330
557
|
if (SharedTransition.DEBUG) {
|
|
331
558
|
console.log(`zIndex settings:`, snapshotData.map((s) => {
|
|
332
559
|
return {
|
|
@@ -365,13 +592,43 @@ export class SharedTransitionHelper {
|
|
|
365
592
|
// add snapshot to animate
|
|
366
593
|
transitionContext.containerView.addSubview(data.snapshot);
|
|
367
594
|
}
|
|
595
|
+
// Hide every source-side shared element now that snapshots have been
|
|
596
|
+
// captured. The animating snapshot covers them; without this, the user
|
|
597
|
+
// sees a "double image" — the real source element AND the snapshot
|
|
598
|
+
// animating over it back to its position. Use the full list (not just
|
|
599
|
+
// the deduped `presenting`) so duplicates from sister lists are hidden
|
|
600
|
+
// too. Restored in cleanupDismiss below.
|
|
601
|
+
for (const sourceView of transition.sharedElements._allPresentingViews || []) {
|
|
602
|
+
if (sourceView?.ios) {
|
|
603
|
+
sourceView.ios.alpha = 0;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
368
606
|
const cleanupDismiss = () => {
|
|
607
|
+
// Restore alpha on every source view we hid — including duplicate
|
|
608
|
+
// sources with the same sharedTransitionTag — using NS opacity as
|
|
609
|
+
// the source of truth.
|
|
610
|
+
for (const sourceView of transition.sharedElements._allPresentingViews || []) {
|
|
611
|
+
if (sourceView?.ios) {
|
|
612
|
+
sourceView.ios.alpha = sourceView.opacity;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
369
615
|
for (const presenting of transition.sharedElements.presenting) {
|
|
370
|
-
presenting.view.opacity = presenting.startOpacity;
|
|
371
616
|
presenting.snapshot.removeFromSuperview();
|
|
372
617
|
}
|
|
618
|
+
for (const presented of transition.sharedElements.presented) {
|
|
619
|
+
// Detach the imageSourceChange listener and restore alpha. Even though
|
|
620
|
+
// the destination page is being torn down, NS Views can outlive their
|
|
621
|
+
// nativeView and we shouldn't leak observers.
|
|
622
|
+
if (presented.imageSourceChangeListener) {
|
|
623
|
+
presented.view.off('imageSourceChange', presented.imageSourceChangeListener);
|
|
624
|
+
presented.imageSourceChangeListener = null;
|
|
625
|
+
}
|
|
626
|
+
if (presented.view.ios) {
|
|
627
|
+
presented.view.ios.alpha = presented.view.opacity;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
373
630
|
for (const independent of transition.sharedElements.independent) {
|
|
374
|
-
independent.view.
|
|
631
|
+
independent.view.ios.alpha = independent.view.opacity;
|
|
375
632
|
independent.snapshot.removeFromSuperview();
|
|
376
633
|
}
|
|
377
634
|
SharedTransition.finishState(transition.id);
|
|
@@ -390,6 +647,11 @@ export class SharedTransitionHelper {
|
|
|
390
647
|
transition.presented.view.alpha = isNumber(pageReturn?.opacity) ? pageReturn?.opacity : 0;
|
|
391
648
|
const endFrame = getRectFromProps(pageReturn, getPageStartDefaultsForType(type));
|
|
392
649
|
transition.presented.view.frame = CGRectMake(endFrame.x, endFrame.y, endFrame.width, endFrame.height);
|
|
650
|
+
// Animate cornerRadius back toward the start value (e.g. a sheet
|
|
651
|
+
// re-rounds as it slides offscreen).
|
|
652
|
+
if (isNumber(pageReturn?.cornerRadius)) {
|
|
653
|
+
transition.presented.view.layer.cornerRadius = pageReturn.cornerRadius;
|
|
654
|
+
}
|
|
393
655
|
if (pageOut) {
|
|
394
656
|
// always return to defaults if pageOut had been used
|
|
395
657
|
transition.presenting.view.alpha = 1;
|
|
@@ -450,20 +712,78 @@ export class SharedTransitionHelper {
|
|
|
450
712
|
switch (type) {
|
|
451
713
|
case 'page':
|
|
452
714
|
interactiveState.transitionContext.containerView.insertSubviewBelowSubview(state.instance.presenting.view, state.instance.presented.view);
|
|
715
|
+
// Pin the source view to its fullscreen frame so it doesn't slide
|
|
716
|
+
// in from off-left as the user drags. The default UIKit pop expects
|
|
717
|
+
// to animate the source from off-left; for shared-element / morph
|
|
718
|
+
// transitions we want the source to stay put underneath the
|
|
719
|
+
// animating destination so the visuals feel connected.
|
|
720
|
+
{
|
|
721
|
+
const sourceDefaults = getRectFromProps(null);
|
|
722
|
+
state.instance.presenting.view.frame = CGRectMake(0, 0, sourceDefaults.width, sourceDefaults.height);
|
|
723
|
+
}
|
|
453
724
|
break;
|
|
454
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
|
+
}
|
|
455
733
|
}
|
|
456
734
|
static interactiveUpdate(state, interactiveState, type, percent) {
|
|
457
735
|
if (interactiveState) {
|
|
736
|
+
if (state.interactive?.dismiss?.morph) {
|
|
737
|
+
// Morph mode: the gesture handler drives the destination view's
|
|
738
|
+
// transform directly. We only forward the event so observers can
|
|
739
|
+
// react to the gesture's percentage.
|
|
740
|
+
SharedTransition.notifyEvent(SharedTransition.interactiveUpdateEvent, {
|
|
741
|
+
id: state?.instance?.id,
|
|
742
|
+
type,
|
|
743
|
+
percent,
|
|
744
|
+
});
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
458
747
|
if (!interactiveState.added) {
|
|
748
|
+
// Defer setup until the gesture has clearly committed to a horizontal
|
|
749
|
+
// dismiss motion. A vertical scroll on a descendant scroll view also
|
|
750
|
+
// triggers the page-level pan gesture (NS pan recognizers don't yield
|
|
751
|
+
// to scroll-view pans by default), producing near-zero percent values.
|
|
752
|
+
// Without this guard we'd mount the dismiss snapshots and hide the
|
|
753
|
+
// destination during a scroll, making the image look "stuck" until the
|
|
754
|
+
// cancel animation runs.
|
|
755
|
+
if (Math.abs(percent) < 0.05) {
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
459
758
|
interactiveState.added = true;
|
|
460
759
|
for (const p of state.instance.sharedElements.presented) {
|
|
461
|
-
|
|
760
|
+
// Native alpha (not NS opacity) for the same reason as the dismiss path:
|
|
761
|
+
// if the user lets go without crossing the threshold, cancel uses NS
|
|
762
|
+
// opacity as source of truth to restore.
|
|
763
|
+
p.view.ios.alpha = 0;
|
|
462
764
|
}
|
|
463
765
|
for (const p of state.instance.sharedElements.presenting) {
|
|
464
766
|
p.snapshot.alpha = p.endOpacity;
|
|
465
767
|
interactiveState.transitionContext.containerView.addSubview(p.snapshot);
|
|
466
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
|
+
}
|
|
778
|
+
// Hide every source-side shared element (including duplicates that share
|
|
779
|
+
// a sharedTransitionTag in sister lists) so the user only sees the
|
|
780
|
+
// animating snapshot — not the snapshot AND the real source element
|
|
781
|
+
// simultaneously ("double image" during interactive dismissal).
|
|
782
|
+
for (const sourceView of state.instance.sharedElements._allPresentingViews || []) {
|
|
783
|
+
if (sourceView?.ios) {
|
|
784
|
+
sourceView.ios.alpha = 0;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
467
787
|
const pageStart = state.pageStart;
|
|
468
788
|
const startFrame = getRectFromProps(pageStart, getPageStartDefaultsForType(type));
|
|
469
789
|
interactiveState.propertyAnimator = UIViewPropertyAnimator.alloc().initWithDurationDampingRatioAnimations(1, 1, () => {
|
|
@@ -472,8 +792,25 @@ export class SharedTransitionHelper {
|
|
|
472
792
|
iOSUtils.copyLayerProperties(p.snapshot, p.view.ios, p.propertiesToMatch);
|
|
473
793
|
p.snapshot.alpha = 1;
|
|
474
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
|
+
}
|
|
475
807
|
state.instance.presented.view.alpha = isNumber(state.pageReturn?.opacity) ? state.pageReturn?.opacity : 0;
|
|
476
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);
|
|
809
|
+
// Drive cornerRadius via the same percent the user controls — round
|
|
810
|
+
// the page back up as they pan it offscreen.
|
|
811
|
+
if (isNumber(state.pageReturn?.cornerRadius)) {
|
|
812
|
+
state.instance.presented.view.layer.cornerRadius = state.pageReturn.cornerRadius;
|
|
813
|
+
}
|
|
477
814
|
});
|
|
478
815
|
}
|
|
479
816
|
interactiveState.propertyAnimator.fractionComplete = percent;
|
|
@@ -485,17 +822,108 @@ export class SharedTransitionHelper {
|
|
|
485
822
|
}
|
|
486
823
|
}
|
|
487
824
|
static interactiveCancel(state, interactiveState, type) {
|
|
825
|
+
if (state?.instance && interactiveState && state.interactive?.dismiss?.morph) {
|
|
826
|
+
// Morph mode cancel — spring the destination view back to identity.
|
|
827
|
+
const view = state.instance.presented?.view;
|
|
828
|
+
let didFinalize = false;
|
|
829
|
+
const finalize = () => {
|
|
830
|
+
if (didFinalize)
|
|
831
|
+
return;
|
|
832
|
+
didFinalize = true;
|
|
833
|
+
if (view)
|
|
834
|
+
view.transform = CGAffineTransformIdentity;
|
|
835
|
+
// Restore the source-side element alphas that the gesture hid on
|
|
836
|
+
// engagement so the source page is correct when shown again.
|
|
837
|
+
for (const v of state.instance.sharedElements?._allPresentingViews || []) {
|
|
838
|
+
if (v?.ios)
|
|
839
|
+
v.ios.alpha = v.opacity;
|
|
840
|
+
}
|
|
841
|
+
if (interactiveState.transitionContext) {
|
|
842
|
+
interactiveState.transitionContext.cancelInteractiveTransition();
|
|
843
|
+
interactiveState.transitionContext.completeTransition(false);
|
|
844
|
+
}
|
|
845
|
+
SharedTransition.updateState(state?.instance?.id, {
|
|
846
|
+
interactiveBegan: false,
|
|
847
|
+
interactiveCancelled: true,
|
|
848
|
+
});
|
|
849
|
+
SharedTransition.notifyEvent(SharedTransition.interactiveCancelledEvent, {
|
|
850
|
+
id: state?.instance?.id,
|
|
851
|
+
type,
|
|
852
|
+
});
|
|
853
|
+
};
|
|
854
|
+
if (view) {
|
|
855
|
+
iOSUtils.animateWithSpring({
|
|
856
|
+
animations: () => {
|
|
857
|
+
view.transform = CGAffineTransformIdentity;
|
|
858
|
+
// Cross-fade sources back IN PARALLEL with the spring so
|
|
859
|
+
// they're visible from frame one of the snap-back. This
|
|
860
|
+
// also makes the source restoration robust to a new
|
|
861
|
+
// gesture interrupting our completion callback.
|
|
862
|
+
for (const v of state.instance.sharedElements?._allPresentingViews || []) {
|
|
863
|
+
if (v?.ios)
|
|
864
|
+
v.ios.alpha = v.opacity;
|
|
865
|
+
}
|
|
866
|
+
},
|
|
867
|
+
completion: () => finalize(),
|
|
868
|
+
});
|
|
869
|
+
// Safety: ensure finalize runs even if the animation completion
|
|
870
|
+
// is dropped (e.g. interrupted by another gesture).
|
|
871
|
+
setTimeout(finalize, 800);
|
|
872
|
+
}
|
|
873
|
+
else {
|
|
874
|
+
finalize();
|
|
875
|
+
}
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
if (state?.instance && interactiveState && !interactiveState.added) {
|
|
879
|
+
// The gesture engaged the interactive transition but never crossed the
|
|
880
|
+
// motion threshold in interactiveUpdate (e.g., the user was scrolling
|
|
881
|
+
// vertically). Nothing was visually set up — just tell UIKit we're
|
|
882
|
+
// cancelling and clear the flags.
|
|
883
|
+
if (interactiveState.transitionContext) {
|
|
884
|
+
interactiveState.transitionContext.cancelInteractiveTransition();
|
|
885
|
+
interactiveState.transitionContext.completeTransition(false);
|
|
886
|
+
}
|
|
887
|
+
SharedTransition.updateState(state?.instance?.id, {
|
|
888
|
+
interactiveBegan: false,
|
|
889
|
+
interactiveCancelled: true,
|
|
890
|
+
});
|
|
891
|
+
SharedTransition.notifyEvent(SharedTransition.interactiveCancelledEvent, {
|
|
892
|
+
id: state?.instance?.id,
|
|
893
|
+
type,
|
|
894
|
+
});
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
488
897
|
if (state?.instance && interactiveState?.added && interactiveState?.propertyAnimator) {
|
|
489
898
|
interactiveState.propertyAnimator.reversed = true;
|
|
490
899
|
const duration = isNumber(state.pageEnd?.duration) ? state.pageEnd?.duration / 1000 : CORE_ANIMATION_DEFAULTS.duration;
|
|
491
900
|
interactiveState.propertyAnimator.continueAnimationWithTimingParametersDurationFactor(null, duration);
|
|
492
901
|
setTimeout(() => {
|
|
493
902
|
for (const p of state.instance.sharedElements.presented) {
|
|
494
|
-
|
|
903
|
+
// Restore native alpha from NS opacity (the user-intended value).
|
|
904
|
+
p.view.ios.alpha = p.view.opacity;
|
|
905
|
+
}
|
|
906
|
+
// Restore alpha on every source view we hid during interactiveUpdate
|
|
907
|
+
// (including duplicates from sister lists).
|
|
908
|
+
for (const sourceView of state.instance.sharedElements._allPresentingViews || []) {
|
|
909
|
+
if (sourceView?.ios) {
|
|
910
|
+
sourceView.ios.alpha = sourceView.opacity;
|
|
911
|
+
}
|
|
495
912
|
}
|
|
496
913
|
for (const p of state.instance.sharedElements.presenting) {
|
|
497
914
|
p.snapshot.removeFromSuperview();
|
|
498
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
|
+
}
|
|
499
927
|
state.instance.presented.view.alpha = 1;
|
|
500
928
|
interactiveState.propertyAnimator = null;
|
|
501
929
|
interactiveState.added = false;
|
|
@@ -513,15 +941,263 @@ export class SharedTransitionHelper {
|
|
|
513
941
|
}
|
|
514
942
|
}
|
|
515
943
|
static interactiveFinish(state, interactiveState, type) {
|
|
944
|
+
if (state?.instance && interactiveState && state.interactive?.dismiss?.morph) {
|
|
945
|
+
// Morph mode finish — animate the destination view to the matching
|
|
946
|
+
// source element's frame and fade it out, then complete.
|
|
947
|
+
const view = state.instance.presented?.view;
|
|
948
|
+
const destPresented = state.instance.sharedElements?.presented?.[0];
|
|
949
|
+
const destTag = destPresented?.view?.sharedTransitionTag;
|
|
950
|
+
const matchingSource = state.instance.sharedElements?.presenting?.find?.((p) => p.view.sharedTransitionTag === destTag);
|
|
951
|
+
const srcIos = matchingSource?.view?.ios;
|
|
952
|
+
const destSharedIos = destPresented?.view?.ios;
|
|
953
|
+
const containerView = interactiveState.transitionContext?.containerView;
|
|
954
|
+
let targetTransform = null;
|
|
955
|
+
if (view && srcIos && containerView) {
|
|
956
|
+
const sourceFrame = srcIos.convertRectToView(srcIos.bounds, containerView);
|
|
957
|
+
const bounds = view.bounds;
|
|
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));
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
// Build a snapshot fly-back for each shared element so the album
|
|
999
|
+
// art (etc.) does a real shared-element transition to its source
|
|
1000
|
+
// frame in parallel with the destination view's shrink+fade.
|
|
1001
|
+
// Without this, the image rides along with the morphing view and
|
|
1002
|
+
// lands at a visually offset position rather than precisely back
|
|
1003
|
+
// at the source thumbnail.
|
|
1004
|
+
const sharedSnapshots = [];
|
|
1005
|
+
if (containerView) {
|
|
1006
|
+
for (const presented of state.instance.sharedElements?.presented || []) {
|
|
1007
|
+
const destSharedView = presented.view?.ios;
|
|
1008
|
+
const tag = presented.view?.sharedTransitionTag;
|
|
1009
|
+
if (!destSharedView || !tag)
|
|
1010
|
+
continue;
|
|
1011
|
+
const sourceShared = state.instance.sharedElements?.presenting?.find?.((p) => p.view.sharedTransitionTag === tag);
|
|
1012
|
+
const srcSharedView = sourceShared?.view?.ios;
|
|
1013
|
+
if (!srcSharedView)
|
|
1014
|
+
continue;
|
|
1015
|
+
const startFrame = destSharedView.convertRectToView(destSharedView.bounds, containerView);
|
|
1016
|
+
const endFrame = srcSharedView.convertRectToView(srcSharedView.bounds, containerView);
|
|
1017
|
+
if (startFrame.size.width <= 0 || endFrame.size.width <= 0)
|
|
1018
|
+
continue;
|
|
1019
|
+
const snap = UIImageView.alloc().init();
|
|
1020
|
+
if (destSharedView instanceof UIImageView && destSharedView.image) {
|
|
1021
|
+
snap.image = destSharedView.image;
|
|
1022
|
+
snap.contentMode = destSharedView.contentMode;
|
|
1023
|
+
snap.tintColor = destSharedView.tintColor;
|
|
1024
|
+
}
|
|
1025
|
+
else {
|
|
1026
|
+
try {
|
|
1027
|
+
snap.image = iOSUtils.snapshotView(presented.view.ios, Screen.mainScreen.scale);
|
|
1028
|
+
}
|
|
1029
|
+
catch (e) {
|
|
1030
|
+
continue;
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
snap.clipsToBounds = true;
|
|
1034
|
+
snap.layer.cornerRadius = destSharedView.layer?.cornerRadius || 0;
|
|
1035
|
+
snap.frame = startFrame;
|
|
1036
|
+
containerView.addSubview(snap);
|
|
1037
|
+
// Hide the destination shared element so we don't see it
|
|
1038
|
+
// shrinking inside the morphing view at the same time the
|
|
1039
|
+
// snapshot is flying back to source — that would double up.
|
|
1040
|
+
destSharedView.alpha = 0;
|
|
1041
|
+
sharedSnapshots.push({
|
|
1042
|
+
snap,
|
|
1043
|
+
endFrame,
|
|
1044
|
+
endCornerRadius: srcSharedView.layer?.cornerRadius || 0,
|
|
1045
|
+
srcSharedView,
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
// Identify which source NS views have a matching fly-back snapshot.
|
|
1050
|
+
// Those sources stay at alpha 0 through the spring and are restored
|
|
1051
|
+
// inside finalize(), so the snapshot doesn't overlay an already-
|
|
1052
|
+
// visible source mid-flight (which looks like two images for a beat).
|
|
1053
|
+
const snapshottedSources = new Set();
|
|
1054
|
+
for (const entry of sharedSnapshots) {
|
|
1055
|
+
if (entry.srcSharedView)
|
|
1056
|
+
snapshottedSources.add(entry.srcSharedView);
|
|
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.
|
|
1062
|
+
let didFinalize = false;
|
|
1063
|
+
const finalize = () => {
|
|
1064
|
+
if (didFinalize)
|
|
1065
|
+
return;
|
|
1066
|
+
didFinalize = true;
|
|
1067
|
+
// Detach the imageSourceChange listeners that were attached during
|
|
1068
|
+
// PRESENT — the destination View is about to be torn down.
|
|
1069
|
+
for (const presented of state.instance.sharedElements?.presented || []) {
|
|
1070
|
+
if (presented.imageSourceChangeListener) {
|
|
1071
|
+
presented.view.off('imageSourceChange', presented.imageSourceChangeListener);
|
|
1072
|
+
presented.imageSourceChangeListener = null;
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
// Restore the snapshotted sources (held at alpha 0 through the
|
|
1076
|
+
// spring) immediately before removing the snapshot, so the
|
|
1077
|
+
// snapshot's last frame and the source's first visible frame
|
|
1078
|
+
// coincide exactly — no double-image, no gap.
|
|
1079
|
+
for (const entry of sharedSnapshots) {
|
|
1080
|
+
const srcSharedView = entry.srcSharedView;
|
|
1081
|
+
if (srcSharedView) {
|
|
1082
|
+
const nsView = state.instance.sharedElements?.presenting?.find?.((p) => p.view?.ios === srcSharedView)?.view;
|
|
1083
|
+
srcSharedView.alpha = nsView ? nsView.opacity : 1;
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
for (const { snap } of sharedSnapshots) {
|
|
1087
|
+
snap.removeFromSuperview();
|
|
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
|
+
}
|
|
1100
|
+
if (view) {
|
|
1101
|
+
view.transform = CGAffineTransformIdentity;
|
|
1102
|
+
view.alpha = 1;
|
|
1103
|
+
}
|
|
1104
|
+
// Remove the sibling shadow view if interactive engagement added one.
|
|
1105
|
+
removeInteractiveDismissShadow(view);
|
|
1106
|
+
SharedTransition.finishState(state.instance.id);
|
|
1107
|
+
if (interactiveState.transitionContext) {
|
|
1108
|
+
interactiveState.transitionContext.finishInteractiveTransition();
|
|
1109
|
+
interactiveState.transitionContext.completeTransition(true);
|
|
1110
|
+
}
|
|
1111
|
+
SharedTransition.notifyEvent(SharedTransition.finishedEvent, {
|
|
1112
|
+
id: state?.instance?.id,
|
|
1113
|
+
type,
|
|
1114
|
+
action: 'interactiveFinish',
|
|
1115
|
+
});
|
|
1116
|
+
};
|
|
1117
|
+
if (view && targetTransform) {
|
|
1118
|
+
const shadowViewToAnimate = view.__sharedTransitionShadowView;
|
|
1119
|
+
iOSUtils.animateWithSpring({
|
|
1120
|
+
animations: () => {
|
|
1121
|
+
view.transform = targetTransform;
|
|
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
|
+
}
|
|
1131
|
+
// Restore source-side element alphas IN PARALLEL with the
|
|
1132
|
+
// destination's morph + fade — but skip sources that have
|
|
1133
|
+
// a flying snapshot. Those are held at alpha 0 and
|
|
1134
|
+
// revealed by finalize() right before the snapshot is
|
|
1135
|
+
// removed, so the user never sees both the snapshot AND
|
|
1136
|
+
// the source visible at the same time.
|
|
1137
|
+
for (const v of state.instance.sharedElements?._allPresentingViews || []) {
|
|
1138
|
+
if (v?.ios && !snapshottedSources.has(v.ios)) {
|
|
1139
|
+
v.ios.alpha = v.opacity;
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
// Source-only orphan views were already restored at
|
|
1143
|
+
// engagement; nothing more to do for them here.
|
|
1144
|
+
// Fly each shared-element snapshot to its source frame.
|
|
1145
|
+
// Rides the same spring so the image lands precisely back
|
|
1146
|
+
// at the source position as the destination view fades.
|
|
1147
|
+
for (const { snap, endFrame, endCornerRadius } of sharedSnapshots) {
|
|
1148
|
+
snap.frame = endFrame;
|
|
1149
|
+
snap.layer.cornerRadius = endCornerRadius;
|
|
1150
|
+
}
|
|
1151
|
+
},
|
|
1152
|
+
completion: () => finalize(),
|
|
1153
|
+
});
|
|
1154
|
+
// Safety: ensure finalize runs even if the animation completion
|
|
1155
|
+
// is dropped (e.g. interrupted by another gesture).
|
|
1156
|
+
setTimeout(finalize, 800);
|
|
1157
|
+
}
|
|
1158
|
+
else {
|
|
1159
|
+
// No target transform; ensure sources are still restored.
|
|
1160
|
+
for (const v of state.instance.sharedElements?._allPresentingViews || []) {
|
|
1161
|
+
if (v?.ios)
|
|
1162
|
+
v.ios.alpha = v.opacity;
|
|
1163
|
+
}
|
|
1164
|
+
finalize();
|
|
1165
|
+
}
|
|
1166
|
+
return;
|
|
1167
|
+
}
|
|
516
1168
|
if (state?.instance && interactiveState?.added && interactiveState?.propertyAnimator) {
|
|
517
1169
|
interactiveState.propertyAnimator.reversed = false;
|
|
518
1170
|
const duration = isNumber(state.pageReturn?.duration) ? state.pageReturn?.duration / 1000 : CORE_ANIMATION_DEFAULTS.duration;
|
|
519
1171
|
interactiveState.propertyAnimator.continueAnimationWithTimingParametersDurationFactor(null, duration);
|
|
520
1172
|
setTimeout(() => {
|
|
1173
|
+
// Restore alpha on every source view we hid (including duplicates) so
|
|
1174
|
+
// the source page is visible if shown again.
|
|
1175
|
+
for (const sourceView of state.instance.sharedElements._allPresentingViews || []) {
|
|
1176
|
+
if (sourceView?.ios) {
|
|
1177
|
+
sourceView.ios.alpha = sourceView.opacity;
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
521
1180
|
for (const presenting of state.instance.sharedElements.presenting) {
|
|
522
|
-
presenting.view.opacity = presenting.startOpacity;
|
|
523
1181
|
presenting.snapshot.removeFromSuperview();
|
|
524
1182
|
}
|
|
1183
|
+
for (const presented of state.instance.sharedElements.presented) {
|
|
1184
|
+
if (presented.imageSourceChangeListener) {
|
|
1185
|
+
presented.view.off('imageSourceChange', presented.imageSourceChangeListener);
|
|
1186
|
+
presented.imageSourceChangeListener = null;
|
|
1187
|
+
}
|
|
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
|
+
}
|
|
525
1201
|
SharedTransition.finishState(state.instance.id);
|
|
526
1202
|
interactiveState.propertyAnimator = null;
|
|
527
1203
|
interactiveState.added = false;
|