diff --git a/apps/toolbox/src/app-root.xml b/apps/toolbox/src/app-root.xml
index aec46ed6f..54e70d976 100644
--- a/apps/toolbox/src/app-root.xml
+++ b/apps/toolbox/src/app-root.xml
@@ -1,2 +1,2 @@
-
+
diff --git a/packages/core/ui/index.ts b/packages/core/ui/index.ts
index 21f6de033..6b724547a 100644
--- a/packages/core/ui/index.ts
+++ b/packages/core/ui/index.ts
@@ -80,5 +80,6 @@ export { PageTransition } from './transition/page-transition';
export { FadeTransition } from './transition/fade-transition';
export { SlideTransition } from './transition/slide-transition';
export { SharedTransition, SharedTransitionAnimationType } from './transition/shared-transition';
+export type { SharedTransitionConfig } from './transition/shared-transition';
export { WebView } from './web-view';
export type { LoadEventData, WebViewNavigationType } from './web-view';
diff --git a/packages/core/ui/transition/index.d.ts b/packages/core/ui/transition/index.d.ts
index 85a01707d..ca0979e4d 100644
--- a/packages/core/ui/transition/index.d.ts
+++ b/packages/core/ui/transition/index.d.ts
@@ -1,7 +1,26 @@
-import type { BackstackEntry } from '../frame';
+import type { View } from '../core/view';
+import type { BackstackEntry } from '../frame';
+export type SharedElementSettings = { view: View; startFrame: any; endFrame?: any; startOpacity?: number; endOpacity?: number; scale?: { x?: number; y?: number }; startTransform?: any; snapshot?: any };
+export type SharedTransitionType = 'page' | 'modal';
+export interface SharedInteractiveState {
+ started?: false;
+ added?: boolean;
+ transitionContext?: any;
+ propertyAnimator?: any;
+}
export declare class Transition {
id: number;
+ transitionController?: any;
+ interactiveController?: any;
+ presented?: any;
+ presenting?: any;
+ sharedElements?: {
+ presented?: Array;
+ presenting?: Array;
+ // independent sharedTransitionTags which are part of the shared transition but only on one page
+ independent?: Array;
+ };
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;
diff --git a/packages/core/ui/transition/modal-transition.ios.ts b/packages/core/ui/transition/modal-transition.ios.ts
index af8a790dc..5f0b444ca 100644
--- a/packages/core/ui/transition/modal-transition.ios.ts
+++ b/packages/core/ui/transition/modal-transition.ios.ts
@@ -2,10 +2,10 @@ import type { View } from '../core/view';
import { Screen } from '../../platform';
import { iOSNativeHelper } from '../../utils/native-helper';
import { isNumber } from '../../utils/types';
-import { Transition } from '.';
-import { SharedTransition, SharedTransitionAnimationType, DEFAULT_DURATION, DEFAULT_SPRING, getRectFromProps } from './shared-transition';
+import { Transition, SharedElementSettings, SharedInteractiveState } from '.';
+import { SharedTransition, SharedTransitionAnimationType, DEFAULT_DURATION, DEFAULT_SPRING, getRectFromProps, getPageStartDefaultsForType } from './shared-transition';
+import { SharedTransitionHelper } from './shared-transition-helper';
import { PanGestureEventData, GestureStateTypes } from '../gestures';
-type SharedElementSettings = { view: View; startFrame: CGRect; endFrame?: CGRect; startOpacity?: number; endOpacity?: number; scale?: { x?: number; y?: number }; startTransform?: CGAffineTransform; snapshot?: UIImageView };
export class ModalTransition extends Transition {
transitionController: ModalTransitionController;
@@ -59,10 +59,6 @@ export class ModalTransition extends Transition {
private _interactiveDismissGestureHandler(args: PanGestureEventData) {
if (args?.ios?.view) {
const state = SharedTransition.getState(this.id);
- // SharedTransition.updateState(this.id, {
- // interactiveBegan: true,
- // interactiveCancelled: false
- // });
const percent = state.interactive?.dismiss?.percentFormula ? state.interactive.dismiss.percentFormula(args) : args.deltaY / (args.ios.view.bounds.size.height / 2);
if (SharedTransition.DEBUG) {
console.log('Interactive dismissal percentage:', percent);
@@ -107,9 +103,7 @@ export class ModalTransition extends Transition {
class PercentInteractiveController extends UIPercentDrivenInteractiveTransition implements UIViewControllerInteractiveTransitioning {
static ObjCProtocols = [UIViewControllerInteractiveTransitioning];
owner: WeakRef;
- started = false;
- transitionContext: UIViewControllerContextTransitioning;
- backgroundAnimation: UIViewPropertyAnimator;
+ interactiveState: SharedInteractiveState;
static initWithOwner(owner: WeakRef) {
const ctrl = PercentInteractiveController.new();
@@ -119,93 +113,41 @@ class PercentInteractiveController extends UIPercentDrivenInteractiveTransition
startInteractiveTransition(transitionContext: UIViewControllerContextTransitioning) {
// console.log('startInteractiveTransition');
- this.transitionContext = transitionContext;
+ if (!this.interactiveState) {
+ this.interactiveState = {
+ transitionContext,
+ };
+ const owner = this.owner?.deref();
+ if (owner) {
+ const state = SharedTransition.getState(owner.id);
+ SharedTransitionHelper.interactiveStart(state, this.interactiveState, 'modal');
+ }
+ }
}
updateInteractiveTransition(percentComplete: number) {
const owner = 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);
- }
- const state = SharedTransition.getState(owner.id);
- const toProps = state.pageStart;
- 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;
- iOSNativeHelper.copyLayerProperties(p.snapshot, p.view.ios);
-
- p.snapshot.alpha = 1;
- }
- owner.presented.view.alpha = 0;
- owner.presented.view.frame = CGRectMake(0, startY, owner.presented.view.bounds.size.width, owner.presented.view.bounds.size.height);
- });
- }
-
- this.backgroundAnimation.fractionComplete = percentComplete;
+ const state = SharedTransition.getState(owner.id);
+ SharedTransitionHelper.interactiveUpdate(state, this.interactiveState, 'modal', percentComplete);
}
}
cancelInteractiveTransition() {
// console.log('cancelInteractiveTransition');
const owner = this.owner?.deref();
- if (owner && this.started) {
+ if (owner) {
const state = SharedTransition.getState(owner.id);
- if (!state) {
- return;
- }
- if (this.backgroundAnimation) {
- this.backgroundAnimation.reversed = true;
- const duration = isNumber(state.pageStart?.duration) ? state.pageStart?.duration / 1000 : DEFAULT_DURATION;
- 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.cancelInteractiveTransition();
- this.transitionContext.completeTransition(false);
- }, duration * 1000);
- }
+ SharedTransitionHelper.interactiveCancel(state, this.interactiveState, 'modal');
}
}
finishInteractiveTransition() {
// console.log('finishInteractiveTransition');
const owner = this.owner?.deref();
- if (owner && this.started) {
- if (this.backgroundAnimation) {
- this.backgroundAnimation.reversed = false;
- const state = SharedTransition.getState(owner.id);
- if (!state) {
- SharedTransition.finishState(owner.id);
- this.transitionContext.completeTransition(true);
- return;
- }
-
- const duration = isNumber(state.pageReturn?.duration) ? state.pageReturn?.duration / 1000 : DEFAULT_DURATION;
- this.backgroundAnimation.continueAnimationWithTimingParametersDurationFactor(null, duration);
- setTimeout(() => {
- for (const presenting of owner.sharedElements.presenting) {
- presenting.view.opacity = presenting.startOpacity;
- }
- SharedTransition.finishState(owner.id);
- this.transitionContext.finishInteractiveTransition();
- this.transitionContext.completeTransition(true);
- }, duration * 1000);
- }
+ if (owner) {
+ const state = SharedTransition.getState(owner.id);
+ SharedTransitionHelper.interactiveFinish(state, this.interactiveState, 'modal');
}
}
}
@@ -234,396 +176,7 @@ class ModalTransitionController extends NSObject implements UIViewControllerAnim
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();
-
- const { sharedElements, presented, presenting } = SharedTransition.getSharedElements(state.page, state.toPage);
-
- if (SharedTransition.DEBUG) {
- console.log(' ModalTransition: Present');
- 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:`);
- }
-
- const pageStart = state.pageStart;
- owner.presented.view.alpha = isNumber(pageStart?.opacity) ? pageStart?.opacity : 0;
-
- const startFrame = getRectFromProps(pageStart, {
- x: 0,
- y: Screen.mainScreen.heightDIPs,
- width: Screen.mainScreen.widthDIPs,
- height: Screen.mainScreen.heightDIPs,
- });
-
- const pageEnd = state.pageEnd;
- const pageEndIndependentTags = Object.keys(pageEnd?.sharedTransitionTags || {});
- console.log('pageEndIndependentTags:', pageEndIndependentTags);
-
- for (const presentingView of sharedElements) {
- if (!owner.sharedElements) {
- owner.sharedElements = {
- presented: [],
- presenting: [],
- independent: [],
- };
- }
- const presentingSharedElement = presentingView.ios;
- // console.log('fromTarget instanceof UIImageView:', fromTarget instanceof UIImageView)
-
- // TODO: discuss whether we should check if UIImage/UIImageView type to always snapshot images or if other view types could be duped/added vs. snapshotted
- // Note: snapshot may be most efficient/simple
- // console.log('---> ', presentingView.sharedTransitionTag, ': ', presentingSharedElement)
-
- const presentedView = presented.find((v) => v.sharedTransitionTag === presentingView.sharedTransitionTag);
- const presentedSharedElement = presentedView.ios;
-
- const snapshot = UIImageView.alloc().init();
-
- // treat images differently...
- if (presentedSharedElement instanceof UIImageView) {
- // in case the image is loaded async, we need to update the snapshot when it changes
- // todo: remove listener on transition end
- presentedView.on('imageSourceChange', () => {
- snapshot.image = iOSNativeHelper.snapshotView(presentedSharedElement, Screen.mainScreen.scale);
- snapshot.tintColor = presentedSharedElement.tintColor;
- });
-
- snapshot.tintColor = presentedSharedElement.tintColor;
- snapshot.contentMode = presentedSharedElement.contentMode;
- }
-
- iOSNativeHelper.copyLayerProperties(snapshot, presentingSharedElement);
- snapshot.clipsToBounds = true;
- // console.log('---> snapshot: ', snapshot);
-
- const startFrame = presentingSharedElement.convertRectToView(presentingSharedElement.bounds, transitionContext.containerView);
- const endFrame = presentedSharedElement.convertRectToView(presentedSharedElement.bounds, transitionContext.containerView);
- snapshot.frame = startFrame;
- if (SharedTransition.DEBUG) {
- console.log('---> ', presentingView.sharedTransitionTag, ' frame:', iOSNativeHelper.printCGRect(snapshot.frame));
- }
-
- owner.sharedElements.presenting.push({
- view: presentingView,
- startFrame,
- endFrame,
- snapshot,
- startOpacity: presentingView.opacity,
- endOpacity: presentedView.opacity,
- });
- owner.sharedElements.presented.push({
- view: presentedView,
- startFrame: endFrame,
- endFrame: startFrame,
- startOpacity: presentedView.opacity,
- endOpacity: presentingView.opacity,
- });
-
- // set initial opacity to match the source view opacity
- snapshot.alpha = presentingView.opacity;
- // hide both while animating within the transition context
- presentingView.opacity = 0;
- presentedView.opacity = 0;
- // add snapshot to animate
- transitionContext.containerView.addSubview(snapshot);
- }
-
- for (const tag of pageEndIndependentTags) {
- // only consider start when there's a matching end
- const pageStartIndependentProps = pageStart?.sharedTransitionTags[tag];
- console.log('start:', tag, pageStartIndependentProps);
- const pageEndIndependentProps = pageEnd?.sharedTransitionTags[tag];
- let independentView = presenting.find((v) => v.sharedTransitionTag === tag);
- let isPresented = false;
- if (!independentView) {
- independentView = presented.find((v) => v.sharedTransitionTag === tag);
- if (!independentView) {
- break;
- }
- isPresented = true;
- }
- const independentSharedElement: UIView = independentView.ios;
-
- let snapshot: UIImageView;
- if (isPresented) {
- snapshot = UIImageView.alloc().init();
- } else {
- snapshot = UIImageView.alloc().initWithImage(iOSNativeHelper.snapshotView(independentSharedElement, Screen.mainScreen.scale));
- }
-
- if (independentSharedElement instanceof UIImageView) {
- // in case the image is loaded async, we need to update the snapshot when it changes
- // todo: remove listener on transition end
- if (isPresented) {
- independentView.on('imageSourceChange', () => {
- snapshot.image = iOSNativeHelper.snapshotView(independentSharedElement, Screen.mainScreen.scale);
- snapshot.tintColor = independentSharedElement.tintColor;
- });
- }
-
- snapshot.tintColor = independentSharedElement.tintColor;
- snapshot.contentMode = independentSharedElement.contentMode;
- }
- snapshot.clipsToBounds = true;
-
- const startFrame = independentSharedElement.convertRectToView(independentSharedElement.bounds, transitionContext.containerView);
- const startFrameRect = getRectFromProps(pageStartIndependentProps);
- // adjust for any specified start positions
- const startFrameAdjusted = CGRectMake(startFrame.origin.x + startFrameRect.x, startFrame.origin.y + startFrameRect.y, startFrame.size.width, startFrame.size.height);
- console.log('startFrameAdjusted:', tag, iOSNativeHelper.printCGRect(startFrameAdjusted));
- // if (pageStartIndependentProps?.scale) {
- // snapshot.transform = CGAffineTransformConcat(CGAffineTransformMakeTranslation(startFrameAdjusted.origin.x, startFrameAdjusted.origin.y), CGAffineTransformMakeScale(pageStartIndependentProps.scale.x, pageStartIndependentProps.scale.y))
- // } else {
- snapshot.frame = startFrameAdjusted;
- // }
- if (SharedTransition.DEBUG) {
- console.log('---> ', independentView.sharedTransitionTag, ' frame:', iOSNativeHelper.printCGRect(snapshot.frame));
- }
-
- const endFrameRect = getRectFromProps(pageEndIndependentProps);
-
- const endFrame = CGRectMake(startFrame.origin.x + endFrameRect.x, startFrame.origin.y + endFrameRect.y, startFrame.size.width, startFrame.size.height);
- console.log('endFrame:', tag, iOSNativeHelper.printCGRect(endFrame));
- owner.sharedElements.independent.push({
- view: independentView,
- isPresented,
- startFrame,
- snapshot,
- endFrame,
- startTransform: independentSharedElement.transform,
- scale: pageEndIndependentProps.scale,
- startOpacity: independentView.opacity,
- endOpacity: isNumber(pageEndIndependentProps.opacity) ? pageEndIndependentProps.opacity : 0,
- });
-
- independentView.opacity = 0;
- // add snapshot to animate
- transitionContext.containerView.addSubview(snapshot);
- }
-
- // Important: always set after above shared element positions have had their start positions set
- owner.presented.view.frame = CGRectMake(startFrame.x, startFrame.y, startFrame.width, startFrame.height);
-
- const cleanupPresent = () => {
- for (const presented of owner.sharedElements.presented) {
- presented.view.opacity = presented.startOpacity;
- }
- for (const presenting of owner.sharedElements.presenting) {
- presenting.snapshot.removeFromSuperview();
- }
- for (const independent of owner.sharedElements.independent) {
- independent.snapshot.removeFromSuperview();
- if (independent.isPresented) {
- independent.view.opacity = independent.startOpacity;
- }
- }
- SharedTransition.updateState(owner.id, {
- activeType: SharedTransitionAnimationType.dismiss,
- });
- transitionContext.completeTransition(true);
- };
-
- const animateProperties = () => {
- if (SharedTransition.DEBUG) {
- console.log('3. Animating shared elements:');
- }
- owner.presented.view.alpha = isNumber(pageEnd?.opacity) ? pageEnd?.opacity : 1;
-
- const endFrame = getRectFromProps(pageEnd);
- owner.presented.view.frame = CGRectMake(endFrame.x, endFrame.y, endFrame.width, endFrame.height);
-
- // animate page properties to the following:
- // 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();
-
- for (const presented of owner.sharedElements.presented) {
- const presentingMatch = owner.sharedElements.presenting.find((v) => v.view.sharedTransitionTag === presented.view.sharedTransitionTag);
- // Workaround wrong origin due ongoing layout process.
- const updatedEndFrame = presented.view.ios.convertRectToView(presented.view.ios.bounds, transitionContext.containerView);
- const correctedEndFrame = CGRectMake(updatedEndFrame.origin.x, updatedEndFrame.origin.y, presentingMatch.endFrame.size.width, presentingMatch.endFrame.size.height);
- presentingMatch.snapshot.frame = correctedEndFrame;
-
- // apply view and layer properties to the snapshot view to match the source/presented view
- iOSNativeHelper.copyLayerProperties(presentingMatch.snapshot, presented.view.ios);
- // create a snapshot of the presented view
- presentingMatch.snapshot.image = iOSNativeHelper.snapshotView(presented.view.ios, Screen.mainScreen.scale);
- // apply correct alpha
- presentingMatch.snapshot.alpha = presentingMatch.endOpacity;
-
- if (SharedTransition.DEBUG) {
- console.log(`---> ${presentingMatch.view.sharedTransitionTag} animate to: `, iOSNativeHelper.printCGRect(correctedEndFrame));
- }
- }
- for (const independent of owner.sharedElements.independent) {
- let endFrame: CGRect = independent.endFrame;
- // if (independent.isPresented) {
- // const updatedEndFrame = independent.view.ios.convertRectToView(independent.view.ios.bounds, transitionContext.containerView);
- // endFrame = CGRectMake(updatedEndFrame.origin.x, updatedEndFrame.origin.y, independent.endFrame.size.width, independent.endFrame.size.height);
- // }
- if (independent.scale) {
- independent.snapshot.transform = CGAffineTransformConcat(CGAffineTransformMakeTranslation(endFrame.origin.x, endFrame.origin.y), CGAffineTransformMakeScale(independent.scale.x, independent.scale.y));
- } else {
- independent.snapshot.frame = endFrame;
- }
- independent.snapshot.alpha = independent.endOpacity;
-
- if (SharedTransition.DEBUG) {
- console.log(`---> ${independent.view.sharedTransitionTag} animate to: `, iOSNativeHelper.printCGRect(independent.endFrame));
- }
- }
- };
-
- if (isNumber(pageEnd?.duration)) {
- // override spring and use only linear animation
- UIView.animateWithDurationAnimationsCompletion(
- pageEnd?.duration / 1000,
- () => {
- animateProperties();
- },
- () => {
- cleanupPresent();
- }
- );
- } else {
- const spring = pageEnd?.spring;
- iOSNativeHelper.animateWithSpring({
- tension: isNumber(spring?.tension) ? spring?.tension : DEFAULT_SPRING.tension,
- friction: isNumber(spring?.friction) ? spring?.friction : DEFAULT_SPRING.friction,
- mass: isNumber(spring?.mass) ? spring?.mass : DEFAULT_SPRING.mass,
- velocity: isNumber(spring?.velocity) ? spring?.velocity : DEFAULT_SPRING.velocity,
- delay: isNumber(spring?.delay) ? spring?.delay : DEFAULT_SPRING.delay,
- animations: () => {
- animateProperties();
- },
- completion: () => {
- cleanupPresent();
- },
- });
- }
-
- break;
- }
- case SharedTransitionAnimationType.dismiss: {
- // console.log('-- Transition dismiss --');
-
- // console.log('transitionContext.containerView.subviews.count:', transitionContext.containerView.subviews.count);
-
- if (SharedTransition.DEBUG) {
- console.log(' ModalTransition: Dismiss');
- console.log(
- `1. Dismiss sharedTransitionTags to animate:`,
- owner.sharedElements.presented.map((p) => p.view.sharedTransitionTag)
- );
-
- console.log(`2. Add back previously stored sharedElements to dismiss:`);
- }
-
- for (const p of owner.sharedElements.presented) {
- p.view.opacity = 0;
- }
- for (const p of owner.sharedElements.presenting) {
- p.snapshot.alpha = p.endOpacity;
- transitionContext.containerView.addSubview(p.snapshot);
- }
- for (const independent of owner.sharedElements.independent) {
- independent.snapshot.alpha = independent.endOpacity;
- transitionContext.containerView.addSubview(independent.snapshot);
- }
-
- const pageReturn = state.pageReturn;
-
- const cleanupDismiss = () => {
- for (const presenting of owner.sharedElements.presenting) {
- presenting.view.opacity = presenting.startOpacity;
- }
- for (const independent of owner.sharedElements.independent) {
- independent.view.opacity = independent.startOpacity;
- }
- SharedTransition.finishState(owner.id);
- owner.sharedElements = null;
- transitionContext.completeTransition(true);
- };
-
- const animateProperties = () => {
- if (SharedTransition.DEBUG) {
- console.log('3. Dismissing shared elements:');
- }
-
- owner.presented.view.alpha = isNumber(pageReturn?.opacity) ? pageReturn?.opacity : 0;
-
- const endFrame = getRectFromProps(pageReturn, {
- x: 0,
- y: Screen.mainScreen.heightDIPs,
- width: Screen.mainScreen.widthDIPs,
- height: Screen.mainScreen.heightDIPs,
- });
- owner.presented.view.frame = CGRectMake(endFrame.x, endFrame.y, endFrame.width, endFrame.height);
-
- for (const presenting of owner.sharedElements.presenting) {
- iOSNativeHelper.copyLayerProperties(presenting.snapshot, presenting.view.ios);
- presenting.snapshot.frame = presenting.startFrame;
- presenting.snapshot.alpha = presenting.startOpacity;
-
- if (SharedTransition.DEBUG) {
- console.log(`---> ${presenting.view.sharedTransitionTag} animate to: `, iOSNativeHelper.printCGRect(presenting.startFrame));
- }
- }
-
- for (const independent of owner.sharedElements.independent) {
- independent.snapshot.alpha = independent.startOpacity;
- if (independent.scale) {
- independent.snapshot.transform = independent.startTransform;
- } else {
- independent.snapshot.frame = independent.startFrame;
- }
-
- if (SharedTransition.DEBUG) {
- console.log(`---> ${independent.view.sharedTransitionTag} animate to: `, iOSNativeHelper.printCGRect(independent.startFrame));
- }
- }
- };
-
- if (isNumber(pageReturn?.duration)) {
- // override spring and use only linear animation
- UIView.animateWithDurationAnimationsCompletion(
- pageReturn?.duration / 1000,
- () => {
- animateProperties();
- },
- () => {
- cleanupDismiss();
- }
- );
- } else {
- const spring = pageReturn?.spring;
- iOSNativeHelper.animateWithSpring({
- tension: isNumber(spring?.tension) ? spring?.tension : DEFAULT_SPRING.tension,
- friction: isNumber(spring?.friction) ? spring?.friction : DEFAULT_SPRING.friction,
- animations: () => {
- animateProperties();
- },
- completion: () => {
- cleanupDismiss();
- },
- });
- }
-
- break;
- }
- }
+ SharedTransitionHelper.animate(state, transitionContext, 'modal');
}
}
}
diff --git a/packages/core/ui/transition/page-transition.ios.ts b/packages/core/ui/transition/page-transition.ios.ts
index 786c9180d..0597227c0 100644
--- a/packages/core/ui/transition/page-transition.ios.ts
+++ b/packages/core/ui/transition/page-transition.ios.ts
@@ -1,10 +1,11 @@
import type { View } from '../core/view';
-import { Transition } from '.';
+import { SharedElementSettings, SharedInteractiveState, Transition } from '.';
import { Screen } from '../../platform';
import { iOSNativeHelper } from '../../utils/native-helper';
import { isNumber } from '../../utils/types';
import { PanGestureEventData, GestureStateTypes } from '../gestures';
-import { SharedTransition, SharedTransitionAnimationType, DEFAULT_DURATION, DEFAULT_SPRING } from './shared-transition';
+import { SharedTransition, SharedTransitionAnimationType, DEFAULT_DURATION, DEFAULT_SPRING, getRectFromProps, getPageStartDefaultsForType } from './shared-transition';
+import { SharedTransitionHelper } from './shared-transition-helper';
export class PageTransition extends Transition {
transitionController: PageTransitionController;
@@ -14,8 +15,10 @@ export class PageTransition extends Transition {
navigationController: UINavigationController;
operation: number;
sharedElements: {
- presented?: Array<{ view: View; startFrame?: CGRect; endFrame?: CGRect; snapshot?: UIImageView; startOpacity?: number; endOpacity?: number }>;
- presenting?: Array<{ view: View; startFrame: CGRect; endFrame?: CGRect; snapshot?: UIImageView; startOpacity?: number; endOpacity?: number }>;
+ presented?: Array;
+ presenting?: Array;
+ // independent sharedTransitionTags which are part of the shared transition but only on one page
+ independent?: Array;
};
private _interactiveStartCallback: () => void;
private _interactiveDismissGesture: (args: any /*PanGestureEventData*/) => void;
@@ -52,7 +55,9 @@ export class PageTransition extends Transition {
this._interactiveGestureTeardown = () => {
// console.log(`-- TEARDOWN setupInteractiveGesture --`);
- view.off('pan', this._interactiveDismissGesture);
+ if (view) {
+ view.off('pan', this._interactiveDismissGesture);
+ }
this._interactiveDismissGesture = null;
};
return this._interactiveGestureTeardown;
@@ -62,6 +67,14 @@ export class PageTransition extends Transition {
if (args?.ios?.view) {
// console.log('this.id:', this.id);
const state = SharedTransition.getState(this.id);
+ if (!state) {
+ // cleanup and exit, already shutdown
+ if (this._interactiveGestureTeardown) {
+ this._interactiveGestureTeardown();
+ this._interactiveGestureTeardown = null;
+ }
+ return;
+ }
const percent = state.interactive?.dismiss?.percentFormula ? state.interactive.dismiss.percentFormula(args) : args.deltaX / (args.ios.view.bounds.size.width / 2);
if (SharedTransition.DEBUG) {
@@ -111,9 +124,7 @@ export class PageTransition extends Transition {
class PercentInteractiveController extends UIPercentDrivenInteractiveTransition implements UIViewControllerInteractiveTransitioning {
static ObjCProtocols = [UIViewControllerInteractiveTransitioning];
owner: WeakRef;
- started = false;
- transitionContext: UIViewControllerContextTransitioning;
- backgroundAnimation: UIViewPropertyAnimator;
+ interactiveState: SharedInteractiveState;
static initWithOwner(owner: WeakRef) {
const ctrl = PercentInteractiveController.new();
@@ -121,16 +132,16 @@ class PercentInteractiveController extends UIPercentDrivenInteractiveTransition
return ctrl;
}
- added = false;
startInteractiveTransition(transitionContext: UIViewControllerContextTransitioning) {
// console.log('startInteractiveTransition');
- this.transitionContext = transitionContext;
-
- if (!this.started) {
- this.started = true;
- const owner: any = this.owner?.deref();
+ if (!this.interactiveState) {
+ this.interactiveState = {
+ transitionContext,
+ };
+ const owner = this.owner?.deref();
if (owner) {
- this.transitionContext.containerView.insertSubviewBelowSubview(owner.presenting.view, owner.presented.view);
+ const state = SharedTransition.getState(owner.id);
+ SharedTransitionHelper.interactiveStart(state, this.interactiveState, 'page');
}
}
}
@@ -138,94 +149,26 @@ class PercentInteractiveController extends UIPercentDrivenInteractiveTransition
updateInteractiveTransition(percentComplete: number) {
const owner = this.owner?.deref();
if (owner) {
- if (!this.added) {
- this.added = 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);
- }
- const state = SharedTransition.getState(owner.id);
- const props = state.pageReturn;
- 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 = isNumber(props?.opacity) ? props?.opacity : 0;
- 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;
- owner.presented.view.frame = CGRectMake(endX, endY, endWidth, endHeight);
- });
- }
-
- this.backgroundAnimation.fractionComplete = percentComplete;
+ const state = SharedTransition.getState(owner.id);
+ SharedTransitionHelper.interactiveUpdate(state, this.interactiveState, 'page', percentComplete);
}
}
cancelInteractiveTransition() {
// console.log('cancelInteractiveTransition');
const owner = this.owner?.deref();
- if (owner && this.added) {
+ if (owner) {
const state = SharedTransition.getState(owner.id);
- if (!state) {
- return;
- }
- if (this.backgroundAnimation) {
- this.backgroundAnimation.reversed = true;
- const duration = isNumber(state.pageStart?.duration) ? state.pageStart?.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.added = false;
- this.transitionContext.cancelInteractiveTransition();
- this.transitionContext.completeTransition(false);
- }, duration * 1000);
- }
+ SharedTransitionHelper.interactiveCancel(state, this.interactiveState, 'page');
}
}
finishInteractiveTransition() {
// console.log('finishInteractiveTransition');
const owner = this.owner?.deref();
- if (owner && this.added) {
- if (this.backgroundAnimation) {
- this.backgroundAnimation.reversed = false;
- const state = SharedTransition.getState(owner.id);
- if (!state) {
- SharedTransition.finishState(owner.id);
- this.transitionContext.completeTransition(true);
- return;
- }
-
- const duration = isNumber(state.pageReturn?.duration) ? state.pageReturn?.duration / 1000 : 0.35;
- this.backgroundAnimation.continueAnimationWithTimingParametersDurationFactor(null, duration);
- setTimeout(() => {
- for (const presenting of owner.sharedElements.presenting) {
- presenting.view.opacity = presenting.startOpacity;
- presenting.snapshot.removeFromSuperview();
- }
-
- SharedTransition.finishState(owner.id);
- this.backgroundAnimation = null;
- this.added = false;
- this.transitionContext.finishInteractiveTransition();
- this.transitionContext.completeTransition(true);
- }, duration * 1000);
- }
+ if (owner) {
+ const state = SharedTransition.getState(owner.id);
+ SharedTransitionHelper.interactiveFinish(state, this.interactiveState, 'page');
}
}
}
@@ -253,287 +196,11 @@ class PageTransitionController extends NSObject implements UIViewControllerAnima
const owner = this.owner.deref();
if (owner) {
// console.log('--- PageTransitionController animateTransition');
- // console.log('owner.presented:', owner.presented);
- // console.log('owner.presenting:', owner.presenting);
-
- // console.log('owner.id:', owner.id);
const state = SharedTransition.getState(owner.id);
if (!state) {
return;
}
- // console.log('state.activeType:', state.activeType);
- // console.log('operation:', owner.operation);
- // switch (state.activeType) {
- switch (owner.operation) {
- case UINavigationControllerOperation.Push: {
- // console.log('pushing!!');
- // this.presented = toViewCtrl;
- // this.presenting = fromViewCtrl;
-
- transitionContext.containerView.insertSubviewAboveSubview(owner.presented.view, owner.presenting.view);
- owner.presented.view.layoutIfNeeded();
-
- const { sharedElements, presented } = SharedTransition.getSharedElements(state.page, state.toPage);
-
- if (SharedTransition.DEBUG) {
- console.log(' PageTransition: Push');
- 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:`);
- }
-
- for (const presentingView of sharedElements) {
- if (!owner.sharedElements) {
- owner.sharedElements = {
- presented: [],
- presenting: [],
- };
- }
- const presentingSharedElement = presentingView.ios;
-
- // TODO: discuss whether we should check if UIImage/UIImageView type to always snapshot images or if other view types could be duped/added vs. snapshotted
- // Note: snapshot may be most efficient/simple
- // console.log('---> ', presentingView.sharedTransitionTag, ': ', presentingSharedElement)
-
- const presentedView = presented.find((v) => v.sharedTransitionTag === presentingView.sharedTransitionTag);
- const presentedSharedElement = presentedView.ios;
-
- const sharedElementSnapshot = UIImageView.alloc().init(); // WithImage(snapshotView(presentedSharedElement));
-
- // treat images differently...
- if (presentedSharedElement instanceof UIImageView) {
- // in case the image is loaded async, we need to update the snapshot when it changes
- // todo: remove listener on transition end
- presentedView.on('imageSourceChange', () => {
- sharedElementSnapshot.image = iOSNativeHelper.snapshotView(presentedSharedElement, Screen.mainScreen.scale);
- sharedElementSnapshot.tintColor = presentedSharedElement.tintColor;
- });
-
- sharedElementSnapshot.tintColor = presentedSharedElement.tintColor;
- sharedElementSnapshot.contentMode = presentedSharedElement.contentMode;
- }
-
- 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;
- if (SharedTransition.DEBUG) {
- console.log('---> ', presentingView.sharedTransitionTag, ' frame:', iOSNativeHelper.printCGRect(sharedElementSnapshot.frame));
- }
-
- owner.sharedElements.presenting.push({
- view: presentingView,
- startFrame,
- endFrame,
- snapshot: sharedElementSnapshot,
- startOpacity: presentingView.opacity,
- endOpacity: presentedView.opacity,
- });
- owner.sharedElements.presented.push({
- view: presentedView,
- startFrame: endFrame,
- endFrame: startFrame,
- startOpacity: presentedView.opacity,
- endOpacity: presentingView.opacity,
- });
-
- // set initial opacity to match the source view opacity
- sharedElementSnapshot.alpha = presentingView.opacity;
- // hide both while animating within the transition context
- presentingView.opacity = 0;
- presentedView.opacity = 0;
- // add snapshot to animate
- transitionContext.containerView.addSubview(sharedElementSnapshot);
- }
-
- const cleanupPresent = () => {
- for (const presented of owner.sharedElements.presented) {
- presented.view.opacity = presented.startOpacity;
- }
- for (const presenting of owner.sharedElements.presenting) {
- presenting.snapshot.removeFromSuperview();
- }
- SharedTransition.updateState(owner.id, {
- activeType: SharedTransitionAnimationType.dismiss,
- });
- owner.presenting.view.removeFromSuperview();
- 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();
- 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.
- const updatedEndFrame = presented.view.ios.convertRectToView(presented.view.ios.bounds, transitionContext.containerView);
- const correctedEndFrame = CGRectMake(updatedEndFrame.origin.x, updatedEndFrame.origin.y, presentingMatch.endFrame.size.width, presentingMatch.endFrame.size.height);
- presentingMatch.snapshot.frame = correctedEndFrame;
-
- // apply view and layer properties to the snapshot view to match the source/presented view
- iOSNativeHelper.copyLayerProperties(presentingMatch.snapshot, presented.view.ios);
- // create a snapshot of the presented view
- presentingMatch.snapshot.image = iOSNativeHelper.snapshotView(presented.view.ios, Screen.mainScreen.scale);
- // apply correct alpha
- presentingMatch.snapshot.alpha = presentingMatch.endOpacity;
-
- if (SharedTransition.DEBUG) {
- console.log(`---> ${presentingMatch.view.sharedTransitionTag} animate to: `, iOSNativeHelper.printCGRect(correctedEndFrame));
- }
- }
- };
-
- // starting page properties
- const toProps = state.pageStart;
- owner.presented.view.alpha = isNumber(toProps?.opacity) ? toProps?.opacity : 0;
-
- 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;
- owner.presented.view.frame = CGRectMake(startX, startY, startWidth, startHeight);
-
- const animateProperties = () => {
- const props = state.pageEnd;
- // animate page properties to the following:
- owner.presented.view.alpha = isNumber(props?.opacity) ? props?.opacity : 1;
-
- 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?.friction : DEFAULT_SPRING.friction,
- animations: () => {
- animateProperties();
-
- updateFramePresent();
- },
- completion: () => {
- cleanupPresent();
- },
- });
- }
- break;
- }
- case UINavigationControllerOperation.Pop: {
- // console.log('popping!');
- transitionContext.containerView.insertSubviewBelowSubview(owner.presenting.view, owner.presented.view);
-
- // console.log('transitionContext.containerView.subviews.count:', transitionContext.containerView.subviews.count);
-
- if (SharedTransition.DEBUG) {
- console.log(' PageTransition: Pop');
- console.log(
- `1. Dismiss sharedTransitionTags to animate:`,
- owner.sharedElements.presented.map((p) => p.view.sharedTransitionTag)
- );
-
- console.log(`2. Add back previously stored sharedElements to dismiss:`);
- }
-
- for (const p of owner.sharedElements.presented) {
- p.view.opacity = 0;
- }
- for (const p of owner.sharedElements.presenting) {
- p.snapshot.alpha = p.endOpacity;
- transitionContext.containerView.addSubview(p.snapshot);
- }
-
- const cleanupDismiss = () => {
- for (const presenting of owner.sharedElements.presenting) {
- presenting.view.opacity = presenting.startOpacity;
- presenting.snapshot.removeFromSuperview();
- }
- SharedTransition.finishState(owner.id);
- transitionContext.completeTransition(true);
- };
- const updateFrameDismiss = () => {
- if (SharedTransition.DEBUG) {
- console.log('3. Dismissing shared elements:');
- }
- for (const presenting of owner.sharedElements.presenting) {
- iOSNativeHelper.copyLayerProperties(presenting.snapshot, presenting.view.ios);
- presenting.snapshot.frame = presenting.startFrame;
- presenting.snapshot.alpha = presenting.startOpacity;
-
- if (SharedTransition.DEBUG) {
- console.log(`---> ${presenting.view.sharedTransitionTag} animate to: `, iOSNativeHelper.printCGRect(presenting.startFrame));
- }
- }
- };
-
- const props = state.pageReturn;
-
- const animateProperties = () => {
- owner.presented.view.alpha = isNumber(props?.opacity) ? props?.opacity : 0;
-
- 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;
- 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?.friction : DEFAULT_SPRING.friction,
- animations: () => {
- animateProperties();
-
- updateFrameDismiss();
- },
- completion: () => {
- cleanupDismiss();
- },
- });
- }
- break;
- }
- }
+ SharedTransitionHelper.animate(state, transitionContext, 'page');
}
}
}
diff --git a/packages/core/ui/transition/shared-transition-helper.android.ts b/packages/core/ui/transition/shared-transition-helper.android.ts
new file mode 100644
index 000000000..60f9183a8
--- /dev/null
+++ b/packages/core/ui/transition/shared-transition-helper.android.ts
@@ -0,0 +1,7 @@
+import { SharedTransitionState } from './shared-transition';
+
+export class SharedTransitionHelper {
+ static animate(state: SharedTransitionState, transitionContext: any, type: 'page' | 'modal') {
+ // may be able to consolidate android handling here in future
+ }
+}
diff --git a/packages/core/ui/transition/shared-transition-helper.d.ts b/packages/core/ui/transition/shared-transition-helper.d.ts
new file mode 100644
index 000000000..c45122376
--- /dev/null
+++ b/packages/core/ui/transition/shared-transition-helper.d.ts
@@ -0,0 +1,10 @@
+import type { SharedTransitionType, SharedInteractiveState } from '.';
+import type { SharedTransitionState } from './shared-transition';
+
+export declare class SharedTransitionHelper {
+ static animate(state: SharedTransitionState, transitionContext: any /* iOS: UIViewControllerContextTransitioning */, type: SharedTransitionType): void;
+ static interactiveStart(state: SharedTransitionState, interactiveState: SharedInteractiveState, type: SharedTransitionType): void;
+ static interactiveUpdate(state: SharedTransitionState, interactiveState: SharedInteractiveState, type: SharedTransitionType, percent: number): void;
+ static interactiveCancel(state: SharedTransitionState, interactiveState: SharedInteractiveState, type: SharedTransitionType): void;
+ static interactiveFinish(state: SharedTransitionState, interactiveState: SharedInteractiveState, type: SharedTransitionType): void;
+}
diff --git a/packages/core/ui/transition/shared-transition-helper.ios.ts b/packages/core/ui/transition/shared-transition-helper.ios.ts
new file mode 100644
index 000000000..e257a82f1
--- /dev/null
+++ b/packages/core/ui/transition/shared-transition-helper.ios.ts
@@ -0,0 +1,502 @@
+import type { SharedInteractiveState, SharedTransitionType } from '.';
+import { getPageStartDefaultsForType, getRectFromProps, getSpringFromProps, SharedTransition, SharedTransitionAnimationType, SharedTransitionState } from './shared-transition';
+import { isNumber } from '../../utils/types';
+import { Screen } from '../../platform';
+import { iOSNativeHelper } from '../../utils/native-helper';
+interface PlatformSharedInteractiveState extends SharedInteractiveState {
+ transitionContext?: UIViewControllerContextTransitioning;
+ propertyAnimator?: UIViewPropertyAnimator;
+}
+export class SharedTransitionHelper {
+ static animate(state: SharedTransitionState, transitionContext: UIViewControllerContextTransitioning, type: SharedTransitionType) {
+ const transition = state.instance;
+ setTimeout(() => {
+ // Run on next tick
+ // ensures that existing UI state finishes before snapshotting
+ // (eg, button touch up state)
+ switch (state.activeType) {
+ case SharedTransitionAnimationType.present: {
+ // console.log('-- Transition present --');
+ SharedTransition.events().notify({
+ eventName: SharedTransition.startedEvent,
+ });
+
+ if (type === 'modal') {
+ transitionContext.containerView.addSubview(transition.presented.view);
+ } else if (type === 'page') {
+ transitionContext.containerView.insertSubviewAboveSubview(transition.presented.view, transition.presenting.view);
+ }
+ transition.presented.view.layoutIfNeeded();
+
+ const { sharedElements, presented, presenting } = SharedTransition.getSharedElements(state.page, state.toPage);
+
+ if (SharedTransition.DEBUG) {
+ console.log(' ModalTransition: Present');
+ 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:`);
+ }
+
+ const pageStart = state.pageStart;
+
+ const startFrame = getRectFromProps(pageStart, getPageStartDefaultsForType(type));
+
+ const pageEnd = state.pageEnd;
+ const pageEndIndependentTags = Object.keys(pageEnd?.sharedTransitionTags || {});
+ // console.log('pageEndIndependentTags:', pageEndIndependentTags);
+
+ for (const presentingView of sharedElements) {
+ if (!transition.sharedElements) {
+ transition.sharedElements = {
+ presented: [],
+ presenting: [],
+ independent: [],
+ };
+ }
+ const presentingSharedElement = presentingView.ios;
+ // console.log('fromTarget instanceof UIImageView:', fromTarget instanceof UIImageView)
+
+ // TODO: discuss whether we should check if UIImage/UIImageView type to always snapshot images or if other view types could be duped/added vs. snapshotted
+ // Note: snapshot may be most efficient/simple
+ // console.log('---> ', presentingView.sharedTransitionTag, ': ', presentingSharedElement)
+
+ const presentedView = presented.find((v) => v.sharedTransitionTag === presentingView.sharedTransitionTag);
+ const presentedSharedElement = presentedView.ios;
+
+ const snapshot = UIImageView.alloc().init();
+
+ // treat images differently...
+ if (presentedSharedElement instanceof UIImageView) {
+ // in case the image is loaded async, we need to update the snapshot when it changes
+ // todo: remove listener on transition end
+ presentedView.on('imageSourceChange', () => {
+ snapshot.image = iOSNativeHelper.snapshotView(presentedSharedElement, Screen.mainScreen.scale);
+ snapshot.tintColor = presentedSharedElement.tintColor;
+ });
+
+ snapshot.tintColor = presentedSharedElement.tintColor;
+ snapshot.contentMode = presentedSharedElement.contentMode;
+ }
+
+ iOSNativeHelper.copyLayerProperties(snapshot, presentingSharedElement);
+ snapshot.clipsToBounds = true;
+ // console.log('---> snapshot: ', snapshot);
+
+ const startFrame = presentingSharedElement.convertRectToView(presentingSharedElement.bounds, transitionContext.containerView);
+ const endFrame = presentedSharedElement.convertRectToView(presentedSharedElement.bounds, transitionContext.containerView);
+ snapshot.frame = startFrame;
+ if (SharedTransition.DEBUG) {
+ console.log('---> ', presentingView.sharedTransitionTag, ' frame:', iOSNativeHelper.printCGRect(snapshot.frame));
+ }
+
+ transition.sharedElements.presenting.push({
+ view: presentingView,
+ startFrame,
+ endFrame,
+ snapshot,
+ startOpacity: presentingView.opacity,
+ endOpacity: presentedView.opacity,
+ });
+ transition.sharedElements.presented.push({
+ view: presentedView,
+ startFrame: endFrame,
+ endFrame: startFrame,
+ startOpacity: presentedView.opacity,
+ endOpacity: presentingView.opacity,
+ });
+
+ // set initial opacity to match the source view opacity
+ snapshot.alpha = presentingView.opacity;
+ // hide both while animating within the transition context
+ presentingView.opacity = 0;
+ presentedView.opacity = 0;
+ // add snapshot to animate
+ transitionContext.containerView.addSubview(snapshot);
+ }
+
+ for (const tag of pageEndIndependentTags) {
+ // only consider start when there's a matching end
+ const pageStartIndependentProps = pageStart?.sharedTransitionTags[tag];
+ console.log('start:', tag, pageStartIndependentProps);
+ const pageEndIndependentProps = pageEnd?.sharedTransitionTags[tag];
+ let independentView = presenting.find((v) => v.sharedTransitionTag === tag);
+ let isPresented = false;
+ if (!independentView) {
+ independentView = presented.find((v) => v.sharedTransitionTag === tag);
+ if (!independentView) {
+ break;
+ }
+ isPresented = true;
+ }
+ const independentSharedElement: UIView = independentView.ios;
+
+ let snapshot: UIImageView;
+ // if (isPresented) {
+ // snapshot = UIImageView.alloc().init();
+ // } else {
+ snapshot = UIImageView.alloc().initWithImage(iOSNativeHelper.snapshotView(independentSharedElement, Screen.mainScreen.scale));
+ // }
+
+ if (independentSharedElement instanceof UIImageView) {
+ // in case the image is loaded async, we need to update the snapshot when it changes
+ // todo: remove listener on transition end
+ // if (isPresented) {
+ // independentView.on('imageSourceChange', () => {
+ // snapshot.image = iOSNativeHelper.snapshotView(independentSharedElement, Screen.mainScreen.scale);
+ // snapshot.tintColor = independentSharedElement.tintColor;
+ // });
+ // }
+
+ snapshot.tintColor = independentSharedElement.tintColor;
+ snapshot.contentMode = independentSharedElement.contentMode;
+ }
+ snapshot.clipsToBounds = true;
+
+ const startFrame = independentSharedElement.convertRectToView(independentSharedElement.bounds, transitionContext.containerView);
+ const startFrameRect = getRectFromProps(pageStartIndependentProps);
+ // adjust for any specified start positions
+ const startFrameAdjusted = CGRectMake(startFrame.origin.x + startFrameRect.x, startFrame.origin.y + startFrameRect.y, startFrame.size.width, startFrame.size.height);
+ console.log('startFrameAdjusted:', tag, iOSNativeHelper.printCGRect(startFrameAdjusted));
+ // if (pageStartIndependentProps?.scale) {
+ // snapshot.transform = CGAffineTransformConcat(CGAffineTransformMakeTranslation(startFrameAdjusted.origin.x, startFrameAdjusted.origin.y), CGAffineTransformMakeScale(pageStartIndependentProps.scale.x, pageStartIndependentProps.scale.y))
+ // } else {
+ snapshot.frame = startFrame; //startFrameAdjusted;
+ // }
+ if (SharedTransition.DEBUG) {
+ console.log('---> ', independentView.sharedTransitionTag, ' frame:', iOSNativeHelper.printCGRect(snapshot.frame));
+ }
+
+ const endFrameRect = getRectFromProps(pageEndIndependentProps);
+
+ const endFrame = CGRectMake(startFrame.origin.x + endFrameRect.x, startFrame.origin.y + endFrameRect.y, startFrame.size.width, startFrame.size.height);
+ console.log('endFrame:', tag, iOSNativeHelper.printCGRect(endFrame));
+ transition.sharedElements.independent.push({
+ view: independentView,
+ isPresented,
+ startFrame,
+ snapshot,
+ endFrame,
+ startTransform: independentSharedElement.transform,
+ scale: pageEndIndependentProps.scale,
+ startOpacity: independentView.opacity,
+ endOpacity: isNumber(pageEndIndependentProps.opacity) ? pageEndIndependentProps.opacity : 0,
+ });
+
+ independentView.opacity = 0;
+ // add snapshot to animate
+ transitionContext.containerView.addSubview(snapshot);
+ }
+
+ // Important: always set after above shared element positions have had their start positions set
+ transition.presented.view.alpha = isNumber(pageStart?.opacity) ? pageStart?.opacity : 0;
+ transition.presented.view.frame = CGRectMake(startFrame.x, startFrame.y, startFrame.width, startFrame.height);
+
+ const cleanupPresent = () => {
+ for (const presented of transition.sharedElements.presented) {
+ presented.view.opacity = presented.startOpacity;
+ }
+ for (const presenting of transition.sharedElements.presenting) {
+ presenting.snapshot.removeFromSuperview();
+ }
+ for (const independent of transition.sharedElements.independent) {
+ independent.snapshot.removeFromSuperview();
+ if (independent.isPresented) {
+ independent.view.opacity = independent.startOpacity;
+ }
+ }
+ SharedTransition.updateState(transition.id, {
+ activeType: SharedTransitionAnimationType.dismiss,
+ });
+ if (type === 'page') {
+ transition.presenting.view.removeFromSuperview();
+ }
+ transitionContext.completeTransition(true);
+ SharedTransition.events().notify({
+ eventName: SharedTransition.finishedEvent,
+ });
+ };
+
+ const animateProperties = () => {
+ if (SharedTransition.DEBUG) {
+ console.log('3. Animating shared elements:');
+ }
+ transition.presented.view.alpha = isNumber(pageEnd?.opacity) ? pageEnd?.opacity : 1;
+
+ const endFrame = getRectFromProps(pageEnd);
+ transition.presented.view.frame = CGRectMake(endFrame.x, endFrame.y, endFrame.width, endFrame.height);
+
+ // animate page properties to the following:
+ // 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();
+
+ for (const presented of transition.sharedElements.presented) {
+ const presentingMatch = transition.sharedElements.presenting.find((v) => v.view.sharedTransitionTag === presented.view.sharedTransitionTag);
+ // Workaround wrong origin due ongoing layout process.
+ const updatedEndFrame = presented.view.ios.convertRectToView(presented.view.ios.bounds, transitionContext.containerView);
+ const correctedEndFrame = CGRectMake(updatedEndFrame.origin.x, updatedEndFrame.origin.y, presentingMatch.endFrame.size.width, presentingMatch.endFrame.size.height);
+ presentingMatch.snapshot.frame = correctedEndFrame;
+
+ // apply view and layer properties to the snapshot view to match the source/presented view
+ iOSNativeHelper.copyLayerProperties(presentingMatch.snapshot, presented.view.ios);
+ // create a snapshot of the presented view
+ presentingMatch.snapshot.image = iOSNativeHelper.snapshotView(presented.view.ios, Screen.mainScreen.scale);
+ // apply correct alpha
+ presentingMatch.snapshot.alpha = presentingMatch.endOpacity;
+
+ if (SharedTransition.DEBUG) {
+ console.log(`---> ${presentingMatch.view.sharedTransitionTag} animate to: `, iOSNativeHelper.printCGRect(correctedEndFrame));
+ }
+ }
+ for (const independent of transition.sharedElements.independent) {
+ let endFrame: CGRect = independent.endFrame;
+ // if (independent.isPresented) {
+ // const updatedEndFrame = independent.view.ios.convertRectToView(independent.view.ios.bounds, transitionContext.containerView);
+ // endFrame = CGRectMake(updatedEndFrame.origin.x, updatedEndFrame.origin.y, independent.endFrame.size.width, independent.endFrame.size.height);
+ // }
+ if (independent.scale) {
+ independent.snapshot.transform = CGAffineTransformConcat(CGAffineTransformMakeTranslation(endFrame.origin.x, endFrame.origin.y), CGAffineTransformMakeScale(independent.scale.x, independent.scale.y));
+ } else {
+ independent.snapshot.frame = endFrame;
+ }
+ independent.snapshot.alpha = independent.endOpacity;
+
+ if (SharedTransition.DEBUG) {
+ console.log(`---> ${independent.view.sharedTransitionTag} animate to: `, iOSNativeHelper.printCGRect(independent.endFrame));
+ }
+ }
+ };
+
+ if (isNumber(pageEnd?.duration)) {
+ // override spring and use only linear animation
+ UIView.animateWithDurationAnimationsCompletion(
+ pageEnd?.duration / 1000,
+ () => {
+ animateProperties();
+ },
+ () => {
+ cleanupPresent();
+ }
+ );
+ } else {
+ iOSNativeHelper.animateWithSpring({
+ ...getSpringFromProps(pageEnd?.spring),
+ animations: () => {
+ animateProperties();
+ },
+ completion: () => {
+ cleanupPresent();
+ },
+ });
+ }
+
+ break;
+ }
+ case SharedTransitionAnimationType.dismiss: {
+ // console.log('-- Transition dismiss --');
+ SharedTransition.events().notify({
+ eventName: SharedTransition.startedEvent,
+ });
+ if (type === 'page') {
+ transitionContext.containerView.insertSubviewBelowSubview(transition.presenting.view, transition.presented.view);
+ }
+
+ // console.log('transitionContext.containerView.subviews.count:', transitionContext.containerView.subviews.count);
+
+ if (SharedTransition.DEBUG) {
+ console.log(' ModalTransition: Dismiss');
+ console.log(
+ `1. Dismiss sharedTransitionTags to animate:`,
+ transition.sharedElements.presented.map((p) => p.view.sharedTransitionTag)
+ );
+
+ console.log(`2. Add back previously stored sharedElements to dismiss:`);
+ }
+
+ for (const p of transition.sharedElements.presented) {
+ p.view.opacity = 0;
+ }
+ for (const p of transition.sharedElements.presenting) {
+ p.snapshot.alpha = p.endOpacity;
+ transitionContext.containerView.addSubview(p.snapshot);
+ }
+ for (const independent of transition.sharedElements.independent) {
+ independent.snapshot.alpha = independent.endOpacity;
+ transitionContext.containerView.addSubview(independent.snapshot);
+ }
+
+ const pageReturn = state.pageReturn;
+
+ const cleanupDismiss = () => {
+ for (const presenting of transition.sharedElements.presenting) {
+ presenting.view.opacity = presenting.startOpacity;
+ presenting.snapshot.removeFromSuperview();
+ }
+ for (const independent of transition.sharedElements.independent) {
+ independent.view.opacity = independent.startOpacity;
+ independent.snapshot.removeFromSuperview();
+ }
+ SharedTransition.finishState(transition.id);
+ transition.sharedElements = null;
+ transitionContext.completeTransition(true);
+ SharedTransition.events().notify({
+ eventName: SharedTransition.finishedEvent,
+ });
+ };
+
+ const animateProperties = () => {
+ if (SharedTransition.DEBUG) {
+ console.log('3. Dismissing shared elements:');
+ }
+
+ transition.presented.view.alpha = isNumber(pageReturn?.opacity) ? pageReturn?.opacity : 0;
+
+ const endFrame = getRectFromProps(pageReturn, getPageStartDefaultsForType(type));
+ transition.presented.view.frame = CGRectMake(endFrame.x, endFrame.y, endFrame.width, endFrame.height);
+
+ for (const presenting of transition.sharedElements.presenting) {
+ iOSNativeHelper.copyLayerProperties(presenting.snapshot, presenting.view.ios);
+ presenting.snapshot.frame = presenting.startFrame;
+ presenting.snapshot.alpha = presenting.startOpacity;
+
+ if (SharedTransition.DEBUG) {
+ console.log(`---> ${presenting.view.sharedTransitionTag} animate to: `, iOSNativeHelper.printCGRect(presenting.startFrame));
+ }
+ }
+
+ for (const independent of transition.sharedElements.independent) {
+ independent.snapshot.alpha = independent.startOpacity;
+ if (independent.scale) {
+ independent.snapshot.transform = independent.startTransform;
+ } else {
+ independent.snapshot.frame = independent.startFrame;
+ }
+
+ if (SharedTransition.DEBUG) {
+ console.log(`---> ${independent.view.sharedTransitionTag} animate to: `, iOSNativeHelper.printCGRect(independent.startFrame));
+ }
+ }
+ };
+
+ if (isNumber(pageReturn?.duration)) {
+ // override spring and use only linear animation
+ UIView.animateWithDurationAnimationsCompletion(
+ pageReturn?.duration / 1000,
+ () => {
+ animateProperties();
+ },
+ () => {
+ cleanupDismiss();
+ }
+ );
+ } else {
+ iOSNativeHelper.animateWithSpring({
+ ...getSpringFromProps(pageReturn?.spring),
+ animations: () => {
+ animateProperties();
+ },
+ completion: () => {
+ cleanupDismiss();
+ },
+ });
+ }
+
+ break;
+ }
+ }
+ });
+ }
+
+ static interactiveStart(state: SharedTransitionState, interactiveState: SharedInteractiveState, type: SharedTransitionType) {
+ SharedTransition.events().notify({
+ eventName: SharedTransition.startedEvent,
+ });
+ switch (type) {
+ case 'page':
+ interactiveState.transitionContext.containerView.insertSubviewBelowSubview(state.instance.presenting.view, state.instance.presented.view);
+ break;
+ }
+ }
+
+ static interactiveUpdate(state: SharedTransitionState, interactiveState: SharedInteractiveState, type: SharedTransitionType, percent: number) {
+ if (!interactiveState.added) {
+ interactiveState.added = true;
+ for (const p of state.instance.sharedElements.presented) {
+ p.view.opacity = 0;
+ }
+ for (const p of state.instance.sharedElements.presenting) {
+ p.snapshot.alpha = p.endOpacity;
+ interactiveState.transitionContext.containerView.addSubview(p.snapshot);
+ }
+
+ const pageStart = state.pageStart;
+
+ const startFrame = getRectFromProps(pageStart, getPageStartDefaultsForType(type));
+ interactiveState.propertyAnimator = UIViewPropertyAnimator.alloc().initWithDurationDampingRatioAnimations(1, 1, () => {
+ for (const p of state.instance.sharedElements.presenting) {
+ p.snapshot.frame = p.startFrame;
+ iOSNativeHelper.copyLayerProperties(p.snapshot, p.view.ios);
+
+ p.snapshot.alpha = 1;
+ }
+ state.instance.presented.view.alpha = isNumber(state.pageReturn?.opacity) ? state.pageReturn?.opacity : 0;
+ state.instance.presented.view.frame = CGRectMake(startFrame.x, startFrame.y, state.instance.presented.view.bounds.size.width, state.instance.presented.view.bounds.size.height);
+ });
+ }
+ interactiveState.propertyAnimator.fractionComplete = percent;
+ }
+
+ static interactiveCancel(state: SharedTransitionState, interactiveState: SharedInteractiveState, type: SharedTransitionType) {
+ if (state.instance && interactiveState.added && interactiveState.propertyAnimator) {
+ interactiveState.propertyAnimator.reversed = true;
+ const duration = isNumber(state.pageStart?.duration) ? state.pageStart?.duration / 1000 : 0.35;
+ interactiveState.propertyAnimator.continueAnimationWithTimingParametersDurationFactor(null, duration);
+ setTimeout(() => {
+ for (const p of state.instance.sharedElements.presented) {
+ p.view.opacity = 1;
+ }
+ for (const p of state.instance.sharedElements.presenting) {
+ p.snapshot.removeFromSuperview();
+ }
+ state.instance.presented.view.alpha = 1;
+ interactiveState.propertyAnimator = null;
+ interactiveState.added = false;
+ interactiveState.transitionContext.cancelInteractiveTransition();
+ interactiveState.transitionContext.completeTransition(false);
+ SharedTransition.events().notify({
+ eventName: SharedTransition.cancelledEvent,
+ });
+ }, duration * 1000);
+ }
+ }
+
+ static interactiveFinish(state: SharedTransitionState, interactiveState: SharedInteractiveState, type: SharedTransitionType) {
+ if (state.instance && interactiveState.added && interactiveState.propertyAnimator) {
+ interactiveState.propertyAnimator.reversed = false;
+
+ const duration = isNumber(state.pageReturn?.duration) ? state.pageReturn?.duration / 1000 : 0.35;
+ interactiveState.propertyAnimator.continueAnimationWithTimingParametersDurationFactor(null, duration);
+ setTimeout(() => {
+ for (const presenting of state.instance.sharedElements.presenting) {
+ presenting.view.opacity = presenting.startOpacity;
+ presenting.snapshot.removeFromSuperview();
+ }
+
+ SharedTransition.finishState(state.instance.id);
+ interactiveState.propertyAnimator = null;
+ interactiveState.added = false;
+ interactiveState.transitionContext.finishInteractiveTransition();
+ interactiveState.transitionContext.completeTransition(true);
+ SharedTransition.events().notify({
+ eventName: SharedTransition.finishedEvent,
+ });
+ }, duration * 1000);
+ }
+ }
+}
diff --git a/packages/core/ui/transition/shared-transition.ts b/packages/core/ui/transition/shared-transition.ts
index 2ab4605bc..b827bbf99 100644
--- a/packages/core/ui/transition/shared-transition.ts
+++ b/packages/core/ui/transition/shared-transition.ts
@@ -1,4 +1,5 @@
import type { Transition } from '.';
+import { Observable } from '../../data/observable';
import { Screen } from '../../platform';
import { isNumber } from '../../utils/types';
import { querySelectorAll, ViewBase } from '../core/view-base';
@@ -34,10 +35,6 @@ export interface SharedTransitionInteractiveOptions {
percentFormula?: (eventData: PanGestureEventData) => number;
}
export interface SharedTransitionConfig {
- /**
- * Page which will start the transition.
- */
- page?: ViewBase;
/**
* Preconfigured transition or your own custom configured one.
*/
@@ -66,6 +63,10 @@ export interface SharedTransitionConfig {
pageReturn?: SharedTransitionPageProperties;
}
export interface SharedTransitionState extends SharedTransitionConfig {
+ /**
+ * Page which will start the transition.
+ */
+ page?: ViewBase;
activeType?: SharedTransitionAnimationType;
toPage?: ViewBase;
// used internally for determining interactive gesture state of the transition
@@ -74,6 +75,14 @@ export interface SharedTransitionState extends SharedTransitionConfig {
}
export type SharedRect = { x?: number; y?: number; width?: number; height?: number };
export type SharedProperties = SharedRect & { opacity?: number; scale?: { x?: number; y?: number } };
+export type SharedSpringProperties = {
+ tension?: number;
+ friction?: number;
+ mass?: number;
+ delay?: number;
+ velocity?: number;
+ animateOptions?: any /* ios only: UIViewAnimationOptions */;
+};
type SharedTransitionPageProperties = SharedProperties & {
/**
* Linear duration in milliseconds
@@ -85,15 +94,9 @@ type SharedTransitionPageProperties = SharedProperties & {
* 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 */;
- };
+ spring?: SharedSpringProperties;
};
+let sharedTransitionEvents: Observable;
/**
* Shared Element Transitions (preview)
* Allows you to auto animate between shared elements on two different screesn to create smooth navigational experiences.
@@ -114,6 +117,15 @@ export class SharedTransition {
});
return { instance: transition };
}
+ static events() {
+ if (!sharedTransitionEvents) {
+ sharedTransitionEvents = new Observable();
+ }
+ return sharedTransitionEvents;
+ }
+ static startedEvent = 'SharedTransitionStartedEvent';
+ static finishedEvent = 'SharedTransitionFinishedEvent';
+ static cancelledEvent = 'SharedTransitionCancelledEvent';
/**
* Enable to see various console logging output of Shared Element Transition behavior.
@@ -145,13 +157,13 @@ export class SharedTransition {
* @private
*/
static getState(id: number) {
- return SharedTransition.currentStack.find((t) => t.instance.id === id);
+ return SharedTransition.currentStack?.find((t) => t.instance.id === id);
}
/**
* @private
*/
static finishState(id: number) {
- const index = SharedTransition.currentStack.findIndex((t) => t.instance.id === id);
+ const index = SharedTransition.currentStack?.findIndex((t) => t.instance.id === id);
if (index > -1) {
SharedTransition.currentStack.splice(index, 1);
}
@@ -176,10 +188,10 @@ export class SharedTransition {
// 2. Presenting view: gather all sharedTransitionTag views
const presentingSharedElements = >querySelectorAll(fromPage, 'sharedTransitionTag');
- console.log(
- 'presenting sharedTransitionTags:',
- presentingSharedElements.map((v) => v.sharedTransitionTag)
- );
+ // console.log(
+ // 'presenting sharedTransitionTags:',
+ // presentingSharedElements.map((v) => v.sharedTransitionTag)
+ // );
// 3. only handle sharedTransitionTag on presenting which match presented
const presentedTags = presentedSharedElements.map((v) => v.sharedTransitionTag);
@@ -206,3 +218,22 @@ export function getRectFromProps(props: SharedTransitionPageProperties, defaults
height: isNumber(props?.height) ? props?.height : defaults.height,
};
}
+
+export function getSpringFromProps(props: SharedSpringProperties) {
+ return {
+ tension: isNumber(props?.tension) ? props?.tension : DEFAULT_SPRING.tension,
+ friction: isNumber(props?.friction) ? props?.friction : DEFAULT_SPRING.friction,
+ mass: isNumber(props?.mass) ? props?.mass : DEFAULT_SPRING.mass,
+ velocity: isNumber(props?.velocity) ? props?.velocity : DEFAULT_SPRING.velocity,
+ delay: isNumber(props?.delay) ? props?.delay : DEFAULT_SPRING.delay,
+ };
+}
+
+export function getPageStartDefaultsForType(type: 'page' | 'modal') {
+ return {
+ x: type === 'page' ? Screen.mainScreen.widthDIPs : 0,
+ y: type === 'page' ? 0 : Screen.mainScreen.heightDIPs,
+ width: Screen.mainScreen.widthDIPs,
+ height: Screen.mainScreen.heightDIPs,
+ };
+}