diff --git a/apps/toolbox/src/app-root.xml b/apps/toolbox/src/app-root.xml
index 54e70d976..aec46ed6f 100644
--- a/apps/toolbox/src/app-root.xml
+++ b/apps/toolbox/src/app-root.xml
@@ -1,2 +1,2 @@
-
+
diff --git a/apps/toolbox/src/main-page.xml b/apps/toolbox/src/main-page.xml
index 45d5f51a3..0588cfa03 100644
--- a/apps/toolbox/src/main-page.xml
+++ b/apps/toolbox/src/main-page.xml
@@ -20,6 +20,7 @@
+
diff --git a/apps/toolbox/src/pages/transitions.ts b/apps/toolbox/src/pages/transitions.ts
new file mode 100644
index 000000000..acc20f9dc
--- /dev/null
+++ b/apps/toolbox/src/pages/transitions.ts
@@ -0,0 +1,47 @@
+import { Observable, EventData, Page, ShowModalOptions, SharedTransition, ModalTransition, TransitionType, Screen } from '@nativescript/core';
+// import { DetailTransition } from './transitions/detail';
+let page: Page;
+
+export function navigatingTo(args: EventData) {
+ page = args.object;
+ page.bindingContext = new TransitionsModel();
+}
+
+// class SampleCustomModalTransition extends ModalTransition implements TransitionType {
+
+// }
+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,
+ // },
+ });
+ }
+
+ openModal() {
+ page.showModal('pages/transitions/transitions-modal', {
+ context: {},
+ transition: {
+ instance: SharedTransition.configure({
+ page,
+ instance: new ModalTransition(),
+ incomingViewStart: {
+ y: 200,
+ duration: 1000,
+ },
+ dismissViewEnd: {
+ y: 100,
+ duration: 500,
+ },
+ }),
+ },
+ closeCallback(args) {
+ console.log('close modal callback', args);
+ },
+ } as ShowModalOptions);
+ }
+}
diff --git a/apps/toolbox/src/pages/transitions.xml b/apps/toolbox/src/pages/transitions.xml
new file mode 100644
index 000000000..3f81958b8
--- /dev/null
+++ b/apps/toolbox/src/pages/transitions.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/toolbox/src/pages/transitions/transitions-detail.ts b/apps/toolbox/src/pages/transitions/transitions-detail.ts
new file mode 100644
index 000000000..953e54cb4
--- /dev/null
+++ b/apps/toolbox/src/pages/transitions/transitions-detail.ts
@@ -0,0 +1,10 @@
+import { Observable, EventData, Page } from '@nativescript/core';
+
+let page: Page;
+
+export function navigatingTo(args: EventData) {
+ page = args.object;
+ page.bindingContext = new TransitionsModel();
+}
+
+export class TransitionsModel extends Observable {}
diff --git a/apps/toolbox/src/pages/transitions/transitions-detail.xml b/apps/toolbox/src/pages/transitions/transitions-detail.xml
new file mode 100644
index 000000000..843b00f05
--- /dev/null
+++ b/apps/toolbox/src/pages/transitions/transitions-detail.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/toolbox/src/pages/transitions/transitions-modal.ts b/apps/toolbox/src/pages/transitions/transitions-modal.ts
new file mode 100644
index 000000000..13b933043
--- /dev/null
+++ b/apps/toolbox/src/pages/transitions/transitions-modal.ts
@@ -0,0 +1,24 @@
+import { Observable, ShownModallyData, LoadEventData, Page, ShowModalOptions } from '@nativescript/core';
+
+let page: Page;
+let closeCallback: Function;
+export function onShownModally(args: ShownModallyData) {
+ console.log('page shown modally');
+
+ closeCallback = args.closeCallback;
+
+ if (args.context) {
+ args.context.shownModally = true;
+ }
+}
+
+export function onLoaded(args: LoadEventData) {
+ page = args.object as Page;
+ page.bindingContext = new TransitionModalPage();
+}
+
+export class TransitionModalPage extends Observable {
+ close() {
+ closeCallback();
+ }
+}
diff --git a/apps/toolbox/src/pages/transitions/transitions-modal.xml b/apps/toolbox/src/pages/transitions/transitions-modal.xml
new file mode 100644
index 000000000..25062599a
--- /dev/null
+++ b/apps/toolbox/src/pages/transitions/transitions-modal.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/core/accessibility/index.d.ts b/packages/core/accessibility/index.d.ts
index 491ceec20..cb7a9d825 100644
--- a/packages/core/accessibility/index.d.ts
+++ b/packages/core/accessibility/index.d.ts
@@ -1,5 +1,4 @@
import type { Page } from '../ui/page';
-import type { ViewBase } from '../ui/core/view-base';
import type { View } from '../ui/core/view';
import type { AndroidAccessibilityEvent } from './accessibility-types';
diff --git a/packages/core/accessibility/index.ios.ts b/packages/core/accessibility/index.ios.ts
index 230611b74..53f8d90aa 100644
--- a/packages/core/accessibility/index.ios.ts
+++ b/packages/core/accessibility/index.ios.ts
@@ -121,7 +121,7 @@ function ensureNativeClasses() {
});
}
-export function setupAccessibleView(view: Partial): void {
+export function setupAccessibleView(view: View): void {
const uiView = view.nativeViewProtected as UIView;
if (!uiView) {
return;
diff --git a/packages/core/ui/core/view-base/index.d.ts b/packages/core/ui/core/view-base/index.d.ts
index a92ad24b0..2276dfb93 100644
--- a/packages/core/ui/core/view-base/index.d.ts
+++ b/packages/core/ui/core/view-base/index.d.ts
@@ -8,7 +8,7 @@ import { Page } from '../../page';
import { Order, FlexGrow, FlexShrink, FlexWrapBefore, AlignSelf } from '../../layouts/flexbox-layout';
import { Length } from '../../styling/style-properties';
import { DOMNode } from '../../../debugger/dom-node';
-import type { CustomTransitionModal } from '../../transition';
+import type { ModalTransition } from '../../transition/modal-transition';
/**
* Iterates through all child views (via visual tree) and executes a function.
@@ -43,9 +43,17 @@ export function getViewById(view: ViewBase, id: string): ViewBase;
*/
export function getViewByDomId(view: ViewBase, domId: number): ViewBase;
-export interface ModalTransition {
+/**
+ * Gets a child view by selector.
+ * @param view - The parent (container) view of the view to look for.
+ * @param selector - The selector of the view to look for.
+ * Returns an instance of a view (if found), otherwise undefined.
+ */
+export function querySelectorAll(view: ViewBase, selector: string): Array;
+
+export interface ModalTransitionType {
name?: string;
- instance?: CustomTransitionModal;
+ instance?: ModalTransition;
duration?: number;
curve?: any;
}
@@ -298,6 +306,11 @@ export abstract class ViewBase extends Observable {
*/
public className: string;
+ /**
+ * Gets or sets the shared transition tag for animated view transitions
+ */
+ public sharedTransitionTag: string;
+
/**
* Gets owner page. This is a read-only property.
*/
diff --git a/packages/core/ui/core/view-base/index.ts b/packages/core/ui/core/view-base/index.ts
index f06b2f0a8..eaf5df936 100644
--- a/packages/core/ui/core/view-base/index.ts
+++ b/packages/core/ui/core/view-base/index.ts
@@ -10,7 +10,7 @@ import { Trace } from '../../../trace';
import { Observable, PropertyChangeData, WrappedValue } from '../../../data/observable';
import { Style } from '../../styling/style';
import { paddingTopProperty, paddingRightProperty, paddingBottomProperty, paddingLeftProperty } from '../../styling/style-properties';
-import type { CustomTransitionModal } from '../../transition';
+import type { ModalTransition } from '../../transition/modal-transition';
// TODO: Remove this import!
import { getClass } from '../../../utils/types';
@@ -38,9 +38,9 @@ function ensureStyleScopeModule() {
const defaultBindingSource = {};
-export interface ModalTransition {
+export interface ModalTransitionType {
name?: string;
- instance?: CustomTransitionModal;
+ instance?: ModalTransition;
duration?: number;
curve?: any;
}
@@ -74,7 +74,7 @@ export interface ShowModalOptions {
/**
* An optional custom transition effect
*/
- transition?: ModalTransition;
+ transition?: ModalTransitionType;
/**
* An optional parameter that specify options specific to iOS as an object.
@@ -181,6 +181,30 @@ export function getViewByDomId(view: ViewBaseDefinition, domId: number): ViewBas
return retVal;
}
+// TODO: allow all selector types (just using attributes now)
+export function querySelectorAll(view: ViewBaseDefinition, selector: string): Array {
+ if (!view) {
+ return undefined;
+ }
+
+ const retVal: Array = [];
+ if (view[selector]) {
+ retVal.push(view);
+ }
+
+ const descendantsCallback = function (child: ViewBaseDefinition): boolean {
+ if (child[selector]) {
+ retVal.push(child);
+ }
+
+ return true;
+ };
+
+ eachDescendant(view, descendantsCallback);
+
+ return retVal;
+}
+
export function eachDescendant(view: ViewBaseDefinition, callback: (child: ViewBaseDefinition) => boolean) {
if (!callback || !view) {
return;
@@ -283,6 +307,7 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
public id: string;
public className: string;
+ public sharedTransitionTag: string;
public _domId: number;
public _context: any;
@@ -453,7 +478,7 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
return true;
});
- setupAccessibleView(this);
+ setupAccessibleView(this);
this._emit('loaded');
}
diff --git a/packages/core/ui/core/view/index.ios.ts b/packages/core/ui/core/view/index.ios.ts
index 83a3cb36c..ece70a36b 100644
--- a/packages/core/ui/core/view/index.ios.ts
+++ b/packages/core/ui/core/view/index.ios.ts
@@ -932,41 +932,41 @@ class UIViewControllerTransitioningDelegateImpl extends NSObject implements UIVi
}
animationControllerForDismissedController?(dismissed: UIViewController): UIViewControllerAnimatedTransitioning {
- const owner = this.owner?.get();
- if (owner?.dismissedController) {
- return owner.dismissedController(dismissed);
+ const owner = this.owner?.deref();
+ if (owner?.iosDismissedController) {
+ return owner.iosDismissedController(dismissed);
}
return null;
}
animationControllerForPresentedControllerPresentingControllerSourceController?(presented: UIViewController, presenting: UIViewController, source: UIViewController): UIViewControllerAnimatedTransitioning {
- const owner = this.owner?.get();
- if (owner?.presentedController) {
- return owner.presentedController(presented, presenting, source);
+ const owner = this.owner?.deref();
+ if (owner?.iosPresentedController) {
+ return owner.iosPresentedController(presented, presenting, source);
}
return null;
}
interactionControllerForDismissal?(animator: UIViewControllerAnimatedTransitioning): UIViewControllerInteractiveTransitioning {
- const owner = this.owner?.get();
- if (owner?.interactionDismiss) {
- return owner.interactionDismiss(animator);
+ const owner = this.owner?.deref();
+ if (owner?.iosInteractionDismiss) {
+ return owner.iosInteractionDismiss(animator);
}
return null;
}
interactionControllerForPresentation?(animator: UIViewControllerAnimatedTransitioning): UIViewControllerInteractiveTransitioning {
- const owner = this.owner?.get();
- if (owner?.interactionPresented) {
- return owner.interactionPresented(animator);
+ const owner = this.owner?.deref();
+ if (owner?.iosInteractionPresented) {
+ return owner.iosInteractionPresented(animator);
}
return null;
}
presentationControllerForPresentedViewControllerPresentingViewControllerSourceViewController?(presented: UIViewController, presenting: UIViewController, source: UIViewController): UIPresentationController {
- const owner = this.owner?.get();
- if (owner?.presentedViewController) {
- return owner.presentedViewController(presented, presenting, source);
+ const owner = this.owner?.deref();
+ if (owner?.iosPresentedViewController) {
+ return owner.iosPresentedViewController(presented, presenting, source);
}
return null;
}
diff --git a/packages/core/ui/index.ts b/packages/core/ui/index.ts
index a2e4db2aa..892e808c7 100644
--- a/packages/core/ui/index.ts
+++ b/packages/core/ui/index.ts
@@ -11,7 +11,7 @@ export { ContentView } from './content-view';
export { Binding } from './core/bindable';
export type { BindingOptions } from './core/bindable';
export { ControlStateChangeListener } from './core/control-state-change';
-export { ViewBase, eachDescendant, getAncestor, getViewById, booleanConverter } from './core/view-base';
+export { ViewBase, eachDescendant, getAncestor, getViewById, booleanConverter, querySelectorAll } from './core/view-base';
export type { ShowModalOptions } from './core/view-base';
export { View, CSSType, ContainerView, ViewHelper, IOSHelper, isUserInteractionEnabledProperty, PseudoClassHandler, CustomLayoutView } from './core/view';
export type { Template, KeyedTemplate, ShownModallyData, AddArrayFromBuilder, AddChildFromBuilder, Size } from './core/view';
@@ -74,6 +74,9 @@ export { Span } from './text-base/span';
export { TextField } from './text-field';
export { TextView } from './text-view';
export { TimePicker } from './time-picker';
-export { Transition, CustomTransitionModal } from './transition';
+export { Transition } from './transition';
+export { ModalTransition } from './transition/modal-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/page/index.ios.ts b/packages/core/ui/page/index.ios.ts
index 7d5ad4925..f2c1faaaa 100644
--- a/packages/core/ui/page/index.ios.ts
+++ b/packages/core/ui/page/index.ios.ts
@@ -8,6 +8,7 @@ import { PageBase, actionBarHiddenProperty, statusBarStyleProperty } from './pag
import { profile } from '../../profiling';
import { iOSNativeHelper, layout } from '../../utils';
import { getLastFocusedViewOnPage, isAccessibilityServiceEnabled } from '../../accessibility';
+import { SharedTransition } from '../transition/shared-transition';
export * from './page-common';
@@ -430,6 +431,8 @@ export class Page extends PageBase {
// Make transitions look good
controller.view.backgroundColor = this._backgroundColor;
+ console.log('here for Page:', this.viewController);
+ SharedTransition.addPageToTop(this);
}
createNativeView() {
@@ -465,6 +468,7 @@ export class Page extends PageBase {
public onLoaded(): void {
super.onLoaded();
+ console.log('page loaded');
if (this.hasActionBar) {
this.actionBar.update();
}
diff --git a/packages/core/ui/page/page-common.d.ts b/packages/core/ui/page/page-common.d.ts
deleted file mode 100644
index 5c0ef919c..000000000
--- a/packages/core/ui/page/page-common.d.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-import { Page as PageDefinition } from '.';
-import { ContentView } from '../content-view';
-import { Frame } from '../frame';
-import { KeyframeAnimationInfo } from '../animation';
-import { NavigatedData } from '.';
-import { Color } from '../../color';
-import { ActionBar } from '../action-bar';
-import { View } from '../core/view';
-export declare class PageBase extends ContentView {
- static navigatingToEvent: string;
- static navigatedToEvent: string;
- static navigatingFromEvent: string;
- static navigatedFromEvent: string;
- _frame: Frame;
- actionBarHidden: boolean;
- enableSwipeBackNavigation: boolean;
- backgroundSpanUnderStatusBar: boolean;
- hasActionBar: boolean;
- readonly navigationContext: any;
-
- actionBar: ActionBar;
-
- statusBarStyle: 'light' | 'dark';
- androidStatusBarBackground: Color;
-
- readonly page: PageDefinition;
-
- _addChildFromBuilder(name: string, value: any): void;
-
- getKeyframeAnimationWithName(animationName: string): KeyframeAnimationInfo;
-
- readonly frame: Frame;
-
- createNavigatedData(eventName: string, isBackNavigation: boolean): NavigatedData;
-
- onNavigatingTo(context: any, isBackNavigation: boolean, bindingContext?: any): void;
-
- onNavigatedTo(isBackNavigation: boolean): void;
-
- onNavigatingFrom(isBackNavigation: boolean): void;
-
- onNavigatedFrom(isBackNavigation: boolean): void;
-
- eachChildView(callback: (child: View) => boolean): void;
-}
diff --git a/packages/core/ui/transition/index.android.ts b/packages/core/ui/transition/index.android.ts
index 46b7db963..cddfd1e0c 100644
--- a/packages/core/ui/transition/index.android.ts
+++ b/packages/core/ui/transition/index.android.ts
@@ -1,25 +1,28 @@
-// Types.
-import { _resolveAnimationCurve } from '../animation';
+import { _resolveAnimationCurve } from '../animation';
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;
-export class Transition {
+export class Transition implements TransitionType {
static AndroidTransitionType = {
enter: 'enter',
exit: 'exit',
popEnter: 'popEnter',
popExit: 'popExit',
};
+ id: number;
private _duration: number;
private _interpolator: android.view.animation.Interpolator;
- private _id: number;
- constructor(duration: number, curve?: any) {
+ constructor(duration: number = 350, curve?: any) {
this._duration = duration;
this._interpolator = curve ? _resolveAnimationCurve(curve) : _defaultInterpolator();
- this._id = transitionId++;
+ transitionId++;
+ this.id = transitionId;
}
public getDuration(): number {
@@ -39,8 +42,6 @@ export class Transition {
}
public toString(): string {
- return `Transition@${this._id}`;
+ return `Transition@${this.id}`;
}
}
-
-export class CustomTransitionModal extends Transition {}
diff --git a/packages/core/ui/transition/index.d.ts b/packages/core/ui/transition/index.d.ts
index bb4db6117..82eea2a0a 100644
--- a/packages/core/ui/transition/index.d.ts
+++ b/packages/core/ui/transition/index.d.ts
@@ -1,21 +1,24 @@
-export declare class Transition {
- 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 */);
- public getDuration(): number;
- public getCurve(): any;
- public animateIOSTransition(transitionContext: any /*UIViewControllerContextTransitioning */, fromViewCtrl: any /* UIViewController */, toViewCtrl: any /* UIViewController */, operation: any /* UINavigationControllerOperation */): void;
- public createAndroidAnimator(transitionType: string): any;
- public toString(): string;
+export interface TransitionType {
+ id: number;
+
+ iosDismissedController?(dismissed: any /* UIViewController */): any /* UIViewControllerAnimatedTransitioning */;
+
+ iosPresentedController?(presented: any /* UIViewController */, presenting: any /* UIViewController */, source: any /* UIViewController */): any /* UIViewControllerAnimatedTransitioning */;
+
+ iosInteractionDismiss?(animator: any /* UIViewControllerAnimatedTransitioning */): any /* UIViewControllerInteractiveTransitioning */;
+
+ iosInteractionPresented?(animator: any /* UIViewControllerAnimatedTransitioning */): any /* UIViewControllerInteractiveTransitioning */;
+
+ iosPresentedViewController?(presented: any /* UIViewController */, presenting: any /* UIViewController */, source: any /* UIViewController */): any /* UIPresentationController */;
}
-export declare class CustomTransitionModal extends Transition {
- dismissedController?(dismissed: any /* UIViewController */): any /* UIViewControllerAnimatedTransitioning */;
-
- presentedController?(presented: UIViewController, presenting: UIViewController, source: UIViewController): UIViewControllerAnimatedTransitioning;
-
- interactionDismiss?(animator: UIViewControllerAnimatedTransitioning): UIViewControllerInteractiveTransitioning;
-
- interactionPresented?(animator: UIViewControllerAnimatedTransitioning): UIViewControllerInteractiveTransitioning;
-
- presentedViewController?(presented: UIViewController, presenting: UIViewController, source: UIViewController): 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;
}
diff --git a/packages/core/ui/transition/index.ios.ts b/packages/core/ui/transition/index.ios.ts
index 6215f925f..f4fd97dce 100644
--- a/packages/core/ui/transition/index.ios.ts
+++ b/packages/core/ui/transition/index.ios.ts
@@ -1,14 +1,19 @@
-let transitionId = 0;
-export class Transition {
+import type { TransitionType } from '.';
+
+export type { TransitionType } from '.';
+
+let transitionId = 0;
+export class Transition implements TransitionType {
static AndroidTransitionType = {};
+ id: number;
private _duration: number;
private _curve: UIViewAnimationCurve;
- private _id: number;
- constructor(duration: number, curve: UIViewAnimationCurve = UIViewAnimationCurve.EaseInOut) {
+ constructor(duration: number = 350, nativeCurve: UIViewAnimationCurve = UIViewAnimationCurve.EaseInOut) {
this._duration = duration ? duration / 1000 : 0.35;
- this._curve = curve;
- this._id = transitionId++;
+ this._curve = nativeCurve;
+ transitionId++;
+ this.id = transitionId;
}
public getDuration(): number {
@@ -28,29 +33,6 @@ export class Transition {
}
public toString(): string {
- return `Transition@${this._id}`;
- }
-}
-
-export class CustomTransitionModal extends Transition {
- // Extend this class to create your own
- dismissedController?(dismissed: UIViewController): UIViewControllerAnimatedTransitioning {
- return null;
- }
-
- presentedController?(presented: UIViewController, presenting: UIViewController, source: UIViewController): UIViewControllerAnimatedTransitioning {
- return null;
- }
-
- interactionDismiss?(animator: UIViewControllerAnimatedTransitioning): UIViewControllerInteractiveTransitioning {
- return null;
- }
-
- interactionPresented?(animator: UIViewControllerAnimatedTransitioning): UIViewControllerInteractiveTransitioning {
- return null;
- }
-
- presentedViewController?(presented: UIViewController, presenting: UIViewController, source: UIViewController): UIPresentationController {
- return null;
+ return `Transition@${this.id}`;
}
}
diff --git a/packages/core/ui/transition/modal-transition.android.ts b/packages/core/ui/transition/modal-transition.android.ts
new file mode 100644
index 000000000..daf502524
--- /dev/null
+++ b/packages/core/ui/transition/modal-transition.android.ts
@@ -0,0 +1,3 @@
+import { Transition } from '.';
+
+export class ModalTransition extends Transition {}
diff --git a/packages/core/ui/transition/modal-transition.d.ts b/packages/core/ui/transition/modal-transition.d.ts
new file mode 100644
index 000000000..b521ab1e0
--- /dev/null
+++ b/packages/core/ui/transition/modal-transition.d.ts
@@ -0,0 +1,3 @@
+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
new file mode 100644
index 000000000..0a38a7713
--- /dev/null
+++ b/packages/core/ui/transition/modal-transition.ios.ts
@@ -0,0 +1,292 @@
+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';
+
+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})`;
+
+export class ModalTransition extends Transition implements TransitionType {
+ transitionController: ModalTransitionController;
+ presented: UIViewController;
+ presenting: UIViewController;
+ sharedElements: { presented?: Array<{ view: View; startFrame?: CGRect; endFrame?: CGRect; snapshot?: UIImageView }>; presenting?: Array<{ view: View; startFrame: CGRect; endFrame?: CGRect; snapshot?: UIImageView }> };
+
+ iosPresentedController(presented: UIViewController, presenting: UIViewController, source: UIViewController): UIViewControllerAnimatedTransitioning {
+ this.transitionController = ModalTransitionController.initWithOwner(new WeakRef(this));
+ this.presented = presented;
+ // console.log('presenting:', presenting)
+ return this.transitionController;
+ }
+
+ iosDismissedController(dismissed: UIViewController): UIViewControllerAnimatedTransitioning {
+ this.transitionController = ModalTransitionController.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 ModalTransitionController extends NSObject implements UIViewControllerAnimatedTransitioning {
+ static ObjCProtocols = [UIViewControllerAnimatedTransitioning];
+ owner: WeakRef;
+
+ static initWithOwner(owner: WeakRef) {
+ const ctrl = ModalTransitionController.new();
+ ctrl.owner = owner;
+ return ctrl;
+ }
+
+ transitionDuration(transitionContext: UIViewControllerContextTransitioning): number {
+ return 0.35;
+ }
+
+ animateTransition(transitionContext: UIViewControllerContextTransitioning): void {
+ // console.log('FullScreenAnimationController animateTransition:', animationType);
+ 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 sharedElementSnapshot = UIImageView.alloc().initWithImage(snapshotView(presentingSharedElement));
+ sharedElementSnapshot.clipsToBounds = true;
+ // console.log('---> snapshot: ', sharedElementSnapshot);
+
+ const presentedView = presentedSharedElements.find((v) => v.sharedTransitionTag === presentingView.sharedTransitionTag);
+ const presentedSharedElement = presentedView.ios;
+
+ 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));
+
+ owner.sharedElements.presenting.push({
+ view: presentingView,
+ startFrame,
+ endFrame,
+ snapshot: sharedElementSnapshot,
+ });
+ owner.sharedElements.presented.push({
+ view: presentedView,
+ startFrame: endFrame,
+ endFrame: startFrame,
+ });
+
+ // 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 = 1;
+ }
+
+ // TODO: Discuss with Igor whether this is necessary
+ // potential this could help smooth some shared element transitions
+ UIView.animateWithDurationAnimationsCompletion(
+ 0.3,
+ () => {
+ for (const presenting of owner.sharedElements.presenting) {
+ presenting.snapshot.alpha = 0;
+ }
+ },
+ () => {
+ 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;
+ console.log(`---> ${presentingMatch.view.sharedTransitionTag} animate to: `, printRect(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 : 0.35,
+ 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 = 1;
+ transitionContext.containerView.addSubview(p.snapshot);
+ }
+
+ const cleanupDismiss = () => {
+ for (const presenting of owner.sharedElements.presenting) {
+ presenting.view.opacity = 1;
+ }
+ SharedTransition.finishState(owner.id);
+ transitionContext.completeTransition(true);
+ };
+ const updateFrameDismiss = () => {
+ console.log('3. Dismissing shared elements:');
+ for (const presenting of owner.sharedElements.presenting) {
+ presenting.snapshot.frame = presenting.startFrame;
+ console.log(`---> ${presenting.view.sharedTransitionTag} animate to: `, printRect(presenting.startFrame));
+ }
+ console.log(' ');
+ };
+
+ UIView.animateWithDurationDelayUsingSpringWithDampingInitialSpringVelocityOptionsAnimationsCompletion(
+ typeof state.dismissViewEnd?.duration === 'number' ? state.dismissViewEnd?.duration / 1000 : 0.35,
+ 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;
+ }
+ }
+ }
+}
+
+function snapshotView(view: UIView): UIImage {
+ // 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;
+}
diff --git a/packages/core/ui/transition/shared-transition.ts b/packages/core/ui/transition/shared-transition.ts
new file mode 100644
index 000000000..b90da7c85
--- /dev/null
+++ b/packages/core/ui/transition/shared-transition.ts
@@ -0,0 +1,94 @@
+import type { Transition } from '.';
+// import type { View } from '../../core/view';
+import type { Page } from '../page';
+
+// always increment when adding new transitions to be able to track their state
+export enum SharedTransitionAnimationType {
+ present,
+ dismiss,
+}
+export interface SharedTransitionConfig {
+ /**
+ * Page which will start the transition
+ */
+ page?: Page;
+ /**
+ * Preconfigured transition or your own custom configured one
+ */
+ instance?: Transition;
+ /**
+ * View settings to start your transition from.
+ */
+ incomingViewStart?: SharedTransitionPageProperties;
+ /**
+ * View settings to animate your page to.
+ */
+ incomingViewEnd?: SharedTransitionPageProperties;
+ /**
+ * View settings to animate your page to when dismissed.
+ */
+ dismissViewEnd?: SharedTransitionPageProperties;
+}
+export interface SharedTransitionState extends SharedTransitionConfig {
+ id?: number;
+ activeType?: SharedTransitionAnimationType;
+ toPage?: Page;
+}
+type SharedTransitionPageProperties = {
+ x?: number;
+ y?: number;
+ width?: number;
+ height?: number;
+ opacity?: number;
+ /**
+ * Duration in milliseconds
+ */
+ duration?: number;
+};
+export class SharedTransition {
+ static configure(options: SharedTransitionConfig): Transition {
+ SharedTransition.updateState({
+ ...options,
+ id: options.instance.id,
+ activeType: SharedTransitionAnimationType.present,
+ });
+ return options.instance;
+ }
+
+ static currentStack: Array;
+ static updateState(state: SharedTransitionState) {
+ if (!SharedTransition.currentStack) {
+ SharedTransition.currentStack = [];
+ }
+ const existingTransition = SharedTransition.getState(state.id);
+ if (existingTransition) {
+ // updating existing
+ for (const key in state) {
+ existingTransition[key] = state[key];
+ // console.log(' ... updating state: ', key, state[key])
+ }
+ } else {
+ SharedTransition.currentStack.push(state);
+ }
+ }
+ static addPageToTop(page: Page) {
+ if (SharedTransition.currentStack?.length) {
+ const top = SharedTransition.currentStack.slice(-1)[0];
+ if (top) {
+ SharedTransition.updateState({
+ id: top.id,
+ toPage: page,
+ });
+ }
+ }
+ }
+ static getState(id: number) {
+ return SharedTransition.currentStack.find((t) => t.id === id);
+ }
+ static finishState(id: number) {
+ const index = SharedTransition.currentStack.findIndex((t) => t.id === id);
+ if (index > -1) {
+ SharedTransition.currentStack.splice(index, 1);
+ }
+ }
+}