diff --git a/apps/toolbox/src/pages/transitions.ts b/apps/toolbox/src/pages/transitions.ts
index acc20f9dc..096984c13 100644
--- a/apps/toolbox/src/pages/transitions.ts
+++ b/apps/toolbox/src/pages/transitions.ts
@@ -1,5 +1,4 @@
-import { Observable, EventData, Page, ShowModalOptions, SharedTransition, ModalTransition, TransitionType, Screen } from '@nativescript/core';
-// import { DetailTransition } from './transitions/detail';
+import { Observable, EventData, Page, ShowModalOptions, SharedTransition, ModalTransition, PageTransition, Screen } from '@nativescript/core';
let page: Page;
export function navigatingTo(args: EventData) {
@@ -12,13 +11,22 @@ export function navigatingTo(args: EventData) {
// }
export class TransitionsModel extends Observable {
open() {
- // const detailTransition = new DetailTransition(400, global.isIOS ? UIViewAnimationCurve.EaseInOut : null);
- // detailTransition.page = page;
page.frame.navigate({
moduleName: `pages/transitions/transitions-detail`,
- // transition: {
- // instance: detailTransition,
- // },
+ transition: {
+ instance: SharedTransition.configure({
+ page,
+ instance: new PageTransition(),
+ incomingViewStart: {
+ y: 200,
+ duration: 1000,
+ },
+ dismissViewEnd: {
+ y: 100,
+ duration: 500,
+ },
+ }),
+ },
});
}
diff --git a/apps/toolbox/src/pages/transitions/transitions-detail.xml b/apps/toolbox/src/pages/transitions/transitions-detail.xml
index 1edad65a7..df4c20d57 100644
--- a/apps/toolbox/src/pages/transitions/transitions-detail.xml
+++ b/apps/toolbox/src/pages/transitions/transitions-detail.xml
@@ -8,7 +8,7 @@
-
+
diff --git a/packages/core/global-types.d.ts b/packages/core/global-types.d.ts
index 3b68c7c77..983834cfa 100644
--- a/packages/core/global-types.d.ts
+++ b/packages/core/global-types.d.ts
@@ -213,7 +213,7 @@ interface RequireContext {
interface WeakRef {
/**
- * @deprecated Use deref instead with 8.4+
+ * @deprecated Use deref instead with 8.5+
*/
get(): T;
diff --git a/packages/core/ui/core/view/index.ios.ts b/packages/core/ui/core/view/index.ios.ts
index ece70a36b..7454b1c98 100644
--- a/packages/core/ui/core/view/index.ios.ts
+++ b/packages/core/ui/core/view/index.ios.ts
@@ -13,7 +13,7 @@ import { profile } from '../../../profiling';
import { accessibilityEnabledProperty, accessibilityHiddenProperty, accessibilityHintProperty, accessibilityIdentifierProperty, accessibilityLabelProperty, accessibilityLanguageProperty, accessibilityLiveRegionProperty, accessibilityMediaSessionProperty, accessibilityRoleProperty, accessibilityStateProperty, accessibilityValueProperty, accessibilityIgnoresInvertColorsProperty } from '../../../accessibility/accessibility-properties';
import { IOSPostAccessibilityNotificationType, isAccessibilityServiceEnabled, updateAccessibilityProperties, AccessibilityEventOptions, AccessibilityRole, AccessibilityState } from '../../../accessibility';
import { CoreTypes } from '../../../core-types';
-import { CustomTransitionModal } from '../../transition';
+import type { ModalTransition } from '../../transition/modal-transition';
export * from './view-common';
// helpers (these are okay re-exported here)
@@ -922,10 +922,10 @@ View.prototype._nativeBackgroundState = 'unset';
@NativeClass
class UIViewControllerTransitioningDelegateImpl extends NSObject implements UIViewControllerTransitioningDelegate {
- owner: WeakRef;
+ owner: WeakRef;
static ObjCProtocols = [UIViewControllerTransitioningDelegate];
- static initWithOwner(owner: WeakRef) {
+ static initWithOwner(owner: WeakRef) {
const delegate = UIViewControllerTransitioningDelegateImpl.new();
delegate.owner = owner;
return delegate;
diff --git a/packages/core/ui/frame/index.ios.ts b/packages/core/ui/frame/index.ios.ts
index 71e4d73e2..83af216cf 100644
--- a/packages/core/ui/frame/index.ios.ts
+++ b/packages/core/ui/frame/index.ios.ts
@@ -402,11 +402,23 @@ class UINavigationControllerAnimatedDelegate extends NSObject implements UINavig
Trace.write(`UINavigationControllerImpl.navigationControllerAnimationControllerForOperationFromViewControllerToViewController(${operation}, ${fromVC}, ${toVC}), transition: ${JSON.stringify(navigationTransition)}`, Trace.categories.NativeLifecycle);
}
+ // Shared element transitions
+ if (operation === UINavigationControllerOperation.Push && navigationTransition.instance?.iosPresentedController) {
+ return navigationTransition.instance?.iosPresentedController(toVC, fromVC, navigationController);
+ } else if (operation === UINavigationControllerOperation.Pop && navigationTransition.instance?.iosDismissedController) {
+ return navigationTransition.instance?.iosDismissedController(toVC);
+ }
+
const curve = _getNativeCurve(navigationTransition);
const animationController = _createIOSAnimatedTransitioning(navigationTransition, curve, operation, fromVC, toVC);
return animationController;
}
+
+ // TODO: wire this up for interactive gesture control of transition
+ // navigationControllerInteractionControllerForAnimationController(navigationController: UINavigationController, animationController: UIViewControllerAnimatedTransitioning): UIViewControllerInteractiveTransitioning {
+
+ // }
}
@NativeClass
diff --git a/packages/core/ui/index.ts b/packages/core/ui/index.ts
index 892e808c7..719dc71d5 100644
--- a/packages/core/ui/index.ts
+++ b/packages/core/ui/index.ts
@@ -76,7 +76,7 @@ export { TextView } from './text-view';
export { TimePicker } from './time-picker';
export { Transition } from './transition';
export { ModalTransition } from './transition/modal-transition';
+export { PageTransition } from './transition/page-transition';
export { SharedTransition, SharedTransitionAnimationType } from './transition/shared-transition';
-export type { TransitionType } from './transition';
export { WebView } from './web-view';
export type { LoadEventData, WebViewNavigationType } from './web-view';
diff --git a/packages/core/ui/transition/index.android.ts b/packages/core/ui/transition/index.android.ts
index cddfd1e0c..2a0575973 100644
--- a/packages/core/ui/transition/index.android.ts
+++ b/packages/core/ui/transition/index.android.ts
@@ -2,8 +2,6 @@
import lazy from '../../utils/lazy';
import type { Transition as TransitionType } from '.';
-export type { TransitionType } from '.';
-
const _defaultInterpolator = lazy(() => new android.view.animation.AccelerateDecelerateInterpolator());
let transitionId = 0;
@@ -45,3 +43,11 @@ export class Transition implements TransitionType {
return `Transition@${this.id}`;
}
}
+
+export function iosSnapshotView(view: any) {
+ return null;
+}
+
+export function iosMatchLayerProperties(view: any, toView: any) {}
+
+export function iosPrintRect(r: any) {}
diff --git a/packages/core/ui/transition/index.d.ts b/packages/core/ui/transition/index.d.ts
index 82eea2a0a..623de0b98 100644
--- a/packages/core/ui/transition/index.d.ts
+++ b/packages/core/ui/transition/index.d.ts
@@ -1,6 +1,11 @@
-export interface TransitionType {
+export declare class Transition {
id: number;
-
+ static AndroidTransitionType?: { enter?: string; exit?: string; popEnter?: string; popExit?: string };
+ constructor(duration?: number, nativeCurve?: any /* UIViewAnimationCurve | string | CubicBezierAnimationCurve | android.view.animation.Interpolator | android.view.animation.LinearInterpolator */);
+ getDuration(): number;
+ getCurve(): any;
+ animateIOSTransition(transitionContext: any /*UIViewControllerContextTransitioning */, fromViewCtrl: any /* UIViewController */, toViewCtrl: any /* UIViewController */, operation: any /* UINavigationControllerOperation */): void;
+ createAndroidAnimator(transitionType: string): any;
iosDismissedController?(dismissed: any /* UIViewController */): any /* UIViewControllerAnimatedTransitioning */;
iosPresentedController?(presented: any /* UIViewController */, presenting: any /* UIViewController */, source: any /* UIViewController */): any /* UIViewControllerAnimatedTransitioning */;
@@ -12,13 +17,9 @@
iosPresentedViewController?(presented: any /* UIViewController */, presenting: any /* UIViewController */, source: any /* UIViewController */): any /* UIPresentationController */;
}
-export declare class Transition implements TransitionType {
- id: number;
- static AndroidTransitionType?: { enter?: string; exit?: string; popEnter?: string; popExit?: string };
- constructor(duration?: number, nativeCurve?: any /* UIViewAnimationCurve | string | CubicBezierAnimationCurve | android.view.animation.Interpolator | android.view.animation.LinearInterpolator */);
- getDuration(): number;
- getCurve(): any;
- animateIOSTransition(transitionContext: any /*UIViewControllerContextTransitioning */, fromViewCtrl: any /* UIViewController */, toViewCtrl: any /* UIViewController */, operation: any /* UINavigationControllerOperation */): void;
- createAndroidAnimator(transitionType: string): any;
- toString(): string;
-}
+// TODO: move to native-helper.{ios|android}
+export declare function iosSnapshotView(view: any /* UIView */): any; /* UIImage */
+
+export declare function iosMatchLayerProperties(view: any /* UIView */, toView: any /* UIView */);
+
+export declare function iosPrintRect(r: any /* CGRect */): void;
diff --git a/packages/core/ui/transition/index.ios.ts b/packages/core/ui/transition/index.ios.ts
index f4fd97dce..6d04db109 100644
--- a/packages/core/ui/transition/index.ios.ts
+++ b/packages/core/ui/transition/index.ios.ts
@@ -1,6 +1,5 @@
-import type { TransitionType } from '.';
-
-export type { TransitionType } from '.';
+import type { Transition as TransitionType } from '.';
+import { Screen } from '../../platform';
let transitionId = 0;
export class Transition implements TransitionType {
@@ -36,3 +35,40 @@ export class Transition implements TransitionType {
return `Transition@${this.id}`;
}
}
+
+export function iosSnapshotView(view: UIView): UIImage {
+ if (view instanceof UIImageView) {
+ return view.image;
+ }
+ // console.log('snapshotView view.frame:', printRect(view.frame));
+ UIGraphicsBeginImageContextWithOptions(CGSizeMake(view.frame.size.width, view.frame.size.height), false, Screen.mainScreen.scale);
+ view.layer.renderInContext(UIGraphicsGetCurrentContext());
+ const image = UIGraphicsGetImageFromCurrentImageContext();
+ UIGraphicsEndImageContext();
+ return image;
+}
+
+// todo: figure out all the properties/layer properties that we want to transition automatically
+// note: some need to be done at various stages of the animation and are not in here. (e.g. alpha)
+export function iosMatchLayerProperties(view: UIView, toView: UIView) {
+ const viewPropertiesToMatch: Array = ['backgroundColor'];
+ const layerPropertiesToMatch: Array = ['cornerRadius', 'borderWidth', 'borderColor'];
+
+ viewPropertiesToMatch.forEach((property) => {
+ if (view[property] !== toView[property]) {
+ console.log('| -- matching view property:', property);
+ view[property as any] = toView[property];
+ }
+ });
+
+ layerPropertiesToMatch.forEach((property) => {
+ if (view.layer[property] !== toView.layer[property]) {
+ console.log('| -- matching layer property:', property);
+ view.layer[property as any] = toView.layer[property];
+ }
+ });
+}
+
+export function iosPrintRect(r: CGRect) {
+ return `CGRect(${r.origin.x} ${r.origin.y} ${r.size.width} ${r.size.height})`;
+}
diff --git a/packages/core/ui/transition/modal-transition.d.ts b/packages/core/ui/transition/modal-transition.d.ts
index b521ab1e0..c616ba66f 100644
--- a/packages/core/ui/transition/modal-transition.d.ts
+++ b/packages/core/ui/transition/modal-transition.d.ts
@@ -1,3 +1,2 @@
import { Transition } from '.';
-
export declare class ModalTransition extends Transition {}
diff --git a/packages/core/ui/transition/modal-transition.ios.ts b/packages/core/ui/transition/modal-transition.ios.ts
index 9decdef83..6f76bec17 100644
--- a/packages/core/ui/transition/modal-transition.ios.ts
+++ b/packages/core/ui/transition/modal-transition.ios.ts
@@ -1,17 +1,10 @@
import { querySelectorAll } from '../core/view-base';
import type { View } from '../core/view';
import { Screen } from '../../platform';
-import { Transition } from '.';
-import type { TransitionType } from '.';
-import { SharedTransition, SharedTransitionAnimationType } from './shared-transition';
+import { Transition, iosMatchLayerProperties, iosPrintRect, iosSnapshotView } from '.';
+import { SharedTransition, SharedTransitionAnimationType, DEFAULT_DURATION } from './shared-transition';
-const printRect = (r: CGRect) => `CGRect(${r.origin.x} ${r.origin.y} ${r.size.width} ${r.size.height})`;
-const printPoint = (r: CGPoint) => `CGPoint(${r.x} ${r.y})`;
-const printSize = (r: CGSize) => `CGPoint(${r.width} ${r.height})`;
-
-const DEFAULT_DURATION = 0.35;
-
-export class ModalTransition extends Transition implements TransitionType {
+export class ModalTransition extends Transition {
transitionController: ModalTransitionController;
presented: UIViewController;
presenting: UIViewController;
@@ -65,7 +58,7 @@ class ModalTransitionController extends NSObject implements UIViewControllerAnim
}
animateTransition(transitionContext: UIViewControllerContextTransitioning): void {
- // console.log('FullScreenAnimationController animateTransition:', animationType);
+ // console.log('ModalTransitionController animateTransition:', animationType);
const owner = this.owner.deref();
if (owner) {
// console.log('owner.id:', owner.id);
@@ -125,7 +118,7 @@ class ModalTransitionController extends NSObject implements UIViewControllerAnim
// in case the image is loaded async, we need to update the snapshot when it changes
// todo: remove listener on transition end
presentedView.on('imageSourceChange', () => {
- sharedElementSnapshot.image = snapshotView(presentedSharedElement);
+ sharedElementSnapshot.image = iosSnapshotView(presentedSharedElement);
sharedElementSnapshot.tintColor = presentedSharedElement.tintColor;
});
@@ -133,14 +126,14 @@ class ModalTransitionController extends NSObject implements UIViewControllerAnim
sharedElementSnapshot.contentMode = presentedSharedElement.contentMode;
}
- matchLayerProperties(sharedElementSnapshot, presentingSharedElement);
+ iosMatchLayerProperties(sharedElementSnapshot, presentingSharedElement);
sharedElementSnapshot.clipsToBounds = true;
// console.log('---> snapshot: ', sharedElementSnapshot);
const startFrame = presentingSharedElement.convertRectToView(presentingSharedElement.bounds, transitionContext.containerView);
const endFrame = presentedSharedElement.convertRectToView(presentedSharedElement.bounds, transitionContext.containerView);
sharedElementSnapshot.frame = startFrame;
- console.log('---> ', presentingView.sharedTransitionTag, ' frame:', printRect(sharedElementSnapshot.frame));
+ console.log('---> ', presentingView.sharedTransitionTag, ' frame:', iosPrintRect(sharedElementSnapshot.frame));
owner.sharedElements.presenting.push({
view: presentingView,
@@ -209,13 +202,13 @@ class ModalTransitionController extends NSObject implements UIViewControllerAnim
presentingMatch.snapshot.frame = correctedEndFrame;
// apply view and layer properties to the snapshot view to match the source/presented view
- matchLayerProperties(presentingMatch.snapshot, presented.view.ios);
+ iosMatchLayerProperties(presentingMatch.snapshot, presented.view.ios);
// create a snapshot of the presented view
- presentingMatch.snapshot.image = snapshotView(presented.view.ios);
+ presentingMatch.snapshot.image = iosSnapshotView(presented.view.ios);
// apply correct alpha
presentingMatch.snapshot.alpha = presentingMatch.endOpacity;
- console.log(`---> ${presentingMatch.view.sharedTransitionTag} animate to: `, printRect(correctedEndFrame));
+ console.log(`---> ${presentingMatch.view.sharedTransitionTag} animate to: `, iosPrintRect(correctedEndFrame));
}
console.log(' ');
};
@@ -284,11 +277,11 @@ class ModalTransitionController extends NSObject implements UIViewControllerAnim
const updateFrameDismiss = () => {
console.log('3. Dismissing shared elements:');
for (const presenting of owner.sharedElements.presenting) {
- matchLayerProperties(presenting.snapshot, presenting.view.ios);
+ iosMatchLayerProperties(presenting.snapshot, presenting.view.ios);
presenting.snapshot.frame = presenting.startFrame;
presenting.snapshot.alpha = presenting.startOpacity;
- console.log(`---> ${presenting.view.sharedTransitionTag} animate to: `, printRect(presenting.startFrame));
+ console.log(`---> ${presenting.view.sharedTransitionTag} animate to: `, iosPrintRect(presenting.startFrame));
}
console.log(' ');
};
@@ -320,36 +313,3 @@ class ModalTransitionController extends NSObject implements UIViewControllerAnim
}
}
}
-
-function snapshotView(view: UIView): UIImage {
- if (view instanceof UIImageView) {
- return view.image;
- }
- // console.log('snapshotView view.frame:', printRect(view.frame));
- UIGraphicsBeginImageContextWithOptions(CGSizeMake(view.frame.size.width, view.frame.size.height), false, Screen.mainScreen.scale);
- view.layer.renderInContext(UIGraphicsGetCurrentContext());
- const image = UIGraphicsGetImageFromCurrentImageContext();
- UIGraphicsEndImageContext();
- return image;
-}
-
-// todo: figure out all the properties/layer properties that we want to transition automatically
-// note: some need to be done at various stages of the animation and are not in here. (e.g. alpha)
-function matchLayerProperties(view: UIView, toView: UIView) {
- const viewPropertiesToMatch: Array = ['backgroundColor'];
- const layerPropertiesToMatch: Array = ['cornerRadius', 'borderWidth', 'borderColor'];
-
- viewPropertiesToMatch.forEach((property) => {
- if (view[property] !== toView[property]) {
- console.log('| -- matching view property:', property);
- view[property as any] = toView[property];
- }
- });
-
- layerPropertiesToMatch.forEach((property) => {
- if (view.layer[property] !== toView.layer[property]) {
- console.log('| -- matching layer property:', property);
- view.layer[property as any] = toView.layer[property];
- }
- });
-}
diff --git a/packages/core/ui/transition/page-transition.android.ts b/packages/core/ui/transition/page-transition.android.ts
new file mode 100644
index 000000000..3ee67d7c4
--- /dev/null
+++ b/packages/core/ui/transition/page-transition.android.ts
@@ -0,0 +1,124 @@
+import { Transition } from '.';
+import { Screen } from '../../platform';
+import lazy from '../../utils/lazy';
+
+const screenWidth = lazy(() => Screen.mainScreen.widthPixels);
+const screenHeight = lazy(() => Screen.mainScreen.heightPixels);
+
+export class PageTransition extends Transition {
+ private _direction: string;
+
+ constructor(direction: string, duration: number, curve: any) {
+ super(duration, curve);
+ this._direction = direction;
+ }
+
+ public createAndroidAnimator(transitionType: string): android.animation.Animator {
+ const translationValues = Array.create('float', 2);
+ switch (this._direction) {
+ case 'left':
+ switch (transitionType) {
+ case Transition.AndroidTransitionType.enter:
+ translationValues[0] = screenWidth();
+ translationValues[1] = 0;
+ break;
+ case Transition.AndroidTransitionType.exit:
+ translationValues[0] = 0;
+ translationValues[1] = -screenWidth();
+ break;
+ case Transition.AndroidTransitionType.popEnter:
+ translationValues[0] = -screenWidth();
+ translationValues[1] = 0;
+ break;
+ case Transition.AndroidTransitionType.popExit:
+ translationValues[0] = 0;
+ translationValues[1] = screenWidth();
+ break;
+ }
+ break;
+ case 'right':
+ switch (transitionType) {
+ case Transition.AndroidTransitionType.enter:
+ translationValues[0] = -screenWidth();
+ translationValues[1] = 0;
+ break;
+ case Transition.AndroidTransitionType.exit:
+ translationValues[0] = 0;
+ translationValues[1] = screenWidth();
+ break;
+ case Transition.AndroidTransitionType.popEnter:
+ translationValues[0] = screenWidth();
+ translationValues[1] = 0;
+ break;
+ case Transition.AndroidTransitionType.popExit:
+ translationValues[0] = 0;
+ translationValues[1] = -screenWidth();
+ break;
+ }
+ break;
+ case 'top':
+ switch (transitionType) {
+ case Transition.AndroidTransitionType.enter:
+ translationValues[0] = screenHeight();
+ translationValues[1] = 0;
+ break;
+ case Transition.AndroidTransitionType.exit:
+ translationValues[0] = 0;
+ translationValues[1] = -screenHeight();
+ break;
+ case Transition.AndroidTransitionType.popEnter:
+ translationValues[0] = -screenHeight();
+ translationValues[1] = 0;
+ break;
+ case Transition.AndroidTransitionType.popExit:
+ translationValues[0] = 0;
+ translationValues[1] = screenHeight();
+ break;
+ }
+ break;
+ case 'bottom':
+ switch (transitionType) {
+ case Transition.AndroidTransitionType.enter:
+ translationValues[0] = -screenHeight();
+ translationValues[1] = 0;
+ break;
+ case Transition.AndroidTransitionType.exit:
+ translationValues[0] = 0;
+ translationValues[1] = screenHeight();
+ break;
+ case Transition.AndroidTransitionType.popEnter:
+ translationValues[0] = screenHeight();
+ translationValues[1] = 0;
+ break;
+ case Transition.AndroidTransitionType.popExit:
+ translationValues[0] = 0;
+ translationValues[1] = -screenHeight();
+ break;
+ }
+ break;
+ }
+
+ let prop;
+
+ if (this._direction === 'left' || this._direction === 'right') {
+ prop = 'translationX';
+ } else {
+ prop = 'translationY';
+ }
+
+ const animator = android.animation.ObjectAnimator.ofFloat(null, prop, translationValues);
+ const duration = this.getDuration();
+ if (duration !== undefined) {
+ animator.setDuration(duration);
+ }
+ animator.setInterpolator(this.getCurve());
+
+ const animatorSet = new android.animation.AnimatorSet();
+ animatorSet.play(animator);
+ return animatorSet;
+ }
+
+ public toString(): string {
+ return `${super.toString()} ${this._direction}`;
+ }
+}
diff --git a/packages/core/ui/transition/page-transition.d.ts b/packages/core/ui/transition/page-transition.d.ts
new file mode 100644
index 000000000..dc19c5aec
--- /dev/null
+++ b/packages/core/ui/transition/page-transition.d.ts
@@ -0,0 +1,2 @@
+import { Transition } from '.';
+export declare class PageTransition extends Transition {}
diff --git a/packages/core/ui/transition/page-transition.ios.ts b/packages/core/ui/transition/page-transition.ios.ts
new file mode 100644
index 000000000..329475bdc
--- /dev/null
+++ b/packages/core/ui/transition/page-transition.ios.ts
@@ -0,0 +1,316 @@
+import { querySelectorAll } from '../core/view-base';
+import type { View } from '../core/view';
+import { Transition, iosMatchLayerProperties, iosSnapshotView, iosPrintRect } from '.';
+import { Screen } from '../../platform';
+import { SharedTransition, SharedTransitionAnimationType, DEFAULT_DURATION } from './shared-transition';
+
+export class PageTransition extends Transition {
+ transitionController: PageTransitionController;
+ presented: UIViewController;
+ presenting: UIViewController;
+ sharedElements: {
+ presented?: Array<{ view: View; startFrame?: CGRect; endFrame?: CGRect; snapshot?: UIImageView; startOpacity?: number; endOpacity?: number }>;
+ presenting?: Array<{ view: View; startFrame: CGRect; endFrame?: CGRect; snapshot?: UIImageView; startOpacity?: number; endOpacity?: number }>;
+ };
+
+ iosPresentedController(presented: UIViewController, presenting: UIViewController, source: UIViewController): UIViewControllerAnimatedTransitioning {
+ console.log('-- iosPresentedController --');
+ this.transitionController = PageTransitionController.initWithOwner(new WeakRef(this));
+ this.presented = presented;
+ return this.transitionController;
+ }
+
+ iosDismissedController(dismissed: UIViewController): UIViewControllerAnimatedTransitioning {
+ console.log('-- iosDismissedController --');
+ this.transitionController = PageTransitionController.initWithOwner(new WeakRef(this));
+ this.presented = dismissed;
+ return this.transitionController;
+ }
+
+ iosInteractionDismiss(animator: UIViewControllerAnimatedTransitioning): UIViewControllerInteractiveTransitioning {
+ console.log('-- iosInteractionDismiss --');
+ return null;
+ }
+
+ iosInteractionPresented(animator: UIViewControllerAnimatedTransitioning): UIViewControllerInteractiveTransitioning {
+ console.log('-- iosInteractionPresented --');
+ return null;
+ }
+
+ iosPresentedViewController(presented: UIViewController, presenting: UIViewController, source: UIViewController): UIPresentationController {
+ console.log('-- iosPresentedViewController --');
+ return null;
+ }
+}
+
+@NativeClass()
+class PageTransitionController extends NSObject implements UIViewControllerAnimatedTransitioning {
+ static ObjCProtocols = [UIViewControllerAnimatedTransitioning];
+ owner: WeakRef;
+
+ static initWithOwner(owner: WeakRef) {
+ const ctrl = PageTransitionController.new();
+ ctrl.owner = owner;
+ return ctrl;
+ }
+
+ transitionDuration(transitionContext: UIViewControllerContextTransitioning): number {
+ return DEFAULT_DURATION;
+ }
+
+ animateTransition(transitionContext: UIViewControllerContextTransitioning): void {
+ console.log('PageTransitionController animateTransition');
+ const owner = this.owner.deref();
+ if (owner) {
+ // console.log('owner.id:', owner.id);
+ const state = SharedTransition.getState(owner.id);
+ if (!state) {
+ return;
+ }
+ console.log('state.activeType:', state.activeType);
+ switch (state.activeType) {
+ case SharedTransitionAnimationType.present: {
+ console.log('-- Transition present --');
+
+ transitionContext.containerView.addSubview(owner.presented.view);
+ owner.presented.view.layoutIfNeeded();
+
+ // 1. Presented view: gather all sharedTransitionTag views
+ const presentedSharedElements = >querySelectorAll(state.toPage, 'sharedTransitionTag');
+ // console.log('presented sharedTransitionTag total:', presentedSharedElements.length);
+
+ // 2. Presenting view: gather all sharedTransitionTag views
+ const presentingSharedElements = >querySelectorAll(state.page, 'sharedTransitionTag');
+ // console.log('presenting sharedTransitionTag total:', presentingSharedElements.length);
+
+ // 3. only handle sharedTransitionTag on presenting which match presented
+ const presentedTags = presentedSharedElements.map((v) => v.sharedTransitionTag);
+ const presentingViewsToHandle = presentingSharedElements.filter((v) => presentedTags.includes(v.sharedTransitionTag));
+
+ console.log(' ');
+ console.log(
+ `1. Found sharedTransitionTags to animate:`,
+ presentingViewsToHandle.map((v) => v.sharedTransitionTag)
+ );
+
+ console.log(`2. Take snapshots of shared elements and position them based on presenting view:`);
+
+ for (const presentingView of presentingViewsToHandle) {
+ if (!owner.sharedElements) {
+ owner.sharedElements = {
+ presented: [],
+ presenting: [],
+ };
+ }
+ const presentingSharedElement = presentingView.ios;
+ // console.log('fromTarget instanceof UIImageView:', fromTarget instanceof UIImageView)
+
+ // TODO: discuss whether we should check if UIImage/UIImageView type to always snapshot images or if other view types could be duped/added vs. snapshotted
+ // Note: snapshot may be most efficient/simple
+ // console.log('---> ', presentingView.sharedTransitionTag, ': ', presentingSharedElement)
+
+ const presentedView = presentedSharedElements.find((v) => v.sharedTransitionTag === presentingView.sharedTransitionTag);
+ const presentedSharedElement = presentedView.ios;
+
+ const sharedElementSnapshot = UIImageView.alloc().init(); // WithImage(snapshotView(presentedSharedElement));
+
+ // treat images differently...
+ if (presentedSharedElement instanceof UIImageView) {
+ // in case the image is loaded async, we need to update the snapshot when it changes
+ // todo: remove listener on transition end
+ presentedView.on('imageSourceChange', () => {
+ sharedElementSnapshot.image = iosSnapshotView(presentedSharedElement);
+ sharedElementSnapshot.tintColor = presentedSharedElement.tintColor;
+ });
+
+ sharedElementSnapshot.tintColor = presentedSharedElement.tintColor;
+ sharedElementSnapshot.contentMode = presentedSharedElement.contentMode;
+ }
+
+ iosMatchLayerProperties(sharedElementSnapshot, presentingSharedElement);
+ sharedElementSnapshot.clipsToBounds = true;
+ // console.log('---> snapshot: ', sharedElementSnapshot);
+
+ const startFrame = presentingSharedElement.convertRectToView(presentingSharedElement.bounds, transitionContext.containerView);
+ const endFrame = presentedSharedElement.convertRectToView(presentedSharedElement.bounds, transitionContext.containerView);
+ sharedElementSnapshot.frame = startFrame;
+ console.log('---> ', presentingView.sharedTransitionTag, ' frame:', iosPrintRect(sharedElementSnapshot.frame));
+
+ owner.sharedElements.presenting.push({
+ view: presentingView,
+ startFrame,
+ endFrame,
+ snapshot: sharedElementSnapshot,
+ startOpacity: presentingView.opacity,
+ endOpacity: presentedView.opacity,
+ });
+ owner.sharedElements.presented.push({
+ view: presentedView,
+ startFrame: endFrame,
+ endFrame: startFrame,
+ startOpacity: presentedView.opacity,
+ endOpacity: presentingView.opacity,
+ });
+
+ // set initial opacity to match the source view opacity
+ sharedElementSnapshot.alpha = presentingView.opacity;
+ // hide both while animating within the transition context
+ presentingView.opacity = 0;
+ presentedView.opacity = 0;
+ // add snapshot to animate
+ transitionContext.containerView.addSubview(sharedElementSnapshot);
+ }
+
+ const cleanupPresent = () => {
+ for (const presented of owner.sharedElements.presented) {
+ presented.view.opacity = presented.startOpacity;
+ }
+
+ // TODO: Discuss with Igor whether this is necessary
+ // potential this could help smooth some shared element transitions
+ UIView.animateWithDurationAnimationsCompletion(
+ 0, // Igor: disabled for now, we'll talk about this and decide
+ () => {
+ for (const presenting of owner.sharedElements.presenting) {
+ presenting.snapshot.alpha = presenting.endOpacity;
+ }
+ },
+ () => {
+ for (const presenting of owner.sharedElements.presenting) {
+ presenting.snapshot.removeFromSuperview();
+ }
+ SharedTransition.updateState({
+ id: owner.id,
+ activeType: SharedTransitionAnimationType.dismiss,
+ });
+ transitionContext.completeTransition(true);
+ }
+ );
+ };
+ const updateFramePresent = () => {
+ // https://stackoverflow.com/a/27997678/1418981
+ // In order to have proper layout. Seems mostly needed when presenting.
+ // For instance during presentation, destination view doesn't account navigation bar height.
+ // Not sure if best to leave all the time?
+ // owner.presented.view.setNeedsLayout();
+ // owner.presented.view.layoutIfNeeded();
+ console.log('3. Animating shared elements:');
+ for (const presented of owner.sharedElements.presented) {
+ const presentingMatch = owner.sharedElements.presenting.find((v) => v.view.sharedTransitionTag === presented.view.sharedTransitionTag);
+ // Workaround wrong origin due ongoing layout process.
+ const updatedEndFrame = presented.view.ios.convertRectToView(presented.view.ios.bounds, transitionContext.containerView);
+ const correctedEndFrame = CGRectMake(updatedEndFrame.origin.x, updatedEndFrame.origin.y, presentingMatch.endFrame.size.width, presentingMatch.endFrame.size.height);
+ presentingMatch.snapshot.frame = correctedEndFrame;
+
+ // apply view and layer properties to the snapshot view to match the source/presented view
+ iosMatchLayerProperties(presentingMatch.snapshot, presented.view.ios);
+ // create a snapshot of the presented view
+ presentingMatch.snapshot.image = iosSnapshotView(presented.view.ios);
+ // apply correct alpha
+ presentingMatch.snapshot.alpha = presentingMatch.endOpacity;
+
+ console.log(`---> ${presentingMatch.view.sharedTransitionTag} animate to: `, iosPrintRect(correctedEndFrame));
+ }
+ console.log(' ');
+ };
+
+ // starting page properties
+ owner.presented.view.alpha = typeof state.incomingViewStart?.opacity === 'number' ? state.incomingViewStart?.opacity : 0;
+
+ const startX = typeof state.incomingViewStart?.x === 'number' ? state.incomingViewStart?.x : 0;
+ const startY = typeof state.incomingViewStart?.y === 'number' ? state.incomingViewStart?.y : Screen.mainScreen.heightDIPs;
+ const startWidth = typeof state.incomingViewStart?.width === 'number' ? state.incomingViewStart?.width : Screen.mainScreen.widthDIPs;
+ const startHeight = typeof state.incomingViewStart?.height === 'number' ? state.incomingViewStart?.height : Screen.mainScreen.heightDIPs;
+ owner.presented.view.frame = CGRectMake(startX, startY, startWidth, startHeight);
+
+ UIView.animateWithDurationDelayUsingSpringWithDampingInitialSpringVelocityOptionsAnimationsCompletion(
+ typeof state.incomingViewStart?.duration === 'number' ? state.incomingViewStart?.duration / 1000 : DEFAULT_DURATION,
+ 0,
+ 0.5,
+ 3,
+ UIViewAnimationOptions.CurveEaseInOut,
+ () => {
+ // animate page properties to the following:
+ owner.presented.view.alpha = typeof state.incomingViewEnd?.opacity === 'number' ? state.incomingViewEnd?.opacity : 1;
+
+ const endX = typeof state.incomingViewEnd?.x === 'number' ? state.incomingViewEnd?.x : 0;
+ const endY = typeof state.incomingViewEnd?.y === 'number' ? state.incomingViewEnd?.y : 0;
+ const endWidth = typeof state.incomingViewEnd?.width === 'number' ? state.incomingViewEnd?.width : Screen.mainScreen.widthDIPs;
+ const endHeight = typeof state.incomingViewEnd?.height === 'number' ? state.incomingViewEnd?.height : Screen.mainScreen.heightDIPs;
+ owner.presented.view.frame = CGRectMake(endX, endY, endWidth, endHeight);
+
+ updateFramePresent();
+ },
+ () => {
+ cleanupPresent();
+ }
+ );
+ break;
+ }
+ case SharedTransitionAnimationType.dismiss: {
+ console.log('-- Transition dismiss --');
+
+ // console.log('transitionContext.containerView.subviews.count:', transitionContext.containerView.subviews.count);
+
+ console.log(' ');
+ console.log(
+ `1. Dismiss sharedTransitionTags to animate:`,
+ owner.sharedElements.presented.map((p) => p.view.sharedTransitionTag)
+ );
+
+ console.log(`2. Add back previously stored sharedElements to dismiss:`);
+
+ for (const p of owner.sharedElements.presented) {
+ p.view.opacity = 0;
+ }
+ for (const p of owner.sharedElements.presenting) {
+ p.snapshot.alpha = p.endOpacity;
+ transitionContext.containerView.addSubview(p.snapshot);
+ }
+
+ const cleanupDismiss = () => {
+ for (const presenting of owner.sharedElements.presenting) {
+ presenting.view.opacity = presenting.startOpacity;
+ }
+ SharedTransition.finishState(owner.id);
+ transitionContext.completeTransition(true);
+ };
+ const updateFrameDismiss = () => {
+ console.log('3. Dismissing shared elements:');
+ for (const presenting of owner.sharedElements.presenting) {
+ iosMatchLayerProperties(presenting.snapshot, presenting.view.ios);
+ presenting.snapshot.frame = presenting.startFrame;
+ presenting.snapshot.alpha = presenting.startOpacity;
+
+ console.log(`---> ${presenting.view.sharedTransitionTag} animate to: `, iosPrintRect(presenting.startFrame));
+ }
+ console.log(' ');
+ };
+
+ UIView.animateWithDurationDelayUsingSpringWithDampingInitialSpringVelocityOptionsAnimationsCompletion(
+ typeof state.dismissViewEnd?.duration === 'number' ? state.dismissViewEnd?.duration / 1000 : DEFAULT_DURATION,
+ 0,
+ 0.5,
+ 3,
+ UIViewAnimationOptions.CurveEaseInOut,
+ () => {
+ owner.presented.view.alpha = typeof state.dismissViewEnd?.opacity === 'number' ? state.dismissViewEnd?.opacity : 0;
+
+ const endX = typeof state.dismissViewEnd?.x === 'number' ? state.dismissViewEnd?.x : 0;
+ const endY = typeof state.dismissViewEnd?.y === 'number' ? state.dismissViewEnd?.y : Screen.mainScreen.heightDIPs;
+ const endWidth = typeof state.dismissViewEnd?.width === 'number' ? state.dismissViewEnd?.width : Screen.mainScreen.widthDIPs;
+ const endHeight = typeof state.dismissViewEnd?.height === 'number' ? state.dismissViewEnd?.height : Screen.mainScreen.heightDIPs;
+ owner.presented.view.frame = CGRectMake(endX, endY, endWidth, endHeight);
+
+ updateFrameDismiss();
+ },
+ () => {
+ cleanupDismiss();
+ }
+ );
+ break;
+ }
+ }
+ }
+ }
+}
diff --git a/packages/core/ui/transition/shared-transition.ts b/packages/core/ui/transition/shared-transition.ts
index b90da7c85..75429296e 100644
--- a/packages/core/ui/transition/shared-transition.ts
+++ b/packages/core/ui/transition/shared-transition.ts
@@ -2,6 +2,7 @@ import type { Transition } from '.';
// import type { View } from '../../core/view';
import type { Page } from '../page';
+export const DEFAULT_DURATION = 0.35;
// always increment when adding new transitions to be able to track their state
export enum SharedTransitionAnimationType {
present,