diff --git a/apps/toolbox/src/pages/transitions.ts b/apps/toolbox/src/pages/transitions.ts
index 6859885a2..79c352769 100644
--- a/apps/toolbox/src/pages/transitions.ts
+++ b/apps/toolbox/src/pages/transitions.ts
@@ -9,17 +9,19 @@ export function navigatingTo(args: EventData) {
// class SampleCustomModalTransition extends ModalTransition implements TransitionType {
// }
+SharedTransition.DEBUG = true;
export class TransitionsModel extends Observable {
open() {
page.frame.navigate({
moduleName: `pages/transitions/transitions-detail`,
transition: SharedTransition.custom(new PageTransition(), {
- toPageStart: {
- duration: 1000,
- },
- fromPageEnd: {
- duration: 500,
- },
+ interactiveDismissal: true,
+ // toPageStart: {
+ // duration: 400,
+ // },
+ // fromPageEnd: {
+ // duration: 500,
+ // },
}),
});
}
@@ -30,15 +32,15 @@ export class TransitionsModel extends Observable {
interactiveDismissal: true,
toPageStart: {
y: 200,
- duration: 1000,
+ // duration: 400,
},
fromPageEnd: {
y: 100,
- duration: 500,
+ // duration: 500,
},
}),
closeCallback(args) {
- console.log('close modal callback', 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
index ade477f7b..eca284641 100644
--- a/apps/toolbox/src/pages/transitions.xml
+++ b/apps/toolbox/src/pages/transitions.xml
@@ -14,10 +14,10 @@
-
-
-
-
+
+
+
+
diff --git a/apps/toolbox/src/pages/transitions/transitions-modal.ts b/apps/toolbox/src/pages/transitions/transitions-modal.ts
index 13b933043..012de626c 100644
--- a/apps/toolbox/src/pages/transitions/transitions-modal.ts
+++ b/apps/toolbox/src/pages/transitions/transitions-modal.ts
@@ -3,8 +3,6 @@ import { Observable, ShownModallyData, LoadEventData, Page, ShowModalOptions } f
let page: Page;
let closeCallback: Function;
export function onShownModally(args: ShownModallyData) {
- console.log('page shown modally');
-
closeCallback = args.closeCallback;
if (args.context) {
diff --git a/packages/core/ui/core/view/index.android.ts b/packages/core/ui/core/view/index.android.ts
index 2af9978c9..9dd436290 100644
--- a/packages/core/ui/core/view/index.android.ts
+++ b/packages/core/ui/core/view/index.android.ts
@@ -714,29 +714,7 @@ export class View extends ViewCommon {
this._dialogFragment = df;
this._raiseShowingModallyEvent();
- if (options?.transition?.instance) {
- const currentEntry = parent.page.frame.currentEntry;
- const newEntry = this.page.frame;
- const manager: androidx.fragment.app.FragmentManager = parent._getRootFragmentManager(); //this._getFragmentManager();
- const transaction = manager.beginTransaction();
- let navigationTransition: any; //NavigationTransition;
- if (newEntry) {
- navigationTransition = newEntry.transition;
- } else {
- navigationTransition = null;
- }
-
- // _setAndroidFragmentTransitions(true, navigationTransition, currentEntry, newEntry, this._domId, transaction, true);
- // transaction.replace(this.containerViewId, newFragment, newFragmentTag);
-
- this._dialogFragment.show(parent._getRootFragmentManager(), this._domId.toString());
- // @ts-ignore - TODO: refactor to a meaningful name/api
- navigationTransition?.instance?.test?.(transaction, currentEntry, newEntry);
-
- transaction.commitAllowingStateLoss();
- } else {
- this._dialogFragment.show(parent._getRootFragmentManager(), this._domId.toString());
- }
+ this._dialogFragment.show(parent._getRootFragmentManager(), this._domId.toString());
}
protected _hideNativeModalView(parent: View, whenClosedCallback: () => void) {
diff --git a/packages/core/ui/core/view/index.ios.ts b/packages/core/ui/core/view/index.ios.ts
index 136ab4807..833da7465 100644
--- a/packages/core/ui/core/view/index.ios.ts
+++ b/packages/core/ui/core/view/index.ios.ts
@@ -566,7 +566,9 @@ export class View extends ViewCommon implements ViewDefinition {
this.interactiveDismissGestureBegan = true;
this.interactiveDismissGestureCancelled = false;
const percent = args.deltaY / (args.ios.view.bounds.size.height / 2);
- console.log('pan:', percent);
+ if (SharedTransition.DEBUG) {
+ console.log('interactive dismissal pan:', percent);
+ }
switch (args.state) {
case GestureStateTypes.began:
if (this._closeModalCallback) {
@@ -1012,7 +1014,6 @@ class UIViewControllerTransitioningDelegateImpl extends NSObject implements UIVi
const ownerView = this.ownerView?.deref();
if (ownerView) {
if (ownerView.interactiveDismissGestureBegan) {
- console.log('interactionControllerForDismissal!');
ownerView.transitionIteractiveCtrl = owner.iosInteractionDismiss(animator);
return ownerView.transitionIteractiveCtrl;
}
diff --git a/packages/core/ui/frame/frame-common.ts b/packages/core/ui/frame/frame-common.ts
index 68c3e8016..b666e586d 100644
--- a/packages/core/ui/frame/frame-common.ts
+++ b/packages/core/ui/frame/frame-common.ts
@@ -40,7 +40,7 @@ export class FrameBase extends CustomLayoutView {
private _animated: boolean;
private _transition: NavigationTransition;
private _backStack = new Array();
- private _navigationQueue = new Array();
+ _navigationQueue = new Array();
public actionBarVisibility: 'auto' | 'never' | 'always';
public _currentEntry: BackstackEntry;
@@ -410,7 +410,7 @@ export class FrameBase extends CustomLayoutView {
}
@profile
- private performGoBack(navigationContext: NavigationContext) {
+ performGoBack(navigationContext: NavigationContext) {
let backstackEntry = navigationContext.entry;
const backstack = this._backStack;
if (!backstackEntry) {
diff --git a/packages/core/ui/frame/index.android.ts b/packages/core/ui/frame/index.android.ts
index a60ad53e5..d3dd9807f 100644
--- a/packages/core/ui/frame/index.android.ts
+++ b/packages/core/ui/frame/index.android.ts
@@ -464,8 +464,7 @@ export class Frame extends FrameBase {
transaction.replace(this.containerViewId, newFragment, newFragmentTag);
- // @ts-ignore - TODO: refactor to a meaningful name/api
- navigationTransition?.instance?.test?.(transaction, currentEntry, newEntry);
+ navigationTransition?.instance?.androidFragmentTransactionCallback?.(transaction, currentEntry, newEntry);
transaction.commitAllowingStateLoss();
}
@@ -490,8 +489,7 @@ export class Frame extends FrameBase {
transaction.replace(this.containerViewId, backstackEntry.fragment, backstackEntry.fragmentTag);
- // @ts-ignore - TODO: refactor to a meaningful name/api
- backstackEntry.transition?.test?.(transaction, this._currentEntry, backstackEntry);
+ backstackEntry.transition?.androidFragmentTransactionCallback?.(transaction, this._currentEntry, backstackEntry);
transaction.commitAllowingStateLoss();
}
diff --git a/packages/core/ui/frame/index.ios.ts b/packages/core/ui/frame/index.ios.ts
index 271ea9043..6c851b0ec 100644
--- a/packages/core/ui/frame/index.ios.ts
+++ b/packages/core/ui/frame/index.ios.ts
@@ -9,7 +9,11 @@ import { _createIOSAnimatedTransitioning } from './fragment.transitions';
import { IOSHelper } from '../core/view/view-helper';
import { profile } from '../../profiling';
import { iOSNativeHelper, layout } from '../../utils';
+import { isNumber } from '../../utils/types';
import { Trace } from '../../trace';
+import type { PageTransition } from '../transition/page-transition';
+import { SharedTransition } from '../transition/shared-transition';
+import { GestureStateTypes, PanGestureEventData } from '../gestures';
export * from './frame-common';
@@ -24,10 +28,11 @@ const NON_ANIMATED_TRANSITION = 'non-animated';
let navDepth = -1;
export class Frame extends FrameBase {
- public viewController: UINavigationControllerImpl;
- public _animatedDelegate = UINavigationControllerAnimatedDelegate.new();
- // public _transitionDelegate;
+ viewController: UINavigationControllerImpl;
+ _animatedDelegate = UINavigationControllerAnimatedDelegate.initWithOwner(new WeakRef(this));
+ transitionIteractiveCtrl: UIPercentDrivenInteractiveTransition;
public _ios: iOSFrame;
+ private _interactiveDismissGesture: (args: PanGestureEventData) => void;
constructor() {
super();
@@ -100,6 +105,14 @@ export class Frame extends FrameBase {
if (!nativeTransition && navigationTransition) {
this._ios.controller.delegate = this._animatedDelegate;
viewController[DELEGATE] = this._animatedDelegate;
+ const transitionState = SharedTransition.getState(navigationTransition.instance.id);
+ if (transitionState?.interactiveDismissal) {
+ // interactive transitions via gestures
+ // TODO - these could be typed as: boolean | (view: View) => void
+ // to allow users to define their own custom gesture dismissals
+ this._interactiveDismissGesture = this._interactiveDismissGestureHandler.bind(this);
+ this.on('pan', this._interactiveDismissGesture);
+ }
} else {
viewController[DELEGATE] = null;
this._ios.controller.delegate = null;
@@ -183,6 +196,44 @@ export class Frame extends FrameBase {
}
}
+ private _interactiveDismissGestureHandler(args: PanGestureEventData) {
+ if (args?.ios?.view) {
+ this.interactiveDismissGestureBegan = true;
+ this.interactiveDismissGestureCancelled = false;
+ const percent = args.deltaX / (args.ios.view.bounds.size.width / 2);
+ if (SharedTransition.DEBUG) {
+ console.log('interactive dismissal pan:', percent);
+ }
+ switch (args.state) {
+ case GestureStateTypes.began:
+ // const navigationContext = this.parent.page.frame._navigationQueue[0];
+ // this.performGoBack(navigationContext);
+ this._ios.controller.popViewControllerAnimated(true);
+
+ break;
+ case GestureStateTypes.changed:
+ // TODO: allow customization of threshold
+ if (percent < 1) {
+ if (this.transitionIteractiveCtrl) {
+ this.transitionIteractiveCtrl.updateInteractiveTransition(percent);
+ }
+ }
+ break;
+ case GestureStateTypes.cancelled:
+ case GestureStateTypes.ended:
+ if (this.transitionIteractiveCtrl) {
+ if (percent > 0.5) {
+ this.transitionIteractiveCtrl.finishInteractiveTransition();
+ } else {
+ this.interactiveDismissGestureCancelled = true;
+ this.transitionIteractiveCtrl.cancelInteractiveTransition();
+ }
+ }
+ break;
+ }
+ }
+ }
+
private pushViewControllerAnimated(viewController: UIViewController, animated: boolean, isModal: boolean) {
const transitionCoordinator = this._ios.controller.transitionCoordinator;
if (!isModal && transitionCoordinator) {
@@ -202,7 +253,9 @@ export class Frame extends FrameBase {
const controller = backstackEntry.resolvedPage.ios;
const animated = this._currentEntry ? this._getIsAnimatedNavigation(this._currentEntry.entry) : false;
- this._updateActionBar(backstackEntry.resolvedPage);
+ if (!this.interactiveDismissGestureCancelled) {
+ this._updateActionBar(backstackEntry.resolvedPage);
+ }
if (Trace.isEnabled()) {
Trace.write(`${this}.popToViewControllerAnimated(${controller}, ${animated}); depth = ${navDepth}`, Trace.categories.Navigation);
}
@@ -378,9 +431,16 @@ const _defaultTransitionDuration = 0.35;
@NativeClass
class UINavigationControllerAnimatedDelegate extends NSObject implements UINavigationControllerDelegate {
public static ObjCProtocols = [UINavigationControllerDelegate];
+ owner: WeakRef;
+ transition: PageTransition;
+
+ static initWithOwner(owner: WeakRef) {
+ const delegate = UINavigationControllerAnimatedDelegate.new();
+ delegate.owner = owner;
+ return delegate;
+ }
navigationControllerAnimationControllerForOperationFromViewControllerToViewController(navigationController: UINavigationController, operation: number, fromVC: UIViewController, toVC: UIViewController): UIViewControllerAnimatedTransitioning {
- console.log('UINavigationControllerAnimatedDelegate navigationControllerAnimationControllerForOperationFromViewControllerToViewController');
let viewController: UIViewController;
switch (operation) {
case UINavigationControllerOperation.Push:
@@ -404,16 +464,127 @@ class UINavigationControllerAnimatedDelegate extends NSObject implements UINavig
Trace.write(`UINavigationControllerImpl.navigationControllerAnimationControllerForOperationFromViewControllerToViewController(${operation}, ${fromVC}, ${toVC}), transition: ${JSON.stringify(navigationTransition)}`, Trace.categories.NativeLifecycle);
}
+ this.transition = navigationTransition.instance;
+
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 {
+ navigationControllerInteractionControllerForAnimationController(navigationController: UINavigationController, animationController: UIViewControllerAnimatedTransitioning): UIViewControllerInteractiveTransitioning {
+ const owner = this.owner?.deref();
+ if (owner) {
+ if (owner.interactiveDismissGestureBegan) {
+ owner.transitionIteractiveCtrl = PercentInteractiveController.initWithOwner(new WeakRef(this.transition));
+ return owner.transitionIteractiveCtrl;
+ }
+ }
- // }
+ return null;
+ }
+}
+
+@NativeClass()
+class PercentInteractiveController extends UIPercentDrivenInteractiveTransition implements UIViewControllerInteractiveTransitioning {
+ static ObjCProtocols = [UIViewControllerInteractiveTransitioning];
+ owner: WeakRef;
+ started = false;
+ transitionContext: UIViewControllerContextTransitioning;
+ backgroundAnimation: UIViewPropertyAnimator;
+
+ static initWithOwner(owner: WeakRef) {
+ const ctrl = PercentInteractiveController.new();
+ ctrl.owner = owner;
+ return ctrl;
+ }
+
+ startInteractiveTransition(transitionContext: UIViewControllerContextTransitioning) {
+ console.log('startInteractiveTransition');
+ this.transitionContext = transitionContext;
+ }
+
+ updateInteractiveTransition(percentComplete: number) {
+ console.log('percentComplete:', percentComplete);
+ const owner: any = this.owner?.deref();
+ if (owner) {
+ if (!this.started) {
+ this.started = true;
+ for (const p of owner.sharedElements.presented) {
+ p.view.opacity = 0;
+ }
+ for (const p of owner.sharedElements.presenting) {
+ p.snapshot.alpha = p.endOpacity;
+ this.transitionContext.containerView.addSubview(p.snapshot);
+ }
+ this.backgroundAnimation = UIViewPropertyAnimator.alloc().initWithDurationDampingRatioAnimations(1, 1, () => {
+ for (const p of owner.sharedElements.presenting) {
+ p.snapshot.frame = p.startFrame;
+ iOSNativeHelper.copyLayerProperties(p.snapshot, p.view.ios);
+
+ p.snapshot.alpha = 1;
+ }
+ owner.presented.view.alpha = 0;
+ owner.presented.view.frame = CGRectMake(0, 200, owner.presented.view.bounds.size.width, owner.presented.view.bounds.size.height);
+ });
+ }
+
+ this.backgroundAnimation.fractionComplete = percentComplete;
+ }
+ }
+
+ cancelInteractiveTransition() {
+ console.log('cancelInteractiveTransition');
+ const owner: any = this.owner?.deref();
+ if (owner && this.started) {
+ const state = SharedTransition.getState(owner.id);
+ if (!state) {
+ return;
+ }
+ if (this.backgroundAnimation) {
+ this.backgroundAnimation.reversed = true;
+ const duration = isNumber(state.toPageStart?.duration) ? state.toPageStart?.duration / 1000 : 0.35;
+ this.backgroundAnimation.continueAnimationWithTimingParametersDurationFactor(null, duration);
+ setTimeout(() => {
+ for (const p of owner.sharedElements.presented) {
+ p.view.opacity = 1;
+ }
+ for (const p of owner.sharedElements.presenting) {
+ p.snapshot.removeFromSuperview();
+ }
+ owner.presented.view.alpha = 1;
+ this.backgroundAnimation = null;
+ this.started = false;
+ this.transitionContext.completeTransition(false);
+ }, duration * 1000);
+ }
+ }
+ }
+
+ finishInteractiveTransition() {
+ console.log('finishInteractiveTransition');
+ const owner: any = this.owner?.deref();
+ if (owner && this.started) {
+ if (this.backgroundAnimation) {
+ const state = SharedTransition.getState(owner.id);
+ if (!state) {
+ SharedTransition.finishState(owner.id);
+ this.transitionContext.completeTransition(true);
+ return;
+ }
+
+ const duration = isNumber(state.fromPageEnd?.duration) ? state.fromPageEnd?.duration / 1000 : 0.35;
+ this.backgroundAnimation.continueAnimationWithTimingParametersDurationFactor(null, duration);
+ setTimeout(() => {
+ for (const presenting of owner.sharedElements.presenting) {
+ presenting.view.opacity = presenting.startOpacity;
+ }
+ SharedTransition.finishState(owner.id);
+ this.transitionContext.completeTransition(true);
+ }, duration * 1000);
+ }
+ }
+ }
}
@NativeClass
diff --git a/packages/core/ui/transition/fade-transition.d.ts b/packages/core/ui/transition/fade-transition.d.ts
index 08c7b1acd..86de0cbe0 100644
--- a/packages/core/ui/transition/fade-transition.d.ts
+++ b/packages/core/ui/transition/fade-transition.d.ts
@@ -1,5 +1,3 @@
import { Transition } from '.';
-export class FadeTransition extends Transition {
- constructor(duration: number, nativeCurve: any);
-}
+export class FadeTransition extends Transition {}
diff --git a/packages/core/ui/transition/index.android.ts b/packages/core/ui/transition/index.android.ts
index 2a0575973..78cb9b822 100644
--- a/packages/core/ui/transition/index.android.ts
+++ b/packages/core/ui/transition/index.android.ts
@@ -43,11 +43,3 @@ 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 623de0b98..707075367 100644
--- a/packages/core/ui/transition/index.d.ts
+++ b/packages/core/ui/transition/index.d.ts
@@ -1,4 +1,6 @@
-export declare class Transition {
+import type { BackstackEntry } from '../frame';
+
+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 */);
@@ -15,11 +17,6 @@
iosInteractionPresented?(animator: any /* UIViewControllerAnimatedTransitioning */): any /* UIViewControllerInteractiveTransitioning */;
iosPresentedViewController?(presented: any /* UIViewController */, presenting: any /* UIViewController */, source: any /* UIViewController */): any /* UIPresentationController */;
+
+ androidFragmentTransactionCallback?(fragmentTransaction: any /* androidx.fragment.app.FragmentTransaction */, currentEntry: BackstackEntry, newEntry: BackstackEntry): void;
}
-
-// 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 4aee7e43b..294f69469 100644
--- a/packages/core/ui/transition/index.ios.ts
+++ b/packages/core/ui/transition/index.ios.ts
@@ -35,40 +35,3 @@ 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.android.ts b/packages/core/ui/transition/modal-transition.android.ts
index 10ac1ead6..6880b64c0 100644
--- a/packages/core/ui/transition/modal-transition.android.ts
+++ b/packages/core/ui/transition/modal-transition.android.ts
@@ -1,67 +1,8 @@
import { BackstackEntry } from '../frame';
-import { Transition } from '.';
-import { SharedTransition } from './shared-transition';
+import { FadeTransition } from './fade-transition';
-export class ModalTransition extends Transition {
- createAndroidAnimator(transitionType: string) {
- console.log('HELLO ModalTransition', transitionType);
- }
-
- test(fragmentTransaction: androidx.fragment.app.FragmentTransaction, currentEntry: BackstackEntry, newEntry: BackstackEntry) {
- console.log('HELLO ModalTransition', fragmentTransaction);
-
- const fromPage = currentEntry.resolvedPage;
- const toPage = newEntry.resolvedPage;
-
- const currentFragment: androidx.fragment.app.Fragment = currentEntry ? currentEntry.fragment : null;
- const newFragment: androidx.fragment.app.Fragment = newEntry.fragment;
-
- const { sharedElements, presented, presenting } = SharedTransition.getSharedElements(fromPage, toPage);
- // fragmentTransaction.addSharedElement();
-
- toPage.on('loaded', () => {
- presented.forEach((v) => {
- console.log({
- presentedSharedElements: true,
- nativeView: !!v.nativeView,
- sharedTransitionTag: v.sharedTransitionTag,
- });
- androidx.core.view.ViewCompat.setTransitionName(v.nativeView, v.sharedTransitionTag);
- });
-
- // setTimeout(() => {
- newFragment.startPostponedEnterTransition();
- // }, 2000)
- });
-
- presenting.forEach((v) => {
- console.log({
- presentingSharedElements: true,
- nativeView: !!v.nativeView,
- sharedTransitionTag: v.sharedTransitionTag,
- });
- androidx.core.view.ViewCompat.setTransitionName(v.nativeView, v.sharedTransitionTag);
- });
- presenting.forEach((v) => {
- fragmentTransaction.addSharedElement(v.nativeView, v.sharedTransitionTag);
- });
-
- fragmentTransaction.setReorderingAllowed(true);
-
- const transitionSet = new androidx.transition.TransitionSet();
- transitionSet.setDuration(2000);
- transitionSet.addTransition(new androidx.transition.ChangeBounds());
- transitionSet.addTransition(new androidx.transition.ChangeTransform());
- // transitionSet.addTransition(new androidx.transition.ChangeClipBounds());
-
- // postpone enter until we call "loaded" on the new page
- newFragment.postponeEnterTransition();
-
- newFragment.setSharedElementEnterTransition(transitionSet);
- newFragment.setSharedElementReturnTransition(transitionSet);
-
- // newFragment.setSharedElementReturnTransition(new androidx.transition.ChangeBounds());
- // currentFragment.setSharedElementEnterTransition(new androidx.transition.ChangeBounds());
- // currentFragment.setSharedElementReturnTransition(new androidx.transition.ChangeBounds());
+export class ModalTransition extends FadeTransition {
+ androidFragmentTransactionCallback(fragmentTransaction: androidx.fragment.app.FragmentTransaction, currentEntry: BackstackEntry, newEntry: BackstackEntry) {
+ console.log('Not currently supported on Android.');
}
}
diff --git a/packages/core/ui/transition/modal-transition.ios.ts b/packages/core/ui/transition/modal-transition.ios.ts
index 7821db7aa..d273c4c22 100644
--- a/packages/core/ui/transition/modal-transition.ios.ts
+++ b/packages/core/ui/transition/modal-transition.ios.ts
@@ -1,9 +1,9 @@
-import { querySelectorAll } from '../core/view-base';
import type { View } from '../core/view';
import { Screen } from '../../platform';
-import { valueMap } from '../../utils/number-utils';
-import { Transition, iosMatchLayerProperties, iosPrintRect, iosSnapshotView } from '.';
-import { SharedTransition, SharedTransitionAnimationType, DEFAULT_DURATION } from './shared-transition';
+import { iOSNativeHelper } from '../../utils/native-helper';
+import { isNumber } from '../../utils/types';
+import { Transition } from '.';
+import { SharedTransition, SharedTransitionAnimationType, DEFAULT_DURATION, DEFAULT_SPRING } from './shared-transition';
export class ModalTransition extends Transition {
transitionController: ModalTransitionController;
@@ -29,18 +29,18 @@ export class ModalTransition extends Transition {
}
iosInteractionDismiss(animator: UIViewControllerAnimatedTransitioning): UIViewControllerInteractiveTransitioning {
- console.log('-- iosInteractionDismiss --');
+ // console.log('-- iosInteractionDismiss --');
this.interactiveController = PercentInteractiveController.initWithOwner(new WeakRef(this));
return this.interactiveController;
}
iosInteractionPresented(animator: UIViewControllerAnimatedTransitioning): UIViewControllerInteractiveTransitioning {
- console.log('-- iosInteractionPresented --');
+ // console.log('-- iosInteractionPresented --');
return null;
}
iosPresentedViewController(presented: UIViewController, presenting: UIViewController, source: UIViewController): UIPresentationController {
- console.log('-- iosPresentedViewController --');
+ // console.log('-- iosPresentedViewController --');
return null;
}
}
@@ -60,7 +60,7 @@ class PercentInteractiveController extends UIPercentDrivenInteractiveTransition
}
startInteractiveTransition(transitionContext: UIViewControllerContextTransitioning) {
- console.log('startInteractiveTransition');
+ // console.log('startInteractiveTransition');
this.transitionContext = transitionContext;
}
@@ -77,15 +77,18 @@ class PercentInteractiveController extends UIPercentDrivenInteractiveTransition
p.snapshot.alpha = p.endOpacity;
this.transitionContext.containerView.addSubview(p.snapshot);
}
+ const state = SharedTransition.getState(owner.id);
+ const toProps = state.toPageStart;
+ const startY = isNumber(toProps?.y) ? toProps?.y : Screen.mainScreen.heightDIPs;
this.backgroundAnimation = UIViewPropertyAnimator.alloc().initWithDurationDampingRatioAnimations(1, 1, () => {
for (const p of owner.sharedElements.presenting) {
p.snapshot.frame = p.startFrame;
- iosMatchLayerProperties(p.snapshot, p.view.ios);
+ iOSNativeHelper.copyLayerProperties(p.snapshot, p.view.ios);
p.snapshot.alpha = 1;
}
owner.presented.view.alpha = 0;
- owner.presented.view.frame = CGRectMake(0, 200, owner.presented.view.bounds.size.width, owner.presented.view.bounds.size.height);
+ owner.presented.view.frame = CGRectMake(0, startY, owner.presented.view.bounds.size.width, owner.presented.view.bounds.size.height);
});
}
@@ -94,7 +97,7 @@ class PercentInteractiveController extends UIPercentDrivenInteractiveTransition
}
cancelInteractiveTransition() {
- console.log('cancelInteractiveTransition');
+ // console.log('cancelInteractiveTransition');
const owner = this.owner?.deref();
if (owner && this.started) {
const state = SharedTransition.getState(owner.id);
@@ -103,7 +106,7 @@ class PercentInteractiveController extends UIPercentDrivenInteractiveTransition
}
if (this.backgroundAnimation) {
this.backgroundAnimation.reversed = true;
- const duration = typeof state.toPageStart?.duration === 'number' ? state.toPageStart?.duration / 1000 : DEFAULT_DURATION;
+ const duration = isNumber(state.toPageStart?.duration) ? state.toPageStart?.duration / 1000 : DEFAULT_DURATION;
this.backgroundAnimation.continueAnimationWithTimingParametersDurationFactor(null, duration);
setTimeout(() => {
for (const p of owner.sharedElements.presented) {
@@ -122,7 +125,7 @@ class PercentInteractiveController extends UIPercentDrivenInteractiveTransition
}
finishInteractiveTransition() {
- console.log('finishInteractiveTransition');
+ // console.log('finishInteractiveTransition');
const owner = this.owner?.deref();
if (owner && this.started) {
if (this.backgroundAnimation) {
@@ -133,7 +136,7 @@ class PercentInteractiveController extends UIPercentDrivenInteractiveTransition
return;
}
- const duration = typeof state.fromPageEnd?.duration === 'number' ? state.fromPageEnd?.duration / 1000 : DEFAULT_DURATION;
+ const duration = isNumber(state.fromPageEnd?.duration) ? state.fromPageEnd?.duration / 1000 : DEFAULT_DURATION;
this.backgroundAnimation.continueAnimationWithTimingParametersDurationFactor(null, duration);
setTimeout(() => {
for (const presenting of owner.sharedElements.presenting) {
@@ -174,20 +177,22 @@ class ModalTransitionController extends NSObject implements UIViewControllerAnim
// console.log('state.activeType:', state.activeType)
switch (state.activeType) {
case SharedTransitionAnimationType.present: {
- console.log('-- Transition present --');
+ // console.log('-- Transition present --');
transitionContext.containerView.addSubview(owner.presented.view);
owner.presented.view.layoutIfNeeded();
const { sharedElements, presented } = SharedTransition.getSharedElements(state.page, state.toPage);
- console.log(' ');
- console.log(
- `1. Found sharedTransitionTags to animate:`,
- sharedElements.map((v) => v.sharedTransitionTag)
- );
+ if (SharedTransition.DEBUG) {
+ console.log(' ');
+ console.log(
+ `1. Found sharedTransitionTags to animate:`,
+ sharedElements.map((v) => v.sharedTransitionTag)
+ );
- console.log(`2. Take snapshots of shared elements and position them based on presenting view:`);
+ console.log(`2. Take snapshots of shared elements and position them based on presenting view:`);
+ }
for (const presentingView of sharedElements) {
if (!owner.sharedElements) {
@@ -213,7 +218,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 = iosSnapshotView(presentedSharedElement);
+ sharedElementSnapshot.image = iOSNativeHelper.snapshotView(presentedSharedElement, Screen.mainScreen.scale);
sharedElementSnapshot.tintColor = presentedSharedElement.tintColor;
});
@@ -221,14 +226,16 @@ class ModalTransitionController extends NSObject implements UIViewControllerAnim
sharedElementSnapshot.contentMode = presentedSharedElement.contentMode;
}
- iosMatchLayerProperties(sharedElementSnapshot, presentingSharedElement);
+ iOSNativeHelper.copyLayerProperties(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));
+ if (SharedTransition.DEBUG) {
+ console.log('---> ', presentingView.sharedTransitionTag, ' frame:', iOSNativeHelper.printCGRect(sharedElementSnapshot.frame));
+ }
owner.sharedElements.presenting.push({
view: presentingView,
@@ -259,26 +266,13 @@ class ModalTransitionController extends NSObject implements UIViewControllerAnim
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(owner.id, {
- activeType: SharedTransitionAnimationType.dismiss,
- });
- transitionContext.completeTransition(true);
- }
- );
+ for (const presenting of owner.sharedElements.presenting) {
+ presenting.snapshot.removeFromSuperview();
+ }
+ SharedTransition.updateState(owner.id, {
+ activeType: SharedTransitionAnimationType.dismiss,
+ });
+ transitionContext.completeTransition(true);
};
const updateFramePresent = () => {
// https://stackoverflow.com/a/27997678/1418981
@@ -287,7 +281,9 @@ class ModalTransitionController extends NSObject implements UIViewControllerAnim
// Not sure if best to leave all the time?
// owner.presented.view.setNeedsLayout();
// owner.presented.view.layoutIfNeeded();
- console.log('3. Animating shared elements:');
+ if (SharedTransition.DEBUG) {
+ 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.
@@ -296,62 +292,84 @@ 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
- iosMatchLayerProperties(presentingMatch.snapshot, presented.view.ios);
+ iOSNativeHelper.copyLayerProperties(presentingMatch.snapshot, presented.view.ios);
// create a snapshot of the presented view
- presentingMatch.snapshot.image = iosSnapshotView(presented.view.ios);
+ presentingMatch.snapshot.image = iOSNativeHelper.snapshotView(presented.view.ios, Screen.mainScreen.scale);
// apply correct alpha
presentingMatch.snapshot.alpha = presentingMatch.endOpacity;
- console.log(`---> ${presentingMatch.view.sharedTransitionTag} animate to: `, iosPrintRect(correctedEndFrame));
+ if (SharedTransition.DEBUG) {
+ console.log(`---> ${presentingMatch.view.sharedTransitionTag} animate to: `, iOSNativeHelper.printCGRect(correctedEndFrame));
+ }
}
- console.log(' ');
};
// starting page properties
- owner.presented.view.alpha = typeof state.toPageStart?.opacity === 'number' ? state.toPageStart?.opacity : 0;
+ const toProps = state.toPageStart;
+ owner.presented.view.alpha = isNumber(toProps?.opacity) ? toProps?.opacity : 0;
- const startX = typeof state.toPageStart?.x === 'number' ? state.toPageStart?.x : 0;
- const startY = typeof state.toPageStart?.y === 'number' ? state.toPageStart?.y : Screen.mainScreen.heightDIPs;
- const startWidth = typeof state.toPageStart?.width === 'number' ? state.toPageStart?.width : Screen.mainScreen.widthDIPs;
- const startHeight = typeof state.toPageStart?.height === 'number' ? state.toPageStart?.height : Screen.mainScreen.heightDIPs;
+ const startX = isNumber(toProps?.x) ? toProps?.x : 0;
+ const startY = isNumber(toProps?.y) ? toProps?.y : Screen.mainScreen.heightDIPs;
+ const startWidth = isNumber(toProps?.width) ? toProps?.width : Screen.mainScreen.widthDIPs;
+ const startHeight = isNumber(toProps?.height) ? toProps?.height : Screen.mainScreen.heightDIPs;
owner.presented.view.frame = CGRectMake(startX, startY, startWidth, startHeight);
- UIView.animateWithDurationDelayUsingSpringWithDampingInitialSpringVelocityOptionsAnimationsCompletion(
- typeof state.toPageStart?.duration === 'number' ? state.toPageStart?.duration / 1000 : DEFAULT_DURATION,
- 0,
- 0.5,
- 3,
- UIViewAnimationOptions.CurveEaseInOut,
- () => {
- // animate page properties to the following:
- owner.presented.view.alpha = typeof state.toPageEnd?.opacity === 'number' ? state.toPageEnd?.opacity : 1;
+ const animateProperties = () => {
+ const props = state.toPageEnd;
+ // animate page properties to the following:
+ owner.presented.view.alpha = isNumber(props?.opacity) ? props?.opacity : 1;
- const endX = typeof state.toPageEnd?.x === 'number' ? state.toPageEnd?.x : 0;
- const endY = typeof state.toPageEnd?.y === 'number' ? state.toPageEnd?.y : 0;
- const endWidth = typeof state.toPageEnd?.width === 'number' ? state.toPageEnd?.width : Screen.mainScreen.widthDIPs;
- const endHeight = typeof state.toPageEnd?.height === 'number' ? state.toPageEnd?.height : Screen.mainScreen.heightDIPs;
- owner.presented.view.frame = CGRectMake(endX, endY, endWidth, endHeight);
+ const endX = isNumber(props?.x) ? props?.x : 0;
+ const endY = isNumber(props?.y) ? props?.y : 0;
+ const endWidth = isNumber(props?.width) ? props?.width : Screen.mainScreen.widthDIPs;
+ const endHeight = isNumber(props?.height) ? props?.height : Screen.mainScreen.heightDIPs;
+ owner.presented.view.frame = CGRectMake(endX, endY, endWidth, endHeight);
+ };
+
+ if (isNumber(toProps?.duration)) {
+ // override spring and use only linear animation
+ UIView.animateWithDurationAnimationsCompletion(
+ toProps?.duration / 1000,
+ () => {
+ animateProperties();
+ updateFramePresent();
+ },
+ () => {
+ cleanupPresent();
+ }
+ );
+ } else {
+ const spring = toProps?.spring;
+ iOSNativeHelper.animateWithSpring({
+ tension: isNumber(spring?.tension) ? spring?.tension : DEFAULT_SPRING.tension,
+ friction: isNumber(spring?.friction) ? spring?.tension : DEFAULT_SPRING.friction,
+ animations: () => {
+ animateProperties();
+
+ updateFramePresent();
+ },
+ completion: () => {
+ cleanupPresent();
+ },
+ });
+ }
- updateFramePresent();
- },
- () => {
- cleanupPresent();
- }
- );
break;
}
case SharedTransitionAnimationType.dismiss: {
- console.log('-- Transition 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)
- );
+ if (SharedTransition.DEBUG) {
+ 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:`);
+ console.log(`2. Add back previously stored sharedElements to dismiss:`);
+ }
for (const p of owner.sharedElements.presented) {
p.view.opacity = 0;
@@ -369,38 +387,60 @@ class ModalTransitionController extends NSObject implements UIViewControllerAnim
transitionContext.completeTransition(true);
};
const updateFrameDismiss = () => {
- console.log('3. Dismissing shared elements:');
+ if (SharedTransition.DEBUG) {
+ console.log('3. Dismissing shared elements:');
+ }
for (const presenting of owner.sharedElements.presenting) {
- iosMatchLayerProperties(presenting.snapshot, presenting.view.ios);
+ iOSNativeHelper.copyLayerProperties(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));
+ if (SharedTransition.DEBUG) {
+ console.log(`---> ${presenting.view.sharedTransitionTag} animate to: `, iOSNativeHelper.printCGRect(presenting.startFrame));
+ }
}
- console.log(' ');
};
- UIView.animateWithDurationDelayUsingSpringWithDampingInitialSpringVelocityOptionsAnimationsCompletion(
- typeof state.fromPageEnd?.duration === 'number' ? state.fromPageEnd?.duration / 1000 : DEFAULT_DURATION,
- 0,
- 0.5,
- 3,
- UIViewAnimationOptions.CurveEaseInOut,
- () => {
- owner.presented.view.alpha = typeof state.fromPageEnd?.opacity === 'number' ? state.fromPageEnd?.opacity : 0;
+ const props = state.fromPageEnd;
- const endX = typeof state.fromPageEnd?.x === 'number' ? state.fromPageEnd?.x : 0;
- const endY = typeof state.fromPageEnd?.y === 'number' ? state.fromPageEnd?.y : Screen.mainScreen.heightDIPs;
- const endWidth = typeof state.fromPageEnd?.width === 'number' ? state.fromPageEnd?.width : Screen.mainScreen.widthDIPs;
- const endHeight = typeof state.fromPageEnd?.height === 'number' ? state.fromPageEnd?.height : Screen.mainScreen.heightDIPs;
- owner.presented.view.frame = CGRectMake(endX, endY, endWidth, endHeight);
+ const animateProperties = () => {
+ owner.presented.view.alpha = isNumber(props?.opacity) ? props?.opacity : 0;
+
+ const endX = isNumber(props?.x) ? props?.x : 0;
+ const endY = isNumber(props?.y) ? props?.y : Screen.mainScreen.heightDIPs;
+ const endWidth = isNumber(props?.width) ? props?.width : Screen.mainScreen.widthDIPs;
+ const endHeight = isNumber(props?.height) ? props?.height : Screen.mainScreen.heightDIPs;
+ owner.presented.view.frame = CGRectMake(endX, endY, endWidth, endHeight);
+ };
+
+ if (isNumber(props?.duration)) {
+ // override spring and use only linear animation
+ UIView.animateWithDurationAnimationsCompletion(
+ props?.duration / 1000,
+ () => {
+ animateProperties();
+ updateFrameDismiss();
+ },
+ () => {
+ cleanupDismiss();
+ }
+ );
+ } else {
+ const spring = props?.spring;
+ iOSNativeHelper.animateWithSpring({
+ tension: isNumber(spring?.tension) ? spring?.tension : DEFAULT_SPRING.tension,
+ friction: isNumber(spring?.friction) ? spring?.tension : DEFAULT_SPRING.friction,
+ animations: () => {
+ animateProperties();
+
+ updateFrameDismiss();
+ },
+ completion: () => {
+ cleanupDismiss();
+ },
+ });
+ }
- updateFrameDismiss();
- },
- () => {
- cleanupDismiss();
- }
- );
break;
}
}
diff --git a/packages/core/ui/transition/page-transition.android.ts b/packages/core/ui/transition/page-transition.android.ts
index 33f8ec9e7..81595b5ad 100644
--- a/packages/core/ui/transition/page-transition.android.ts
+++ b/packages/core/ui/transition/page-transition.android.ts
@@ -1,15 +1,35 @@
import { BackstackEntry } from '../frame';
-import { Transition } from '.';
import { SharedTransition } from './shared-transition';
+// import { SlideTransition } from './slide-transition';
+import { FadeTransition } from './fade-transition';
-export class PageTransition extends Transition {
- createAndroidAnimator(transitionType: string) {
- console.log('HELLO PageTransition', transitionType);
+export class PageTransition extends FadeTransition {
+ //SlideTransition
+
+ constructor() {
+ super(500);
+ // when extending slide:
+ // super('left', 500);
}
- test(fragmentTransaction: androidx.fragment.app.FragmentTransaction, currentEntry: BackstackEntry, newEntry: BackstackEntry) {
- console.log('HELLO PageTransition', fragmentTransaction);
+ // when extending slide:
+ // createAndroidAnimator(transitionType: string) {
+ // // console.log('HELLO PageTransition', transitionType);
+ // if (
+ // [
+ // Transition.AndroidTransitionType.enter,
+ // Transition.AndroidTransitionType.popEnter,
+ // Transition.AndroidTransitionType.popExit,
+ // // When extending SlideTransition, disable exit
+ // // This causes back navigation shared elements position to be off
+ // // Transition.AndroidTransitionType.exit,
+ // ].includes(transitionType)
+ // ) {
+ // return super.createAndroidAnimator(transitionType);
+ // }
+ // }
+ androidFragmentTransactionCallback(fragmentTransaction: androidx.fragment.app.FragmentTransaction, currentEntry: BackstackEntry, newEntry: BackstackEntry) {
const fromPage = currentEntry.resolvedPage;
const toPage = newEntry.resolvedPage;
@@ -49,7 +69,7 @@ export class PageTransition extends Transition {
fragmentTransaction.setReorderingAllowed(true);
const transitionSet = new androidx.transition.TransitionSet();
- transitionSet.setDuration(2000);
+ transitionSet.setDuration(500);
transitionSet.addTransition(new androidx.transition.ChangeBounds());
transitionSet.addTransition(new androidx.transition.ChangeTransform());
// transitionSet.addTransition(new androidx.transition.ChangeClipBounds());
diff --git a/packages/core/ui/transition/page-transition.ios.ts b/packages/core/ui/transition/page-transition.ios.ts
index 7d85a0181..66b451adb 100644
--- a/packages/core/ui/transition/page-transition.ios.ts
+++ b/packages/core/ui/transition/page-transition.ios.ts
@@ -1,8 +1,9 @@
-import { querySelectorAll } from '../core/view-base';
-import type { View } from '../core/view';
-import { Transition, iosMatchLayerProperties, iosSnapshotView, iosPrintRect } from '.';
+import type { View } from '../core/view';
+import { Transition } from '.';
import { Screen } from '../../platform';
-import { SharedTransition, SharedTransitionAnimationType, DEFAULT_DURATION } from './shared-transition';
+import { iOSNativeHelper } from '../../utils/native-helper';
+import { isNumber } from '../../utils/types';
+import { SharedTransition, SharedTransitionAnimationType, DEFAULT_DURATION, DEFAULT_SPRING } from './shared-transition';
export class PageTransition extends Transition {
presented: UIViewController;
@@ -13,22 +14,22 @@ export class PageTransition extends Transition {
};
animateIOSTransition(transitionContext: UIViewControllerContextTransitioning, fromViewCtrl: UIViewController, toViewCtrl: UIViewController, operation: UINavigationControllerOperation): void {
- console.log('--- PageTransitionController animateTransition');
- console.log('toViewCtrl:', toViewCtrl);
- console.log('fromViewCtrl:', fromViewCtrl);
+ // console.log('--- PageTransitionController animateTransition');
+ // console.log('toViewCtrl:', toViewCtrl);
+ // console.log('fromViewCtrl:', fromViewCtrl);
// console.log('owner.id:', owner.id);
const state = SharedTransition.getState(this.id);
if (!state) {
return;
}
- console.log('state.activeType:', state.activeType);
- console.log('operation:', operation);
+ // console.log('state.activeType:', state.activeType);
+ // console.log('operation:', operation);
// switch (state.activeType) {
switch (operation) {
case UINavigationControllerOperation.Push: {
this.presented = toViewCtrl;
- console.log('-- Transition present --', this.presented);
+ // console.log('-- Transition present --', this.presented);
// transitionContext.containerView.addSubview(this.presented.view);
transitionContext.containerView.insertSubviewAboveSubview(this.presented.view, fromViewCtrl.view);
@@ -36,13 +37,15 @@ export class PageTransition extends Transition {
const { sharedElements, presented } = SharedTransition.getSharedElements(state.page, state.toPage);
- console.log(' ');
- console.log(
- `1. Found sharedTransitionTags to animate:`,
- sharedElements.map((v) => v.sharedTransitionTag)
- );
+ if (SharedTransition.DEBUG) {
+ console.log(' ');
+ console.log(
+ `1. Found sharedTransitionTags to animate:`,
+ sharedElements.map((v) => v.sharedTransitionTag)
+ );
- console.log(`2. Take snapshots of shared elements and position them based on presenting view:`);
+ console.log(`2. Take snapshots of shared elements and position them based on presenting view:`);
+ }
for (const presentingView of sharedElements) {
if (!this.sharedElements) {
@@ -68,7 +71,7 @@ export class PageTransition extends Transition {
// 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.image = iOSNativeHelper.snapshotView(presentedSharedElement, Screen.mainScreen.scale);
sharedElementSnapshot.tintColor = presentedSharedElement.tintColor;
});
@@ -76,14 +79,16 @@ export class PageTransition extends Transition {
sharedElementSnapshot.contentMode = presentedSharedElement.contentMode;
}
- iosMatchLayerProperties(sharedElementSnapshot, presentingSharedElement);
+ iOSNativeHelper.copyLayerProperties(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));
+ if (SharedTransition.DEBUG) {
+ console.log('---> ', presentingView.sharedTransitionTag, ' frame:', iOSNativeHelper.printCGRect(sharedElementSnapshot.frame));
+ }
this.sharedElements.presenting.push({
view: presentingView,
@@ -114,26 +119,13 @@ export class PageTransition extends Transition {
for (const presented of this.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 this.sharedElements.presenting) {
- presenting.snapshot.alpha = presenting.endOpacity;
- }
- },
- () => {
- for (const presenting of this.sharedElements.presenting) {
- presenting.snapshot.removeFromSuperview();
- }
- SharedTransition.updateState(this.id, {
- activeType: SharedTransitionAnimationType.dismiss,
- });
- transitionContext.completeTransition(true);
- }
- );
+ for (const presenting of this.sharedElements.presenting) {
+ presenting.snapshot.removeFromSuperview();
+ }
+ SharedTransition.updateState(this.id, {
+ activeType: SharedTransitionAnimationType.dismiss,
+ });
+ transitionContext.completeTransition(true);
};
const updateFramePresent = () => {
// https://stackoverflow.com/a/27997678/1418981
@@ -142,7 +134,9 @@ export class PageTransition extends Transition {
// Not sure if best to leave all the time?
// owner.presented.view.setNeedsLayout();
// owner.presented.view.layoutIfNeeded();
- console.log('3. Animating shared elements:');
+ if (SharedTransition.DEBUG) {
+ console.log('3. Animating shared elements:');
+ }
for (const presented of this.sharedElements.presented) {
const presentingMatch = this.sharedElements.presenting.find((v) => v.view.sharedTransitionTag === presented.view.sharedTransitionTag);
// Workaround wrong origin due ongoing layout process.
@@ -151,65 +145,85 @@ export class PageTransition extends Transition {
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);
+ iOSNativeHelper.copyLayerProperties(presentingMatch.snapshot, presented.view.ios);
// create a snapshot of the presented view
- presentingMatch.snapshot.image = iosSnapshotView(presented.view.ios);
+ presentingMatch.snapshot.image = iOSNativeHelper.snapshotView(presented.view.ios, Screen.mainScreen.scale);
// apply correct alpha
presentingMatch.snapshot.alpha = presentingMatch.endOpacity;
- console.log(`---> ${presentingMatch.view.sharedTransitionTag} animate to: `, iosPrintRect(correctedEndFrame));
+ if (SharedTransition.DEBUG) {
+ console.log(`---> ${presentingMatch.view.sharedTransitionTag} animate to: `, iOSNativeHelper.printCGRect(correctedEndFrame));
+ }
}
- console.log(' ');
};
// starting page properties
- this.presented.view.alpha = typeof state.toPageStart?.opacity === 'number' ? state.toPageStart?.opacity : 0;
+ const toProps = state.toPageStart;
+ this.presented.view.alpha = isNumber(toProps?.opacity) ? toProps?.opacity : 0;
- const startX = typeof state.toPageStart?.x === 'number' ? state.toPageStart?.x : Screen.mainScreen.widthDIPs;
- const startY = typeof state.toPageStart?.y === 'number' ? state.toPageStart?.y : 0;
- const startWidth = typeof state.toPageStart?.width === 'number' ? state.toPageStart?.width : Screen.mainScreen.widthDIPs;
- const startHeight = typeof state.toPageStart?.height === 'number' ? state.toPageStart?.height : Screen.mainScreen.heightDIPs;
+ const startX = isNumber(toProps?.x) ? toProps?.x : Screen.mainScreen.widthDIPs;
+ const startY = isNumber(toProps?.y) ? toProps?.y : 0;
+ const startWidth = isNumber(toProps?.width) ? toProps?.width : Screen.mainScreen.widthDIPs;
+ const startHeight = isNumber(toProps?.height) ? toProps?.height : Screen.mainScreen.heightDIPs;
this.presented.view.frame = CGRectMake(startX, startY, startWidth, startHeight);
- UIView.animateWithDurationDelayUsingSpringWithDampingInitialSpringVelocityOptionsAnimationsCompletion(
- typeof state.toPageStart?.duration === 'number' ? state.toPageStart?.duration / 1000 : DEFAULT_DURATION,
- 0,
- 0.5,
- 3,
- UIViewAnimationOptions.CurveEaseInOut,
- () => {
- // animate page properties to the following:
- this.presented.view.alpha = typeof state.toPageEnd?.opacity === 'number' ? state.toPageEnd?.opacity : 1;
+ const animateProperties = () => {
+ const props = state.toPageEnd;
+ // animate page properties to the following:
+ this.presented.view.alpha = isNumber(props?.opacity) ? props?.opacity : 1;
- const endX = typeof state.toPageEnd?.x === 'number' ? state.toPageEnd?.x : 0;
- const endY = typeof state.toPageEnd?.y === 'number' ? state.toPageEnd?.y : 0;
- const endWidth = typeof state.toPageEnd?.width === 'number' ? state.toPageEnd?.width : Screen.mainScreen.widthDIPs;
- const endHeight = typeof state.toPageEnd?.height === 'number' ? state.toPageEnd?.height : Screen.mainScreen.heightDIPs;
- this.presented.view.frame = CGRectMake(endX, endY, endWidth, endHeight);
+ const endX = isNumber(props?.x) ? props?.x : 0;
+ const endY = isNumber(props?.y) ? props?.y : 0;
+ const endWidth = isNumber(props?.width) ? props?.width : Screen.mainScreen.widthDIPs;
+ const endHeight = isNumber(props?.height) ? props?.height : Screen.mainScreen.heightDIPs;
+ this.presented.view.frame = CGRectMake(endX, endY, endWidth, endHeight);
+ };
+ if (isNumber(toProps?.duration)) {
+ // override spring and use only linear animation
+ UIView.animateWithDurationAnimationsCompletion(
+ toProps?.duration / 1000,
+ () => {
+ animateProperties();
+ updateFramePresent();
+ },
+ () => {
+ cleanupPresent();
+ }
+ );
+ } else {
+ const spring = toProps?.spring;
+ iOSNativeHelper.animateWithSpring({
+ tension: isNumber(spring?.tension) ? spring?.tension : DEFAULT_SPRING.tension,
+ friction: isNumber(spring?.friction) ? spring?.tension : DEFAULT_SPRING.friction,
+ animations: () => {
+ animateProperties();
- updateFramePresent();
- },
- () => {
- cleanupPresent();
- }
- );
+ updateFramePresent();
+ },
+ completion: () => {
+ cleanupPresent();
+ },
+ });
+ }
break;
}
case UINavigationControllerOperation.Pop: {
// this.presented = fromViewCtrl;
- console.log('-- Transition dismiss --', this.presented);
+ // console.log('-- Transition dismiss --', this.presented);
transitionContext.containerView.insertSubviewBelowSubview(toViewCtrl.view, fromViewCtrl.view);
// console.log('transitionContext.containerView.subviews.count:', transitionContext.containerView.subviews.count);
- console.log(' ');
- console.log(
- `1. Dismiss sharedTransitionTags to animate:`,
- this.sharedElements.presented.map((p) => p.view.sharedTransitionTag)
- );
+ if (SharedTransition.DEBUG) {
+ console.log(' ');
+ console.log(
+ `1. Dismiss sharedTransitionTags to animate:`,
+ this.sharedElements.presented.map((p) => p.view.sharedTransitionTag)
+ );
- console.log(`2. Add back previously stored sharedElements to dismiss:`);
+ console.log(`2. Add back previously stored sharedElements to dismiss:`);
+ }
for (const p of this.sharedElements.presented) {
p.view.opacity = 0;
@@ -228,38 +242,59 @@ export class PageTransition extends Transition {
transitionContext.completeTransition(true);
};
const updateFrameDismiss = () => {
- console.log('3. Dismissing shared elements:');
+ if (SharedTransition.DEBUG) {
+ console.log('3. Dismissing shared elements:');
+ }
for (const presenting of this.sharedElements.presenting) {
- iosMatchLayerProperties(presenting.snapshot, presenting.view.ios);
+ iOSNativeHelper.copyLayerProperties(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));
+ if (SharedTransition.DEBUG) {
+ console.log(`---> ${presenting.view.sharedTransitionTag} animate to: `, iOSNativeHelper.printCGRect(presenting.startFrame));
+ }
}
- console.log(' ');
};
- UIView.animateWithDurationDelayUsingSpringWithDampingInitialSpringVelocityOptionsAnimationsCompletion(
- typeof state.fromPageEnd?.duration === 'number' ? state.fromPageEnd?.duration / 1000 : DEFAULT_DURATION,
- 0,
- 0.5,
- 3,
- UIViewAnimationOptions.CurveEaseInOut,
- () => {
- this.presented.view.alpha = typeof state.fromPageEnd?.opacity === 'number' ? state.fromPageEnd?.opacity : 0;
+ const props = state.fromPageEnd;
- const endX = typeof state.fromPageEnd?.x === 'number' ? state.fromPageEnd?.x : Screen.mainScreen.widthDIPs;
- const endY = typeof state.fromPageEnd?.y === 'number' ? state.fromPageEnd?.y : 0;
- const endWidth = typeof state.fromPageEnd?.width === 'number' ? state.fromPageEnd?.width : Screen.mainScreen.widthDIPs;
- const endHeight = typeof state.fromPageEnd?.height === 'number' ? state.fromPageEnd?.height : Screen.mainScreen.heightDIPs;
- this.presented.view.frame = CGRectMake(endX, endY, endWidth, endHeight);
+ const animateProperties = () => {
+ this.presented.view.alpha = isNumber(props?.opacity) ? props?.opacity : 0;
- updateFrameDismiss();
- },
- () => {
- cleanupDismiss();
- }
- );
+ const endX = isNumber(props?.x) ? props?.x : Screen.mainScreen.widthDIPs;
+ const endY = isNumber(props?.y) ? props?.y : 0;
+ const endWidth = isNumber(props?.width) ? props?.width : Screen.mainScreen.widthDIPs;
+ const endHeight = isNumber(props?.height) ? props?.height : Screen.mainScreen.heightDIPs;
+ this.presented.view.frame = CGRectMake(endX, endY, endWidth, endHeight);
+ };
+
+ if (isNumber(props?.duration)) {
+ // override spring and use only linear animation
+ UIView.animateWithDurationAnimationsCompletion(
+ props?.duration / 1000,
+ () => {
+ animateProperties();
+ updateFrameDismiss();
+ },
+ () => {
+ cleanupDismiss();
+ }
+ );
+ } else {
+ const spring = props?.spring;
+ iOSNativeHelper.animateWithSpring({
+ tension: isNumber(spring?.tension) ? spring?.tension : DEFAULT_SPRING.tension,
+ friction: isNumber(spring?.friction) ? spring?.tension : DEFAULT_SPRING.friction,
+ animations: () => {
+ animateProperties();
+
+ updateFrameDismiss();
+ },
+ completion: () => {
+ cleanupDismiss();
+ },
+ });
+ }
break;
}
}
diff --git a/packages/core/ui/transition/shared-transition.ts b/packages/core/ui/transition/shared-transition.ts
index 4d9c0dc9c..d9f33e241 100644
--- a/packages/core/ui/transition/shared-transition.ts
+++ b/packages/core/ui/transition/shared-transition.ts
@@ -3,6 +3,10 @@ import { querySelectorAll, ViewBase } from '../core/view-base';
import type { View } from '../core/view';
export const DEFAULT_DURATION = 0.35;
+export const DEFAULT_SPRING = {
+ tension: 140,
+ friction: 10,
+};
// always increment when adding new transitions to be able to track their state
export enum SharedTransitionAnimationType {
present,
@@ -45,26 +49,55 @@ type SharedTransitionPageProperties = {
height?: number;
opacity?: number;
/**
- * Duration in milliseconds
+ * Linear duration in milliseconds
+ * Note: When this is defined, it will override spring options and use only linear animation.
*/
duration?: number;
+ /**
+ * Spring animation settings.
+ * Defaults to 140 tension with 10 friction.
+ */
+ spring?: {
+ tension?: number;
+ friction?: number;
+ mass?: number;
+ delay?: number;
+ velocity?: number;
+ animateOptions?: any /* ios only: UIViewAnimationOptions */;
+ };
};
/**
- * Shared Element Transitions (experimental)
- *
- * Note: some APIs may change in future releases
+ * Shared Element Transitions (preview)
+ * Allows you to auto animate between shared elements on two different screesn to create smooth navigational experiences.
+ * View components can define sharedTransitionTag="name" alone with a transition through this API.
*/
export class SharedTransition {
- static custom(instance: Transition, options: SharedTransitionConfig): { instance: Transition } {
- SharedTransition.updateState(instance.id, {
+ /**
+ * Configure a custom transition with presentation/dismissal options.
+ * @param transition The custom Transition instance.
+ * @param options
+ * @returns a configured SharedTransition instance for use with navigational APIs.
+ */
+ static custom(transition: Transition, options: SharedTransitionConfig): { instance: Transition } {
+ SharedTransition.updateState(transition.id, {
...options,
- instance,
+ instance: transition,
activeType: SharedTransitionAnimationType.present,
});
- return { instance };
+ return { instance: transition };
}
+ /**
+ * Enable to see various console logging output of Shared Element Transition behavior.
+ */
+ static DEBUG = false;
+ /**
+ * @private
+ */
static currentStack: Array;
+ /**
+ * @private
+ */
static updateState(id: number, state: SharedTransitionState) {
if (!SharedTransition.currentStack) {
SharedTransition.currentStack = [];
@@ -80,15 +113,27 @@ export class SharedTransition {
SharedTransition.currentStack.push(state);
}
}
+ /**
+ * @private
+ */
static getState(id: number) {
return SharedTransition.currentStack.find((t) => t.instance.id === id);
}
+ /**
+ * @private
+ */
static finishState(id: number) {
const index = SharedTransition.currentStack.findIndex((t) => t.instance.id === id);
if (index > -1) {
SharedTransition.currentStack.splice(index, 1);
}
}
+ /**
+ * Gather view collections based on sharedTransitionTag details.
+ * @param fromPage Page moving away from
+ * @param toPage Page moving to
+ * @returns Collections of views pertaining to shared elements or particular pages
+ */
static getSharedElements(
fromPage: ViewBase,
toPage: ViewBase
diff --git a/packages/core/ui/transition/slide-transition.android.ts b/packages/core/ui/transition/slide-transition.android.ts
index 901381715..675fbaf9a 100644
--- a/packages/core/ui/transition/slide-transition.android.ts
+++ b/packages/core/ui/transition/slide-transition.android.ts
@@ -8,7 +8,7 @@ const screenHeight = lazy(() => Screen.mainScreen.heightPixels);
export class SlideTransition extends Transition {
private _direction: string;
- constructor(direction: string, duration: number, curve: any) {
+ constructor(direction: string, duration: number = 350, curve?: any) {
super(duration, curve);
this._direction = direction;
}
diff --git a/packages/core/ui/transition/slide-transition.d.ts b/packages/core/ui/transition/slide-transition.d.ts
index 16ec4490c..903aee2d0 100644
--- a/packages/core/ui/transition/slide-transition.d.ts
+++ b/packages/core/ui/transition/slide-transition.d.ts
@@ -1,5 +1,5 @@
import { Transition } from '.';
export class SlideTransition extends Transition {
- constructor(direction: string, duration: number, nativeCurve: any);
+ constructor(direction: string, duration?: number, nativeCurve?: any);
}
diff --git a/packages/core/utils/native-helper.d.ts b/packages/core/utils/native-helper.d.ts
index 11ec7d9e4..cf93366e2 100644
--- a/packages/core/utils/native-helper.d.ts
+++ b/packages/core/utils/native-helper.d.ts
@@ -231,4 +231,39 @@ export namespace iOSNativeHelper {
* Checks whether the application is running on real device and not on simulator.
*/
export function isRealDevice(): boolean;
+
+ /**
+ * Debug utility to insert CGRect values into logging output.
+ * Note: when printing a CGRect directly it will print blank so this helps show the values.
+ * @param rect CGRect
+ */
+ export function printCGRect(rect: CGRect): void;
+
+ /**
+ * Take a snapshot of a View on screen.
+ * @param view view to snapshot
+ * @param scale screen scale
+ */
+ export function snapshotView(view: UIView, scale: number): UIImage;
+
+ /**
+ * Copy layer properties from one view to another.
+ * @param view a view to copy layer properties to
+ * @param toView a view to copy later properties from
+ */
+ export function copyLayerProperties(view: UIView, toView: UIView): void;
+
+ /**
+ * Animate views with a configurable spring effect
+ * @param options various animation settings for the spring
+ * - tension: number
+ * - friction: number
+ * - mass: number
+ * - delay: number
+ * - velocity: number
+ * - animateOptions: UIViewAnimationOptions
+ * - animations: () => void, Callback containing the property changes you want animated
+ * - completion: (finished: boolean) => void, Callback when animation is finished
+ */
+ export function animateWithSpring(options?: { tension?: number; friction?: number; mass?: number; delay?: number; velocity?: number; animateOptions?: UIViewAnimationOptions; animations?: () => void; completion?: (finished?: boolean) => void });
}
diff --git a/packages/core/utils/native-helper.ios.ts b/packages/core/utils/native-helper.ios.ts
index 0c73a9495..f8a5c2a43 100644
--- a/packages/core/utils/native-helper.ios.ts
+++ b/packages/core/utils/native-helper.ios.ts
@@ -355,4 +355,84 @@ export namespace iOSNativeHelper {
return true;
}
}
+
+ export function printCGRect(rect: CGRect) {
+ if (rect) {
+ return `CGRect(${rect.origin.x} ${rect.origin.y} ${rect.size.width} ${rect.size.height})`;
+ }
+ }
+
+ export function snapshotView(view: UIView, scale: number): 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, scale);
+ view.layer.renderInContext(UIGraphicsGetCurrentContext());
+ const image = UIGraphicsGetImageFromCurrentImageContext();
+ UIGraphicsEndImageContext();
+ return image;
+ }
+
+ export function copyLayerProperties(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 animateWithSpring(options?: { tension?: number; friction?: number; mass?: number; delay?: number; velocity?: number; animateOptions?: UIViewAnimationOptions; animations?: () => void; completion?: (finished?: boolean) => void }) {
+ const opt = {
+ tension: 140,
+ friction: 10,
+ mass: 1.0,
+ delay: 0,
+ velocity: 0,
+ animateOptions: null,
+ animations: null,
+ completion: null,
+ ...(options || {}),
+ };
+
+ // console.log('createSpringAnimator', opt);
+ const damping = opt.friction / Math.sqrt(2 * opt.tension);
+ const undampedFrequency = Math.sqrt(opt.tension / opt.mass);
+
+ // console.log({
+ // damping,
+ // undampedFrequency
+ // })
+
+ const epsilon = 0.001;
+ let duration = 0;
+
+ if (damping < 1) {
+ // console.log('damping < 1');
+ const a = Math.sqrt(1 - Math.pow(damping, 2));
+ const b = opt.velocity / (a * undampedFrequency);
+ const c = damping / a;
+ const d = -((b - c) / epsilon);
+ if (d > 0) {
+ duration = Math.log(d) / (damping * undampedFrequency);
+ }
+ }
+
+ if (duration === 0) {
+ UIView.animateWithDurationAnimationsCompletion(0, opt.animations, opt.completion);
+ return;
+ }
+ UIView.animateWithDurationDelayUsingSpringWithDampingInitialSpringVelocityOptionsAnimationsCompletion(duration, opt.delay, damping, opt.velocity, opt.animateOptions, opt.animations, opt.completion);
+ }
}