diff --git a/apps/automated/package.json b/apps/automated/package.json
index 154e9cf95..0de8f239a 100644
--- a/apps/automated/package.json
+++ b/apps/automated/package.json
@@ -11,8 +11,8 @@
"nativescript-theme-core": "file:../../node_modules/nativescript-theme-core"
},
"devDependencies": {
- "@nativescript/android": "~8.4.0",
- "@nativescript/ios": "~8.4.0",
+ "@nativescript/android": "~8.5.0",
+ "@nativescript/ios": "~8.5.0",
"@nativescript/webpack": "file:../../dist/packages/nativescript-webpack.tgz",
"typescript": "~4.9.5"
},
diff --git a/apps/automated/src/navigation/custom-transition.ios.ts b/apps/automated/src/navigation/custom-transition.ios.ts
index 2191afb1e..16a013889 100644
--- a/apps/automated/src/navigation/custom-transition.ios.ts
+++ b/apps/automated/src/navigation/custom-transition.ios.ts
@@ -1,4 +1,5 @@
-import { PageTransition, SharedTransition, SharedTransitionHelper, Transition } from '@nativescript/core';
+import { PageTransition, SharedTransition, SharedTransitionAnimationType, SharedTransitionHelper, Transition, Utils } from '@nativescript/core';
+import { CORE_ANIMATION_DEFAULTS } from '@nativescript/core/utils';
export class CustomTransition extends Transition {
constructor(duration: number, curve: any) {
@@ -72,9 +73,24 @@ class PageTransitionController extends NSObject implements UIViewControllerAnima
transitionDuration(transitionContext: UIViewControllerContextTransitioning): number {
const owner = this.owner.deref();
if (owner) {
- return owner.getDuration();
+ const state = SharedTransition.getState(owner.id);
+ switch (state?.activeType) {
+ case SharedTransitionAnimationType.present:
+ if (Utils.isNumber(state?.pageEnd?.duration)) {
+ return state.pageEnd?.duration / 1000;
+ } else {
+ return Utils.getDurationWithDampingFromSpring(state.pageEnd?.spring).duration;
+ }
+
+ case SharedTransitionAnimationType.dismiss:
+ if (Utils.isNumber(state?.pageReturn?.duration)) {
+ return state.pageReturn?.duration / 1000;
+ } else {
+ return Utils.getDurationWithDampingFromSpring(state.pageReturn?.spring).duration;
+ }
+ }
}
- return 0.35;
+ return CORE_ANIMATION_DEFAULTS.duration;
}
animateTransition(transitionContext: UIViewControllerContextTransitioning): void {
diff --git a/apps/automated/src/navigation/transition-tests.ts b/apps/automated/src/navigation/transition-tests.ts
index 48fb9092d..e25d48247 100644
--- a/apps/automated/src/navigation/transition-tests.ts
+++ b/apps/automated/src/navigation/transition-tests.ts
@@ -52,8 +52,8 @@ export function test_Transitions() {
// helper.navigateWithEntry({ create: mainPageFactory, clearHistory: true, animated: false });
}
-export function test_SharedElementTransitions() {
- helper.navigate(() => {
+export function test_SharedElementTransitions(done) {
+ const mainPage = helper.navigate(() => {
const page = new Page();
page.id = 'SharedelementTransitionsTestPage_MAIN';
page.style.backgroundColor = new Color(255, Math.round(Math.random() * 255), Math.round(Math.random() * 255), Math.round(Math.random() * 255));
@@ -76,12 +76,12 @@ export function test_SharedElementTransitions() {
const navigationTransition = SharedTransition.custom(new CustomSharedElementPageTransition());
- var testId = `SharedElementTransition[${JSON.stringify(navigationTransition)}]`;
+ const testId = `SharedElementTransition[${JSON.stringify(navigationTransition)}]`;
if (Trace.isEnabled()) {
Trace.write(`Testing ${testId}`, Trace.categories.Test);
}
- var navigationEntry: NavigationEntry = {
- create: function (): Page {
+ const navigationEntry: NavigationEntry = {
+ create(): Page {
let page = new Page();
page.id = testId;
page.style.backgroundColor = new Color(255, Math.round(Math.random() * 255), Math.round(Math.random() * 255), Math.round(Math.random() * 255));
@@ -99,11 +99,16 @@ export function test_SharedElementTransitions() {
grid.addChild(sharedElement);
page.content = grid;
+
+ page.once('navigatedTo', () => {
+ done();
+ });
+
return page;
},
animated: true,
transition: navigationTransition,
};
- helper.navigateWithEntry(navigationEntry);
+ mainPage.frame.navigate(navigationEntry);
}
diff --git a/apps/toolbox/package.json b/apps/toolbox/package.json
index 636850fce..e3376a7d5 100644
--- a/apps/toolbox/package.json
+++ b/apps/toolbox/package.json
@@ -12,8 +12,8 @@
"@nativescript/imagepicker": "^1.0.6"
},
"devDependencies": {
- "@nativescript/android": "~8.4.0",
- "@nativescript/ios": "~8.4.0",
+ "@nativescript/android": "~8.5.0",
+ "@nativescript/ios": "~8.5.0",
"@nativescript/webpack": "file:../../dist/packages/nativescript-webpack.tgz",
"typescript": "~4.9.5"
}
diff --git a/apps/toolbox/src/pages/transitions/transition-example-detail.xml b/apps/toolbox/src/pages/transitions/transition-example-detail.xml
index 27d1397f3..5cea63e6d 100644
--- a/apps/toolbox/src/pages/transitions/transition-example-detail.xml
+++ b/apps/toolbox/src/pages/transitions/transition-example-detail.xml
@@ -6,17 +6,17 @@
-
+
-
-
-
-
+
+
+
+
-
+
diff --git a/apps/toolbox/src/pages/transitions/transition-example.ts b/apps/toolbox/src/pages/transitions/transition-example.ts
index 3a7224dd7..31b6d1cca 100644
--- a/apps/toolbox/src/pages/transitions/transition-example.ts
+++ b/apps/toolbox/src/pages/transitions/transition-example.ts
@@ -1,4 +1,4 @@
-import { Observable, EventData, Page, ShowModalOptions, SharedTransition, ModalTransition, PageTransition, FadeTransition, SlideTransition, PropertyChangeData } from '@nativescript/core';
+import { Observable, EventData, Page, ShowModalOptions, SharedTransition, ModalTransition, PageTransition, FadeTransition, SlideTransition, PropertyChangeData, ListView } from '@nativescript/core';
let page: Page;
// SharedTransition.DEBUG = true;
export function navigatingTo(args: EventData) {
@@ -64,6 +64,24 @@ export class TransitionsModel extends Observable {
finishThreshold: 0.5,
},
},
+ pageEnd: {
+ sharedTransitionTags: {
+ shape1: {
+ // purple should appear "below" other shapes during transition
+ zIndex: 0,
+ },
+ shape2: {
+ zIndex: 1,
+ },
+ shape3: {
+ zIndex: 3,
+ },
+ shape4: {
+ // yellow should appear "below" shape3
+ zIndex: 2,
+ },
+ },
+ },
// pageEnd: {
// duration: 3000
// },
diff --git a/apps/toolbox/src/pages/transitions/transition-example.xml b/apps/toolbox/src/pages/transitions/transition-example.xml
index 8fa594323..f628c6835 100644
--- a/apps/toolbox/src/pages/transitions/transition-example.xml
+++ b/apps/toolbox/src/pages/transitions/transition-example.xml
@@ -5,7 +5,7 @@
-
+
diff --git a/apps/ui/package.json b/apps/ui/package.json
index f097078ff..8e0ccca65 100644
--- a/apps/ui/package.json
+++ b/apps/ui/package.json
@@ -11,8 +11,8 @@
"@nativescript/core": "file:../../packages/core"
},
"devDependencies": {
- "@nativescript/android": "~8.4.0",
- "@nativescript/ios": "~8.4.0",
+ "@nativescript/android": "~8.5.0",
+ "@nativescript/ios": "~8.5.0",
"@nativescript/webpack": "file:../../dist/packages/nativescript-webpack.tgz",
"typescript": "~4.9.5"
},
diff --git a/package.json b/package.json
index 2c26db0b5..9fdaaa02c 100644
--- a/package.json
+++ b/package.json
@@ -18,21 +18,21 @@
"url": "https://github.com/NativeScript/NativeScript.git"
},
"dependencies": {
- "@nrwl/nx-cloud": "15.1.1",
+ "@nrwl/nx-cloud": "15.3.5",
"nativescript-theme-core": "^1.0.4"
},
"devDependencies": {
"@nativescript/hook": "^2.0.0",
"@nativescript/nx": "~4.2.0",
- "@nrwl/cli": "15.8.3",
- "@nrwl/eslint-plugin-nx": "15.8.3",
- "@nrwl/jest": "15.8.3",
- "@nrwl/node": "15.8.3",
- "@nrwl/workspace": "15.8.3",
+ "@nrwl/cli": "15.9.2",
+ "@nrwl/eslint-plugin-nx": "15.9.2",
+ "@nrwl/jest": "15.9.2",
+ "@nrwl/node": "15.9.2",
+ "@nrwl/workspace": "15.9.2",
"@nstudio/focus": "^15.0.0",
"@nstudio/nps-i": "~2.0.0",
"@prettier/plugin-xml": "^2.2.0",
- "@types/jest": "29.4.0",
+ "@types/jest": "~29.5.0",
"@types/node": "18.7.1",
"@typescript-eslint/eslint-plugin": "^5.30.0",
"@typescript-eslint/parser": "^5.30.0",
@@ -46,12 +46,12 @@
"eslint-config-prettier": "^8.1.0",
"gonzales": "^1.0.7",
"husky": "^8.0.1",
- "jest": "29.4.3",
+ "jest": "~29.5.0",
"lint-staged": "^13.1.0",
"module-alias": "^2.2.2",
- "nativescript": "~8.4.0",
+ "nativescript": "~8.5.0",
"nativescript-typedoc-theme": "1.1.0",
- "nx": "15.8.3",
+ "nx": "15.9.2",
"parse-css": "git+https://github.com/tabatkins/parse-css.git",
"parserlib": "^1.1.1",
"prettier": "^2.6.2",
@@ -59,7 +59,7 @@
"sass": "^1.45.2",
"shady-css-parser": "^0.1.0",
"tree-kill": "^1.2.2",
- "ts-jest": "29.0.5",
+ "ts-jest": "29.1.0",
"ts-node": "10.9.1",
"ts-patch": "^2.1.0",
"tslib": "^2.5.0",
diff --git a/packages/core/index.d.ts b/packages/core/index.d.ts
index e58fc76a8..cb12b4a51 100644
--- a/packages/core/index.d.ts
+++ b/packages/core/index.d.ts
@@ -107,7 +107,7 @@ export type { InstrumentationMode, TimerInfo } from './profiling';
export { encoding } from './text';
export * from './trace';
export * from './ui';
-import { GC, isFontIconURI, isDataURI, isFileOrResourcePath, executeOnMainThread, mainThreadify, isMainThread, dispatchToMainThread, executeOnUIThread, releaseNativeObject, getModuleName, openFile, openUrl, isRealDevice, layout, ad as androidUtils, iOSNativeHelper as iosUtils, Source, escapeRegexSymbols, convertString, dismissSoftInput, dismissKeyboard, queueMacrotask, queueGC, throttle, debounce, dataSerialize, dataDeserialize, copyToClipboard, getFileExtension, isEmoji } from './utils';
+import { GC, isFontIconURI, isDataURI, isFileOrResourcePath, executeOnMainThread, mainThreadify, isMainThread, dispatchToMainThread, executeOnUIThread, releaseNativeObject, getModuleName, openFile, openUrl, isRealDevice, layout, ad as androidUtils, iOSNativeHelper as iosUtils, Source, escapeRegexSymbols, convertString, dismissSoftInput, dismissKeyboard, queueMacrotask, queueGC, throttle, debounce, dataSerialize, dataDeserialize, copyToClipboard, getFileExtension, isEmoji, getDurationWithDampingFromSpring } from './utils';
import { SDK_VERSION } from './utils/constants';
import { ClassInfo, getClass, getBaseClasses, getClassInfo, isBoolean, isDefined, isFunction, isNullOrUndefined, isNumber, isObject, isString, isUndefined, toUIString, verifyCallback, numberHasDecimals, numberIs64Bit } from './utils/types';
export declare const Utils: {
@@ -166,5 +166,6 @@ export declare const Utils: {
dismissKeyboard: typeof dismissKeyboard;
copyToClipboard: typeof copyToClipboard;
isEmoji: typeof isEmoji;
+ getDurationWithDampingFromSpring: typeof getDurationWithDampingFromSpring;
};
export { XmlParser, ParserEventType, ParserEvent } from './xml';
diff --git a/packages/core/index.ts b/packages/core/index.ts
index 239a62d01..23418fc82 100644
--- a/packages/core/index.ts
+++ b/packages/core/index.ts
@@ -139,7 +139,7 @@ export * from './trace';
export * from './ui';
-import { GC, isFontIconURI, isDataURI, isFileOrResourcePath, executeOnMainThread, mainThreadify, isMainThread, dispatchToMainThread, executeOnUIThread, queueMacrotask, queueGC, debounce, throttle, releaseNativeObject, getModuleName, openFile, openUrl, isRealDevice, layout, ad as androidUtils, iOSNativeHelper as iosUtils, Source, RESOURCE_PREFIX, FILE_PREFIX, escapeRegexSymbols, convertString, dismissSoftInput, dismissKeyboard, dataDeserialize, dataSerialize, copyToClipboard, getFileExtension, isEmoji } from './utils';
+import { GC, isFontIconURI, isDataURI, isFileOrResourcePath, executeOnMainThread, mainThreadify, isMainThread, dispatchToMainThread, executeOnUIThread, queueMacrotask, queueGC, debounce, throttle, releaseNativeObject, getModuleName, openFile, openUrl, isRealDevice, layout, ad as androidUtils, iOSNativeHelper as iosUtils, Source, RESOURCE_PREFIX, FILE_PREFIX, escapeRegexSymbols, convertString, dismissSoftInput, dismissKeyboard, dataDeserialize, dataSerialize, copyToClipboard, getFileExtension, isEmoji, getDurationWithDampingFromSpring } from './utils';
import { SDK_VERSION } from './utils/constants';
import { ClassInfo, getClass, getBaseClasses, getClassInfo, isBoolean, isDefined, isFunction, isNullOrUndefined, isNumber, isObject, isString, isUndefined, toUIString, verifyCallback, numberHasDecimals, numberIs64Bit } from './utils/types';
@@ -202,6 +202,7 @@ export const Utils = {
dismissKeyboard,
copyToClipboard,
isEmoji,
+ getDurationWithDampingFromSpring,
};
export { XmlParser, ParserEventType, ParserEvent } from './xml';
diff --git a/packages/core/package.json b/packages/core/package.json
index d08dbedb5..ac209e407 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -1,6 +1,6 @@
{
"name": "@nativescript/core",
- "version": "8.5.0",
+ "version": "8.5.1-rc.3",
"description": "A JavaScript library providing an easy to use api for interacting with iOS and Android platform APIs.",
"main": "index",
"types": "index.d.ts",
diff --git a/packages/core/ui/builder/component-builder/index.ts b/packages/core/ui/builder/component-builder/index.ts
index 757cd282a..45ee4b88a 100644
--- a/packages/core/ui/builder/component-builder/index.ts
+++ b/packages/core/ui/builder/component-builder/index.ts
@@ -7,7 +7,7 @@ import { getBindingOptions, bindingConstants } from '../binding-builder';
import { profile } from '../../../profiling';
import * as debugModule from '../../../utils/debug';
import { Device } from '../../../platform';
-import { sanitizeModuleName } from '../module-name-sanitizer';
+import { sanitizeModuleName } from '../../../utils/common';
import { resolveModuleName } from '../../../module-name-resolver';
export interface ComponentModule {
diff --git a/packages/core/ui/builder/index.ts b/packages/core/ui/builder/index.ts
index 55f853abd..1a99d6f20 100644
--- a/packages/core/ui/builder/index.ts
+++ b/packages/core/ui/builder/index.ts
@@ -7,10 +7,10 @@ import { Page } from '../page';
// Types.
import { debug } from '../../utils/debug';
import { isDefined } from '../../utils/types';
+import { sanitizeModuleName } from '../../utils/common';
import { setPropertyValue, getComponentModule } from './component-builder';
import type { ComponentModule } from './component-builder';
import { platformNames } from '../../platform';
-import { sanitizeModuleName } from './module-name-sanitizer';
import { resolveModuleName } from '../../module-name-resolver';
import { xml2ui } from './xml2ui';
diff --git a/packages/core/ui/builder/module-name-sanitizer.d.ts b/packages/core/ui/builder/module-name-sanitizer.d.ts
deleted file mode 100644
index d712e0f77..000000000
--- a/packages/core/ui/builder/module-name-sanitizer.d.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-/**
- * Helps sanitize a module name if it is prefixed with '~/', '~' or '/'
- * @param moduleName the name
- * @param removeExtension whether to remove extension
- */
-export function sanitizeModuleName(moduleName: string, removeExtension?: boolean): string;
diff --git a/packages/core/ui/builder/module-name-sanitizer.ts b/packages/core/ui/builder/module-name-sanitizer.ts
deleted file mode 100644
index ba34dd000..000000000
--- a/packages/core/ui/builder/module-name-sanitizer.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-/**
- * Helps sanitize a module name if it is prefixed with '~/', '~' or '/'
- * @param moduleName the name
- * @param removeExtension whether to remove extension
- */
-export function sanitizeModuleName(moduleName: string, removeExtension = true): string {
- moduleName = moduleName.trim();
-
- if (moduleName.startsWith('~/')) {
- moduleName = moduleName.substring(2);
- } else if (moduleName.startsWith('~')) {
- moduleName = moduleName.substring(1);
- } else if (moduleName.startsWith('/')) {
- moduleName = moduleName.substring(1);
- }
-
- if (removeExtension) {
- const extToRemove = ['js', 'ts', 'xml', 'html', 'css', 'scss'];
- const extensionRegEx = new RegExp(`(.*)\\.(?:${extToRemove.join('|')})`, 'i');
- moduleName = moduleName.replace(extensionRegEx, '$1');
- }
-
- return moduleName;
-}
diff --git a/packages/core/ui/core/view-base/index.ts b/packages/core/ui/core/view-base/index.ts
index 7ddb0cfa9..6be6465c6 100644
--- a/packages/core/ui/core/view-base/index.ts
+++ b/packages/core/ui/core/view-base/index.ts
@@ -208,7 +208,7 @@ export function getViewByDomId(view: ViewBaseDefinition, domId: number): ViewBas
*/
export function querySelectorAll(view: ViewBaseDefinition, selector: string): Array {
if (!view) {
- return undefined;
+ return [];
}
const retVal: Array = [];
diff --git a/packages/core/ui/core/view/view-common.ts b/packages/core/ui/core/view/view-common.ts
index c3e626a84..0c4f2020a 100644
--- a/packages/core/ui/core/view/view-common.ts
+++ b/packages/core/ui/core/view/view-common.ts
@@ -5,6 +5,7 @@ import { booleanConverter, ShowModalOptions, ViewBase } from '../view-base';
import { getEventOrGestureName } from '../bindable';
import { layout } from '../../../utils';
import { isObject } from '../../../utils/types';
+import { sanitizeModuleName } from '../../../utils/common';
import { Color } from '../../../color';
import { Property, InheritedProperty } from '../properties';
import { EventData } from '../../../data/observable';
@@ -18,7 +19,6 @@ import { observe as gestureObserve, GesturesObserver, GestureTypes, GestureEvent
import { CSSUtils } from '../../../css/system-classes';
import { Builder } from '../../builder';
-import { sanitizeModuleName } from '../../builder/module-name-sanitizer';
import { StyleScope } from '../../styling/style-scope';
import { LinearGradient } from '../../styling/linear-gradient';
diff --git a/packages/core/ui/frame/fragment.transitions.android.ts b/packages/core/ui/frame/fragment.transitions.android.ts
index 40dcf5fcf..57bb45595 100644
--- a/packages/core/ui/frame/fragment.transitions.android.ts
+++ b/packages/core/ui/frame/fragment.transitions.android.ts
@@ -8,6 +8,7 @@ import { FlipTransition } from '../transition/flip-transition';
import { _resolveAnimationCurve } from '../animation';
import lazy from '../../utils/lazy';
import { Trace } from '../../trace';
+import { SharedTransition, SharedTransitionEventData, SharedTransitionAnimationType } from '../transition/shared-transition';
interface TransitionListener {
new (entry: ExpandedEntry, transition: androidx.transition.Transition): ExpandedTransitionListener;
@@ -326,6 +327,27 @@ export function _reverseTransitions(previousEntry: ExpandedEntry, currentEntry:
return transitionUsed;
}
+function notifySharedTransition(id: number, eventName: string) {
+ const state = SharedTransition.getState(id);
+ if (!state) {
+ return;
+ }
+ SharedTransition.notifyEvent(eventName, {
+ id,
+ type: 'page',
+ action: state.activeType === SharedTransitionAnimationType.present ? 'present' : 'dismiss',
+ });
+ if (eventName === SharedTransition.finishedEvent) {
+ if (state.activeType === SharedTransitionAnimationType.present) {
+ SharedTransition.updateState(id, {
+ activeType: SharedTransitionAnimationType.dismiss,
+ });
+ } else {
+ SharedTransition.finishState(id);
+ }
+ }
+}
+
// Transition listener can't be static because
// android is cloning transitions and we can't expand them :(
function getTransitionListener(entry: ExpandedEntry, transition: androidx.transition.Transition): ExpandedTransitionListener {
@@ -347,6 +369,9 @@ function getTransitionListener(entry: ExpandedEntry, transition: androidx.transi
if (Trace.isEnabled()) {
Trace.write(`START ${toShortString(transition)} transition for ${entry.fragmentTag}`, Trace.categories.Transition);
}
+ if (entry?.transition) {
+ notifySharedTransition(entry.transition?.id, SharedTransition.startedEvent);
+ }
}
onTransitionEnd(transition: androidx.transition.Transition): void {
@@ -356,6 +381,9 @@ function getTransitionListener(entry: ExpandedEntry, transition: androidx.transi
}
entry.isAnimationRunning = false;
transitionOrAnimationCompleted(entry, this.backEntry);
+ if (entry?.transition) {
+ notifySharedTransition(entry.transition?.id, SharedTransition.finishedEvent);
+ }
}
onTransitionResume(transition: androidx.transition.Transition): void {
diff --git a/packages/core/ui/frame/frame-common.ts b/packages/core/ui/frame/frame-common.ts
index b666e586d..a7baacec3 100644
--- a/packages/core/ui/frame/frame-common.ts
+++ b/packages/core/ui/frame/frame-common.ts
@@ -8,7 +8,7 @@ import { frameStack, topmost as frameStackTopmost, _pushInFrameStack, _popFromFr
import { viewMatchesModuleContext } from '../core/view/view-common';
import { getAncestor } from '../core/view-base';
import { Builder } from '../builder';
-import { sanitizeModuleName } from '../builder/module-name-sanitizer';
+import { sanitizeModuleName } from '../../utils/common';
import { profile } from '../../profiling';
import { FRAME_SYMBOL } from './frame-helpers';
import { SharedTransition } from '../transition/shared-transition';
@@ -401,8 +401,13 @@ export class FrameBase extends CustomLayoutView {
this._onNavigatingTo(backstackEntry, isBackNavigation);
const navigationTransition = this._getNavigationTransition(backstackEntry.entry);
if (navigationTransition?.instance) {
+ const state = SharedTransition.getState(navigationTransition?.instance.id);
SharedTransition.updateState(navigationTransition?.instance.id, {
- page: this.currentPage,
+ // Allow setting custom page context to override default (from) page
+ // helpful for deeply nested frame navigation setups (eg: Nested Tab Navigation)
+ // when sharing elements in this condition, the (from) page would
+ // get overridden on each frame preventing shared element matching
+ page: state?.page || this.currentPage,
toPage: this,
});
}
diff --git a/packages/core/ui/frame/index.android.ts b/packages/core/ui/frame/index.android.ts
index fd75499d9..ae666c45d 100644
--- a/packages/core/ui/frame/index.android.ts
+++ b/packages/core/ui/frame/index.android.ts
@@ -468,12 +468,6 @@ export class Frame extends FrameBase {
navigationTransition?.instance?.androidFragmentTransactionCallback?.(transaction, currentEntry, newEntry);
transaction.commitAllowingStateLoss();
-
- if (navigationTransition?.instance) {
- SharedTransition.updateState(navigationTransition?.instance?.id, {
- activeType: SharedTransitionAnimationType.dismiss,
- });
- }
}
public _goBackCore(backstackEntry: BackstackEntry & ExpandedEntry) {
@@ -499,10 +493,6 @@ export class Frame extends FrameBase {
backstackEntry.transition?.androidFragmentTransactionCallback?.(transaction, this._currentEntry, backstackEntry);
transaction.commitAllowingStateLoss();
-
- if (backstackEntry?.transition) {
- SharedTransition.finishState(backstackEntry.transition.id);
- }
}
public _removeEntry(removed: BackstackEntry): void {
diff --git a/packages/core/ui/frame/index.ios.ts b/packages/core/ui/frame/index.ios.ts
index 216e2bcf7..f01dced4c 100644
--- a/packages/core/ui/frame/index.ios.ts
+++ b/packages/core/ui/frame/index.ios.ts
@@ -5,7 +5,7 @@ import { Page } from '../page';
import { View } from '../core/view';
import { IOSHelper } from '../core/view/view-helper';
import { profile } from '../../profiling';
-import { iOSNativeHelper, layout } from '../../utils';
+import { CORE_ANIMATION_DEFAULTS, iOSNativeHelper, layout } from '../../utils';
import { Trace } from '../../trace';
import type { PageTransition } from '../transition/page-transition';
import { SlideTransition } from '../transition/slide-transition';
@@ -391,8 +391,6 @@ class TransitionDelegate extends NSObject {
};
}
-const _defaultTransitionDuration = 0.35;
-
@NativeClass
class UINavigationControllerAnimatedDelegate extends NSObject implements UINavigationControllerDelegate {
public static ObjCProtocols = [UINavigationControllerDelegate];
@@ -499,7 +497,7 @@ class UINavigationControllerImpl extends UINavigationController {
}
private animateWithDuration(navigationTransition: NavigationTransition, nativeTransition: UIViewAnimationTransition, transitionType: string, baseCallback: Function): void {
- const duration = navigationTransition.duration ? navigationTransition.duration / 1000 : _defaultTransitionDuration;
+ const duration = navigationTransition.duration ? navigationTransition.duration / 1000 : CORE_ANIMATION_DEFAULTS.duration;
const curve = _getNativeCurve(navigationTransition);
const transitionTraced = Trace.isCategorySet(Trace.categories.Transition);
diff --git a/packages/core/ui/index.ts b/packages/core/ui/index.ts
index afa3ebb73..149f572ab 100644
--- a/packages/core/ui/index.ts
+++ b/packages/core/ui/index.ts
@@ -5,7 +5,7 @@ export type { AnimationDefinition } from './animation';
export { Builder } from './builder';
export type { LoadOptions } from './builder';
export type { ComponentModule } from './builder/component-builder';
-export { sanitizeModuleName } from './builder/module-name-sanitizer';
+export { sanitizeModuleName } from '../utils/common';
export { Button } from './button';
export { ContentView } from './content-view';
export { Binding } from './core/bindable';
@@ -81,6 +81,6 @@ export { FadeTransition } from './transition/fade-transition';
export { SlideTransition } from './transition/slide-transition';
export { SharedTransition, SharedTransitionAnimationType } from './transition/shared-transition';
export { SharedTransitionHelper } from './transition/shared-transition-helper';
-export type { SharedTransitionConfig } from './transition/shared-transition';
+export type { SharedTransitionConfig, SharedTransitionTagProperties } from './transition/shared-transition';
export { WebView } from './web-view';
export type { LoadEventData, WebViewNavigationType } from './web-view';
diff --git a/packages/core/ui/styling/style-scope.ts b/packages/core/ui/styling/style-scope.ts
index 412e183f2..0ee744571 100644
--- a/packages/core/ui/styling/style-scope.ts
+++ b/packages/core/ui/styling/style-scope.ts
@@ -19,7 +19,7 @@ function ensureKeyframeAnimationModule() {
}
import * as capm from './css-animation-parser';
-import { sanitizeModuleName } from '../builder/module-name-sanitizer';
+import { sanitizeModuleName } from '../../utils/common';
import { resolveModuleName } from '../../module-name-resolver';
import { cleanupImportantFlags } from './css-utils';
diff --git a/packages/core/ui/transition/fade-transition.ios.ts b/packages/core/ui/transition/fade-transition.ios.ts
index c7bd355df..8baebebbe 100644
--- a/packages/core/ui/transition/fade-transition.ios.ts
+++ b/packages/core/ui/transition/fade-transition.ios.ts
@@ -1,5 +1,5 @@
-import { Transition } from '.';
-import { DEFAULT_DURATION } from './shared-transition';
+import { CORE_ANIMATION_DEFAULTS } from '../../utils/common';
+import { Transition } from '.';
export class FadeTransition extends Transition {
transitionController: FadeTransitionController;
@@ -33,7 +33,7 @@ export class FadeTransitionController extends NSObject implements UIViewControll
if (owner) {
return owner.getDuration();
}
- return DEFAULT_DURATION;
+ return CORE_ANIMATION_DEFAULTS.duration;
}
animateTransition(transitionContext: UIViewControllerContextTransitioning): void {
diff --git a/packages/core/ui/transition/index.d.ts b/packages/core/ui/transition/index.d.ts
index 25ab068ae..cb5196366 100644
--- a/packages/core/ui/transition/index.d.ts
+++ b/packages/core/ui/transition/index.d.ts
@@ -1,6 +1,16 @@
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 SharedTransitionTagPropertiesToMatch = {
+ /**
+ * View related properties
+ */
+ view?: Array;
+ /**
+ * For iOS, can be specific if CALayer related properties
+ */
+ layer?: Array;
+};
+export type SharedElementSettings = { view: View; startFrame: any; endFrame?: any; startOpacity?: number; endOpacity?: number; scale?: { x?: number; y?: number }; zIndex?: number; startTransform?: any; snapshot?: any; propertiesToMatch?: SharedTransitionTagPropertiesToMatch };
export type TransitionNavigationType = 'page' | 'modal';
export interface TransitionInteractiveState {
started?: false;
@@ -11,6 +21,10 @@ export interface TransitionInteractiveState {
export declare class Transition {
id: number;
+ /**
+ * (Optional) Provide a unique name to identify this transition
+ */
+ name?: string;
transitionController?: any;
interactiveController?: any;
presented?: any;
diff --git a/packages/core/ui/transition/index.ios.ts b/packages/core/ui/transition/index.ios.ts
index 4d4bc06f1..8a868334b 100644
--- a/packages/core/ui/transition/index.ios.ts
+++ b/packages/core/ui/transition/index.ios.ts
@@ -1,4 +1,5 @@
-import type { Transition as TransitionType } from '.';
+import { CORE_ANIMATION_DEFAULTS } from '../../utils/common';
+import type { Transition as TransitionType } from '.';
let transitionId = 0;
export class Transition implements TransitionType {
@@ -8,7 +9,7 @@ export class Transition implements TransitionType {
private _curve: UIViewAnimationCurve;
constructor(duration: number = 350, nativeCurve: UIViewAnimationCurve = UIViewAnimationCurve.EaseInOut) {
- this._duration = duration ? duration / 1000 : 0.35;
+ this._duration = duration ? duration / 1000 : CORE_ANIMATION_DEFAULTS.duration;
this._curve = nativeCurve;
transitionId++;
this.id = transitionId;
diff --git a/packages/core/ui/transition/modal-transition.ios.ts b/packages/core/ui/transition/modal-transition.ios.ts
index 555aaf86c..3eb6e5fe3 100644
--- a/packages/core/ui/transition/modal-transition.ios.ts
+++ b/packages/core/ui/transition/modal-transition.ios.ts
@@ -1,7 +1,8 @@
import type { View } from '../core/view';
+import { CORE_ANIMATION_DEFAULTS, getDurationWithDampingFromSpring } from '../../utils/common';
import { isNumber } from '../../utils/types';
import { Transition, SharedElementSettings, TransitionInteractiveState } from '.';
-import { SharedTransition, DEFAULT_DURATION } from './shared-transition';
+import { SharedTransition, SharedTransitionAnimationType } from './shared-transition';
import { SharedTransitionHelper } from './shared-transition-helper';
import { PanGestureEventData, GestureStateTypes } from '../gestures';
@@ -162,7 +163,26 @@ class ModalTransitionController extends NSObject implements UIViewControllerAnim
}
transitionDuration(transitionContext: UIViewControllerContextTransitioning): number {
- return DEFAULT_DURATION;
+ const owner = this.owner.deref();
+ if (owner) {
+ const state = SharedTransition.getState(owner.id);
+ switch (state?.activeType) {
+ case SharedTransitionAnimationType.present:
+ if (isNumber(state?.pageEnd?.duration)) {
+ return state.pageEnd?.duration / 1000;
+ } else {
+ return getDurationWithDampingFromSpring(state.pageEnd?.spring).duration;
+ }
+
+ case SharedTransitionAnimationType.dismiss:
+ if (isNumber(state?.pageReturn?.duration)) {
+ return state.pageReturn?.duration / 1000;
+ } else {
+ return getDurationWithDampingFromSpring(state.pageReturn?.spring).duration;
+ }
+ }
+ }
+ return CORE_ANIMATION_DEFAULTS.duration;
}
animateTransition(transitionContext: UIViewControllerContextTransitioning): void {
diff --git a/packages/core/ui/transition/page-transition.android.ts b/packages/core/ui/transition/page-transition.android.ts
index 9b463c5c1..cb33dc208 100644
--- a/packages/core/ui/transition/page-transition.android.ts
+++ b/packages/core/ui/transition/page-transition.android.ts
@@ -3,11 +3,13 @@ import { ViewBase } from '../core/view-base';
import { BackstackEntry } from '../frame';
import { isNumber } from '../../utils/types';
import { FadeTransition } from './fade-transition';
-import { SharedTransition, SharedTransitionAnimationType } from './shared-transition';
+import { Transition } from '.';
+import { getPageStartDefaultsForType, getRectFromProps, SharedTransition, SharedTransitionAnimationType, SharedTransitionEventData } from './shared-transition';
import { ImageSource } from '../../image-source';
import { ContentView } from '../content-view';
import { GridLayout } from '../layouts/grid-layout';
import { ad } from '../../utils';
+import { Screen } from '../../platform';
// import { Image } from '../image';
@NativeClass
@@ -74,7 +76,7 @@ function setTransitionName(view: ViewBase) {
}
}
-export class PageTransition extends FadeTransition {
+export class PageTransition extends Transition {
constructor(duration?: number, curve?: any) {
// disable custom curves until we can fix the issue with the animation not completing
if (curve) {
@@ -86,6 +88,89 @@ export class PageTransition extends FadeTransition {
super(duration);
}
+ createAndroidAnimator(transitionType: string) {
+ const state = SharedTransition.getState(this.id);
+ const pageStart = state.pageStart;
+ const startFrame = getRectFromProps(pageStart, {
+ x: 0,
+ y: 0,
+ width: Screen.mainScreen.widthPixels,
+ height: Screen.mainScreen.heightPixels,
+ });
+ const pageEnd = state.pageEnd;
+ const endFrame = getRectFromProps(pageEnd);
+ const pageReturn = state.pageReturn;
+ const returnFrame = getRectFromProps(pageReturn);
+
+ let customDuration = -1;
+ if (state.activeType === SharedTransitionAnimationType.present && isNumber(pageEnd?.duration)) {
+ customDuration = pageEnd.duration;
+ } else if (isNumber(state.pageReturn?.duration)) {
+ customDuration = state.pageReturn.duration;
+ }
+
+ const animationSet = new android.animation.AnimatorSet();
+ animationSet.setDuration(customDuration > -1 ? customDuration : this.getDuration());
+
+ const alphaValues = Array.create('float', 2);
+ const translationXValues = Array.create('float', 2);
+ const translationYValues = Array.create('float', 2);
+ switch (transitionType) {
+ case Transition.AndroidTransitionType.enter:
+ // incoming page (to)
+ alphaValues[0] = isNumber(pageStart?.opacity) ? pageStart?.opacity : 0;
+ alphaValues[1] = isNumber(pageEnd?.opacity) ? pageEnd?.opacity : 1;
+ translationYValues[0] = startFrame.y;
+ translationYValues[1] = endFrame.y;
+ translationXValues[0] = startFrame.x;
+ translationXValues[1] = endFrame.x;
+ break;
+ case Transition.AndroidTransitionType.exit:
+ // current page (from)
+ alphaValues[0] = 1;
+ alphaValues[1] = 0;
+ translationYValues[0] = 0;
+ translationYValues[1] = 0;
+ break;
+ case Transition.AndroidTransitionType.popEnter:
+ // current page (returning to)
+ alphaValues[0] = 0;
+ alphaValues[1] = 1;
+ break;
+ case Transition.AndroidTransitionType.popExit:
+ // removing page (to)
+ alphaValues[0] = isNumber(pageEnd?.opacity) ? pageEnd?.opacity : 1;
+ alphaValues[1] = isNumber(pageStart?.opacity) ? pageStart?.opacity : 0;
+ translationYValues[0] = endFrame.y;
+ translationYValues[1] = startFrame.y;
+ translationXValues[0] = endFrame.x;
+ translationXValues[1] = startFrame.x;
+ break;
+ }
+ const properties = {
+ alpha: alphaValues,
+ translationX: translationXValues,
+ translationY: translationYValues,
+ };
+
+ const animations = new java.util.HashSet();
+ for (const prop in properties) {
+ // console.log(prop, ' ', properties[prop][1]);
+ const animator = android.animation.ObjectAnimator.ofFloat(null, prop, properties[prop]);
+ if (customDuration) {
+ // duration always overrides default spring
+ animator.setInterpolator(new CustomLinearInterpolator());
+ } else {
+ animator.setInterpolator(new CustomSpringInterpolator());
+ }
+ animations.add(animator);
+ }
+
+ animationSet.playTogether(animations);
+
+ return animationSet;
+ }
+
androidFragmentTransactionCallback(fragmentTransaction: androidx.fragment.app.FragmentTransaction, currentEntry: BackstackEntry, newEntry: BackstackEntry) {
const fromPage = currentEntry.resolvedPage;
const toPage = newEntry.resolvedPage;
@@ -151,9 +236,10 @@ export class PageTransition extends FadeTransition {
}
const transitionSet = new androidx.transition.TransitionSet();
- transitionSet.setDuration(customDuration || this.getDuration());
+ transitionSet.setDuration(customDuration > -1 ? customDuration : this.getDuration());
transitionSet.addTransition(new androidx.transition.ChangeBounds());
transitionSet.addTransition(new androidx.transition.ChangeTransform());
+ transitionSet.setOrdering(androidx.transition.TransitionSet.ORDERING_TOGETHER);
if (customDuration) {
// duration always overrides default spring
diff --git a/packages/core/ui/transition/page-transition.ios.ts b/packages/core/ui/transition/page-transition.ios.ts
index 3d48d7176..a0693a1db 100644
--- a/packages/core/ui/transition/page-transition.ios.ts
+++ b/packages/core/ui/transition/page-transition.ios.ts
@@ -1,8 +1,9 @@
import type { View } from '../core/view';
import { SharedElementSettings, TransitionInteractiveState, Transition } from '.';
import { isNumber } from '../../utils/types';
+import { CORE_ANIMATION_DEFAULTS, getDurationWithDampingFromSpring } from '../../utils/common';
import { PanGestureEventData, GestureStateTypes } from '../gestures';
-import { SharedTransition, DEFAULT_DURATION } from './shared-transition';
+import { SharedTransition, SharedTransitionAnimationType } from './shared-transition';
import { SharedTransitionHelper } from './shared-transition-helper';
export class PageTransition extends Transition {
@@ -67,10 +68,7 @@ export class PageTransition extends Transition {
const state = SharedTransition.getState(this.id);
if (!state) {
// cleanup and exit, already shutdown
- if (this._interactiveGestureTeardown) {
- this._interactiveGestureTeardown();
- this._interactiveGestureTeardown = null;
- }
+ this._teardownGesture();
return;
}
@@ -100,10 +98,7 @@ export class PageTransition extends Transition {
if (this.interactiveController) {
const finishThreshold = isNumber(state.interactive?.dismiss?.finishThreshold) ? state.interactive.dismiss.finishThreshold : 0.5;
if (percent > finishThreshold) {
- if (this._interactiveGestureTeardown) {
- this._interactiveGestureTeardown();
- this._interactiveGestureTeardown = null;
- }
+ this._teardownGesture();
this.interactiveController.finishInteractiveTransition();
} else {
SharedTransition.updateState(this.id, {
@@ -116,6 +111,13 @@ export class PageTransition extends Transition {
}
}
}
+
+ private _teardownGesture() {
+ if (this._interactiveGestureTeardown) {
+ this._interactiveGestureTeardown();
+ this._interactiveGestureTeardown = null;
+ }
+ }
}
@NativeClass()
@@ -185,9 +187,24 @@ class PageTransitionController extends NSObject implements UIViewControllerAnima
transitionDuration(transitionContext: UIViewControllerContextTransitioning): number {
const owner = this.owner.deref();
if (owner) {
- return owner.getDuration();
+ const state = SharedTransition.getState(owner.id);
+ switch (state?.activeType) {
+ case SharedTransitionAnimationType.present:
+ if (isNumber(state?.pageEnd?.duration)) {
+ return state.pageEnd?.duration / 1000;
+ } else {
+ return getDurationWithDampingFromSpring(state.pageEnd?.spring).duration;
+ }
+
+ case SharedTransitionAnimationType.dismiss:
+ if (isNumber(state?.pageReturn?.duration)) {
+ return state.pageReturn?.duration / 1000;
+ } else {
+ return getDurationWithDampingFromSpring(state.pageReturn?.spring).duration;
+ }
+ }
}
- return DEFAULT_DURATION;
+ return CORE_ANIMATION_DEFAULTS.duration;
}
animateTransition(transitionContext: UIViewControllerContextTransitioning): void {
diff --git a/packages/core/ui/transition/shared-transition-helper.ios.ts b/packages/core/ui/transition/shared-transition-helper.ios.ts
index ea5f89358..b0d0f6884 100644
--- a/packages/core/ui/transition/shared-transition-helper.ios.ts
+++ b/packages/core/ui/transition/shared-transition-helper.ios.ts
@@ -1,7 +1,8 @@
import type { TransitionInteractiveState, TransitionNavigationType } from '.';
-import { getPageStartDefaultsForType, getRectFromProps, getSpringFromProps, SharedTransition, SharedTransitionAnimationType, SharedTransitionEventData, SharedTransitionState } from './shared-transition';
+import { getPageStartDefaultsForType, getRectFromProps, getSpringFromProps, SharedTransition, SharedTransitionAnimationType, SharedTransitionState } from './shared-transition';
import { isNumber } from '../../utils/types';
import { Screen } from '../../platform';
+import { CORE_ANIMATION_DEFAULTS } from '../../utils/common';
import { iOSNativeHelper } from '../../utils/native-helper';
interface PlatformTransitionInteractiveState extends TransitionInteractiveState {
@@ -12,20 +13,17 @@ interface PlatformTransitionInteractiveState extends TransitionInteractiveState
export class SharedTransitionHelper {
static animate(state: SharedTransitionState, transitionContext: UIViewControllerContextTransitioning, type: TransitionNavigationType) {
const transition = state.instance;
- setTimeout(() => {
+ setTimeout(async () => {
// 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,
- data: {
- id: transition.id,
- type,
- action: 'present',
- },
+ SharedTransition.notifyEvent(SharedTransition.startedEvent, {
+ id: transition.id,
+ type,
+ action: 'present',
});
if (type === 'modal') {
@@ -36,6 +34,7 @@ export class SharedTransitionHelper {
transition.presented.view.layoutIfNeeded();
const { sharedElements, presented, presenting } = SharedTransition.getSharedElements(state.page, state.toPage);
+ const sharedElementTags = sharedElements.map((v) => v.sharedTransitionTag);
if (!transition.sharedElements) {
transition.sharedElements = {
presented: [],
@@ -46,10 +45,7 @@ export class SharedTransitionHelper {
if (SharedTransition.DEBUG) {
console.log(` ${type}: Present`);
- console.log(
- `1. Found sharedTransitionTags to animate:`,
- sharedElements.map((v) => v.sharedTransitionTag)
- );
+ console.log(`1. Found sharedTransitionTags to animate:`, sharedElementTags);
console.log(`2. Take snapshots of shared elements and position them based on presenting view:`);
}
@@ -59,142 +55,182 @@ export class SharedTransitionHelper {
const startFrame = getRectFromProps(pageStart, getPageStartDefaultsForType(type));
const pageEnd = state.pageEnd;
- const pageEndIndependentTags = Object.keys(pageEnd?.sharedTransitionTags || {});
+ const pageEndTags = pageEnd?.sharedTransitionTags || {};
// console.log('pageEndIndependentTags:', pageEndIndependentTags);
- for (const presentingView of sharedElements) {
- const presentingSharedElement = presentingView.ios;
- // console.log('fromTarget instanceof UIImageView:', fromTarget instanceof UIImageView)
+ const positionSharedTags = async () => {
+ for (const presentingView of sharedElements) {
+ 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)
+ // 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 presentedView = presented.find((v) => v.sharedTransitionTag === presentingView.sharedTransitionTag);
+ const presentedSharedElement = presentedView.ios;
+ const pageEndProps = pageEndTags[presentingView.sharedTransitionTag];
- const snapshot = UIImageView.alloc().init();
+ const snapshot = UIImageView.alloc().init();
+ if (pageEndProps?.callback) {
+ await pageEndProps?.callback(presentedView, 'present');
+ }
+
+ // 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;
+ });
- // 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.contentMode = presentedSharedElement.contentMode;
+ }
+
+ iOSNativeHelper.copyLayerProperties(snapshot, presentingSharedElement, pageEndProps?.propertiesToMatch);
+ 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: isNumber(pageEndProps?.opacity) ? pageEndProps.opacity : presentedView.opacity,
+ propertiesToMatch: pageEndProps?.propertiesToMatch,
+ zIndex: isNumber(pageEndProps?.zIndex) ? pageEndProps.zIndex : 0,
+ });
+ transition.sharedElements.presented.push({
+ view: presentedView,
+ startFrame: endFrame,
+ endFrame: startFrame,
+ startOpacity: presentedView.opacity,
+ endOpacity: presentingView.opacity,
+ propertiesToMatch: pageEndProps?.propertiesToMatch,
});
- snapshot.tintColor = presentedSharedElement.tintColor;
- snapshot.contentMode = presentedSharedElement.contentMode;
+ // 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;
}
+ };
- iOSNativeHelper.copyLayerProperties(snapshot, presentingSharedElement);
- snapshot.clipsToBounds = true;
- // console.log('---> snapshot: ', snapshot);
+ const positionIndependentTags = async () => {
+ // independent tags
+ for (const tag in pageEndTags) {
+ // only handle if independent (otherwise it's shared between both pages and handled above)
+ if (!sharedElementTags.includes(tag)) {
+ // only consider start when there's a matching end
+ const pageStartIndependentProps = pageStart?.sharedTransitionTags ? pageStart?.sharedTransitionTags[tag] : null;
+ // console.log('start:', tag, pageStartIndependentProps);
+ const pageEndProps = pageEndTags[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;
- 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));
+ if (pageEndProps?.callback) {
+ await pageEndProps?.callback(independentView, 'present');
+ }
+
+ 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(pageEndProps);
+
+ 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: pageEndProps.scale,
+ startOpacity: independentView.opacity,
+ endOpacity: isNumber(pageEndProps.opacity) ? pageEndProps.opacity : 0,
+ propertiesToMatch: pageEndProps?.propertiesToMatch,
+ zIndex: isNumber(pageEndProps?.zIndex) ? pageEndProps.zIndex : 0,
+ });
+
+ independentView.opacity = 0;
+ }
}
+ };
- 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);
+ // position all sharedTransitionTag elements
+ await positionSharedTags();
+ await positionIndependentTags();
+ // combine to order by zIndex and add to transition context
+ const snapshotData = transition.sharedElements.presenting.concat(transition.sharedElements.independent);
+ snapshotData.sort((a, b) => (a.zIndex > b.zIndex ? 1 : -1));
+ if (SharedTransition.DEBUG) {
+ console.log(
+ `zIndex settings:`,
+ snapshotData.map((s) => {
+ return {
+ sharedTransitionTag: s.view.sharedTransitionTag,
+ zIndex: s.zIndex,
+ };
+ })
+ );
}
- for (const tag of pageEndIndependentTags) {
- // only consider start when there's a matching end
- const pageStartIndependentProps = pageStart?.sharedTransitionTags ? pageStart?.sharedTransitionTags[tag] : null;
- // 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;
+ for (const data of snapshotData) {
// add snapshot to animate
- transitionContext.containerView.addSubview(snapshot);
+ transitionContext.containerView.addSubview(data.snapshot);
}
// Important: always set after above shared element positions have had their start positions set
@@ -221,13 +257,10 @@ export class SharedTransitionHelper {
transition.presenting.view.removeFromSuperview();
}
transitionContext.completeTransition(true);
- SharedTransition.events().notify({
- eventName: SharedTransition.finishedEvent,
- data: {
- id: transition?.id,
- type,
- action: 'present',
- },
+ SharedTransition.notifyEvent(SharedTransition.finishedEvent, {
+ id: transition.id,
+ type,
+ action: 'present',
});
};
@@ -256,7 +289,7 @@ export class SharedTransitionHelper {
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);
+ iOSNativeHelper.copyLayerProperties(presentingMatch.snapshot, presented.view.ios, presented.propertiesToMatch);
// create a snapshot of the presented view
presentingMatch.snapshot.image = iOSNativeHelper.snapshotView(presented.view.ios, Screen.mainScreen.scale);
// apply correct alpha
@@ -287,8 +320,10 @@ export class SharedTransitionHelper {
if (isNumber(pageEnd?.duration)) {
// override spring and use only linear animation
- UIView.animateWithDurationAnimationsCompletion(
+ UIView.animateWithDurationDelayOptionsAnimationsCompletion(
pageEnd?.duration / 1000,
+ 0,
+ UIViewAnimationOptions.CurveEaseInOut,
() => {
animateProperties();
},
@@ -312,13 +347,10 @@ export class SharedTransitionHelper {
}
case SharedTransitionAnimationType.dismiss: {
// console.log('-- Transition dismiss --');
- SharedTransition.events().notify({
- eventName: SharedTransition.startedEvent,
- data: {
- id: transition?.id,
- type,
- action: 'dismiss',
- },
+ SharedTransition.notifyEvent(SharedTransition.startedEvent, {
+ id: transition.id,
+ type,
+ action: 'dismiss',
});
if (type === 'page') {
transitionContext.containerView.insertSubviewBelowSubview(transition.presenting.view, transition.presented.view);
@@ -336,19 +368,65 @@ export class SharedTransitionHelper {
console.log(`2. Add back previously stored sharedElements to dismiss:`);
}
+ const pageEnd = state.pageEnd;
+ const pageEndTags = pageEnd?.sharedTransitionTags || {};
+ const pageReturn = state.pageReturn;
+
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);
+
+ // combine to order by zIndex and add to transition context
+ const snapshotData = transition.sharedElements.presenting.concat(transition.sharedElements.independent);
+ snapshotData.sort((a, b) => (a.zIndex > b.zIndex ? 1 : -1));
+ if (SharedTransition.DEBUG) {
+ console.log(
+ `zIndex settings:`,
+ snapshotData.map((s) => {
+ return {
+ sharedTransitionTag: s.view.sharedTransitionTag,
+ zIndex: s.zIndex,
+ };
+ })
+ );
}
- const pageReturn = state.pageReturn;
+ // first loop through all the shared elements and fire the callback
+ for (const data of snapshotData) {
+ const pageEndProps = pageEndTags[data.view.sharedTransitionTag];
+ if (pageEndProps?.callback) {
+ await pageEndProps?.callback(data.view, 'dismiss');
+ }
+ }
+
+ // now that all the callbacks had their chance to run, we can take the snapshots
+ for (const data of snapshotData) {
+ const view = data.view.ios;
+ const currentAlpha = view.alpha;
+ if (pageReturn?.useStartOpacity) {
+ // when desired, reset the alpha to the start value so the view is visible in the snapshot
+ view.alpha = data.startOpacity;
+ }
+
+ // take a new snapshot
+ data.snapshot.image = iOSNativeHelper.snapshotView(view, Screen.mainScreen.scale);
+
+ // find the currently visible view with the same sharedTransitionTag
+ const fromView = transition.sharedElements.presented.find((p) => p.view.sharedTransitionTag === data.view.sharedTransitionTag)?.view;
+ if (fromView) {
+ // match the snapshot frame to the current frame of the fromView
+ data.snapshot.frame = fromView.ios.convertRectToView(fromView.ios.bounds, transitionContext.containerView);
+ }
+
+ // snapshot has been taken, we can restore the alpha
+ view.alpha = currentAlpha;
+
+ // we recalculate the startFrame because the view might have changed its position in the background
+ data.startFrame = view.convertRectToView(view.bounds, transitionContext.containerView);
+
+ // add snapshot to animate
+ transitionContext.containerView.addSubview(data.snapshot);
+ }
const cleanupDismiss = () => {
for (const presenting of transition.sharedElements.presenting) {
@@ -362,13 +440,10 @@ export class SharedTransitionHelper {
SharedTransition.finishState(transition.id);
transition.sharedElements = null;
transitionContext.completeTransition(true);
- SharedTransition.events().notify({
- eventName: SharedTransition.finishedEvent,
- data: {
- id: transition?.id,
- type,
- action: 'dismiss',
- },
+ SharedTransition.notifyEvent(SharedTransition.finishedEvent, {
+ id: transition.id,
+ type,
+ action: 'dismiss',
});
};
@@ -383,12 +458,12 @@ export class SharedTransitionHelper {
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);
+ iOSNativeHelper.copyLayerProperties(presenting.snapshot, presenting.view.ios, presenting.propertiesToMatch);
presenting.snapshot.frame = presenting.startFrame;
presenting.snapshot.alpha = presenting.startOpacity;
if (SharedTransition.DEBUG) {
- console.log(`---> ${presenting.view.sharedTransitionTag} animate to: `, iOSNativeHelper.printCGRect(presenting.startFrame));
+ console.log(`---> ${presenting.view.sharedTransitionTag} animate to: `, iOSNativeHelper.printCGRect(presenting.snapshot.frame));
}
}
@@ -401,15 +476,17 @@ export class SharedTransitionHelper {
}
if (SharedTransition.DEBUG) {
- console.log(`---> ${independent.view.sharedTransitionTag} animate to: `, iOSNativeHelper.printCGRect(independent.startFrame));
+ console.log(`---> ${independent.view.sharedTransitionTag} animate to: `, iOSNativeHelper.printCGRect(independent.snapshot.frame));
}
}
};
if (isNumber(pageReturn?.duration)) {
// override spring and use only linear animation
- UIView.animateWithDurationAnimationsCompletion(
+ UIView.animateWithDurationDelayOptionsAnimationsCompletion(
pageReturn?.duration / 1000,
+ 0,
+ UIViewAnimationOptions.CurveEaseInOut,
() => {
animateProperties();
},
@@ -436,13 +513,10 @@ export class SharedTransitionHelper {
}
static interactiveStart(state: SharedTransitionState, interactiveState: PlatformTransitionInteractiveState, type: TransitionNavigationType) {
- SharedTransition.events().notify({
- eventName: SharedTransition.startedEvent,
- data: {
- id: state?.instance?.id,
- type,
- action: 'interactiveStart',
- },
+ SharedTransition.notifyEvent(SharedTransition.startedEvent, {
+ id: state.instance.id,
+ type,
+ action: 'interactiveStart',
});
switch (type) {
case 'page':
@@ -468,7 +542,7 @@ export class SharedTransitionHelper {
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);
+ iOSNativeHelper.copyLayerProperties(p.snapshot, p.view.ios, p.propertiesToMatch);
p.snapshot.alpha = 1;
}
@@ -477,20 +551,17 @@ export class SharedTransitionHelper {
});
}
interactiveState.propertyAnimator.fractionComplete = percent;
- SharedTransition.events().notify({
- eventName: SharedTransition.interactiveUpdateEvent,
- data: {
- id: state?.instance?.id,
- type,
- percent,
- },
+ SharedTransition.notifyEvent(SharedTransition.interactiveUpdateEvent, {
+ id: state?.instance?.id,
+ type,
+ percent,
});
}
static interactiveCancel(state: SharedTransitionState, interactiveState: PlatformTransitionInteractiveState, type: TransitionNavigationType) {
if (state?.instance && interactiveState?.added && interactiveState?.propertyAnimator) {
interactiveState.propertyAnimator.reversed = true;
- const duration = isNumber(state.pageEnd?.duration) ? state.pageEnd?.duration / 1000 : 0.35;
+ const duration = isNumber(state.pageEnd?.duration) ? state.pageEnd?.duration / 1000 : CORE_ANIMATION_DEFAULTS.duration;
interactiveState.propertyAnimator.continueAnimationWithTimingParametersDurationFactor(null, duration);
setTimeout(() => {
for (const p of state.instance.sharedElements.presented) {
@@ -504,12 +575,13 @@ export class SharedTransitionHelper {
interactiveState.added = false;
interactiveState.transitionContext.cancelInteractiveTransition();
interactiveState.transitionContext.completeTransition(false);
- SharedTransition.events().notify({
- eventName: SharedTransition.interactiveCancelledEvent,
- data: {
- id: state?.instance?.id,
- type,
- },
+ SharedTransition.updateState(state?.instance?.id, {
+ interactiveBegan: false,
+ interactiveCancelled: true,
+ });
+ SharedTransition.notifyEvent(SharedTransition.interactiveCancelledEvent, {
+ id: state?.instance?.id,
+ type,
});
}, duration * 1000);
}
@@ -519,7 +591,7 @@ export class SharedTransitionHelper {
if (state?.instance && interactiveState?.added && interactiveState?.propertyAnimator) {
interactiveState.propertyAnimator.reversed = false;
- const duration = isNumber(state.pageReturn?.duration) ? state.pageReturn?.duration / 1000 : 0.35;
+ const duration = isNumber(state.pageReturn?.duration) ? state.pageReturn?.duration / 1000 : CORE_ANIMATION_DEFAULTS.duration;
interactiveState.propertyAnimator.continueAnimationWithTimingParametersDurationFactor(null, duration);
setTimeout(() => {
for (const presenting of state.instance.sharedElements.presenting) {
@@ -532,13 +604,10 @@ export class SharedTransitionHelper {
interactiveState.added = false;
interactiveState.transitionContext.finishInteractiveTransition();
interactiveState.transitionContext.completeTransition(true);
- SharedTransition.events().notify({
- eventName: SharedTransition.finishedEvent,
- data: {
- id: state?.instance?.id,
- type,
- action: 'interactiveFinish',
- },
+ SharedTransition.notifyEvent(SharedTransition.finishedEvent, {
+ id: state?.instance?.id,
+ type,
+ action: 'interactiveFinish',
});
}, duration * 1000);
}
diff --git a/packages/core/ui/transition/shared-transition.ts b/packages/core/ui/transition/shared-transition.ts
index 7837edbc7..715efe4ea 100644
--- a/packages/core/ui/transition/shared-transition.ts
+++ b/packages/core/ui/transition/shared-transition.ts
@@ -1,28 +1,48 @@
-import type { Transition, TransitionNavigationType } from '.';
+import type { Transition, TransitionNavigationType, SharedTransitionTagPropertiesToMatch } from '.';
import { Observable } from '../../data/observable';
import { Screen } from '../../platform';
import { isNumber } from '../../utils/types';
+import { CORE_ANIMATION_DEFAULTS } from '../../utils/common';
import { querySelectorAll, ViewBase } from '../core/view-base';
import type { View } from '../core/view';
import type { PanGestureEventData } from '../gestures';
-export const DEFAULT_DURATION = 0.35;
-export const DEFAULT_SPRING = {
- tension: 140,
- friction: 10,
- mass: 1,
- velocity: 0,
- delay: 0,
-};
// always increment when adding new transitions to be able to track their state
export enum SharedTransitionAnimationType {
present,
dismiss,
}
type SharedTransitionEventAction = 'present' | 'dismiss' | 'interactiveStart' | 'interactiveFinish';
-export type SharedTransitionEventData = { eventName: string; data: { id: number; type: TransitionNavigationType; action?: SharedTransitionEventAction; percent?: number } };
+export type SharedTransitionEventDataPayload = { id: number; type: TransitionNavigationType; action?: SharedTransitionEventAction; percent?: number };
+export type SharedTransitionEventData = { eventName: string; data: SharedTransitionEventDataPayload };
export type SharedRect = { x?: number; y?: number; width?: number; height?: number };
-export type SharedProperties = SharedRect & { opacity?: number; scale?: { x?: number; y?: number } };
+export type SharedProperties = SharedRect & {
+ opacity?: number;
+ scale?: { x?: number; y?: number };
+};
+/**
+ * Properties which can be set on individual Shared Elements
+ */
+export type SharedTransitionTagProperties = SharedProperties & {
+ /**
+ * The visual stacking order where 0 is at the bottom.
+ * Shared elements are stacked one on top of the other during each transition.
+ * By default they are not ordered in any particular fashion.
+ */
+ zIndex?: number;
+ /**
+ * Collection of properties to match and animate on each shared element.
+ *
+ * Defaults to: 'backgroundColor', 'cornerRadius', 'borderWidth', 'borderColor'
+ *
+ * Tip: Using an empty array, [], for view or layer will avoid copying any properties if desired.
+ */
+ propertiesToMatch?: SharedTransitionTagPropertiesToMatch;
+ /**
+ *
+ */
+ callback?: (view: View, action: SharedTransitionEventAction) => Promise;
+};
export type SharedSpringProperties = {
tension?: number;
friction?: number;
@@ -36,7 +56,7 @@ type SharedTransitionPageProperties = SharedProperties & {
* (iOS Only) Allow "independent" elements found only on one of the screens to take part in the animation.
* Note: This feature will be brought to Android in a future release.
*/
- sharedTransitionTags?: { [key: string]: SharedProperties };
+ sharedTransitionTags?: { [key: string]: SharedTransitionTagProperties };
/**
* Spring animation settings.
* Defaults to 140 tension with 10 friction.
@@ -87,7 +107,14 @@ export interface SharedTransitionConfig {
/**
* View settings to return to the original page with.
*/
- pageReturn?: SharedTransitionPageWithDurationProperties;
+ pageReturn?: SharedTransitionPageWithDurationProperties & {
+ /**
+ * In some cases you may want the returning animation to start with the original opacity,
+ * instead of beginning where it ended up on pageEnd.
+ * Note: you can try enabling this property in cases where your return animation doesn't appear correct.
+ */
+ useStartOpacity?: boolean;
+ };
}
export interface SharedTransitionState extends SharedTransitionConfig {
/**
@@ -137,10 +164,18 @@ export class SharedTransition {
});
const pageEnd = options?.pageEnd;
if (isNumber(pageEnd?.duration)) {
- transition.setDuration(pageEnd?.duration);
+ // Android uses milliseconds/iOS uses seconds
+ // users pass in milliseconds
+ transition.setDuration(global.isIOS ? pageEnd?.duration / 1000 : pageEnd?.duration);
}
return { instance: transition };
}
+ /**
+ * Whether a transition is in progress or not.
+ * Note: used internally however exposed in case custom state ordering is needed.
+ * Updated when transitions start/end/cancel.
+ */
+ static inProgress: boolean;
/**
* Listen to various shared element transition events.
* @returns Observable
@@ -168,6 +203,29 @@ export class SharedTransition {
*/
static interactiveUpdateEvent = 'SharedTransitionInteractiveUpdateEvent';
+ /**
+ * Notify a Shared Transition event.
+ * @param id transition instance id
+ * @param eventName Shared Transition event name
+ * @param type TransitionNavigationType
+ * @param action SharedTransitionEventAction
+ */
+ static notifyEvent(eventName: string, data: SharedTransitionEventDataPayload) {
+ switch (eventName) {
+ case SharedTransition.startedEvent:
+ case SharedTransition.interactiveUpdateEvent:
+ SharedTransition.inProgress = true;
+ break;
+ default:
+ SharedTransition.inProgress = false;
+ break;
+ }
+ SharedTransition.events().notify({
+ eventName,
+ data,
+ });
+ }
+
/**
* Enable to see various console logging output of Shared Element Transition behavior.
*/
@@ -224,11 +282,11 @@ export class SharedTransition {
presenting: Array;
} {
// 1. Presented view: gather all sharedTransitionTag views
- const presentedSharedElements = >querySelectorAll(toPage, 'sharedTransitionTag').filter((v) => !v.sharedTransitionIgnore);
+ const presentedSharedElements = >querySelectorAll(toPage, 'sharedTransitionTag').filter((v) => !v.sharedTransitionIgnore && typeof v.sharedTransitionTag === 'string');
// console.log('presented sharedTransitionTag total:', presentedSharedElements.length);
// 2. Presenting view: gather all sharedTransitionTag views
- const presentingSharedElements = >querySelectorAll(fromPage, 'sharedTransitionTag').filter((v) => !v.sharedTransitionIgnore);
+ const presentingSharedElements = >querySelectorAll(fromPage, 'sharedTransitionTag').filter((v) => !v.sharedTransitionIgnore && typeof v.sharedTransitionTag === 'string');
// console.log(
// 'presenting sharedTransitionTags:',
// presentingSharedElements.map((v) => v.sharedTransitionTag)
@@ -254,8 +312,8 @@ export function getRectFromProps(props: SharedTransitionPageProperties, defaults
defaults = {
x: 0,
y: 0,
- width: Screen.mainScreen.widthDIPs,
- height: Screen.mainScreen.heightDIPs,
+ width: getPlatformWidth(),
+ height: getPlatformHeight(),
...(defaults || {}),
};
return {
@@ -273,11 +331,11 @@ export function getRectFromProps(props: SharedTransitionPageProperties, defaults
*/
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,
+ tension: isNumber(props?.tension) ? props?.tension : CORE_ANIMATION_DEFAULTS.spring.tension,
+ friction: isNumber(props?.friction) ? props?.friction : CORE_ANIMATION_DEFAULTS.spring.friction,
+ mass: isNumber(props?.mass) ? props?.mass : CORE_ANIMATION_DEFAULTS.spring.mass,
+ velocity: isNumber(props?.velocity) ? props?.velocity : CORE_ANIMATION_DEFAULTS.spring.velocity,
+ delay: isNumber(props?.delay) ? props?.delay : 0,
};
}
@@ -288,9 +346,17 @@ export function getSpringFromProps(props: SharedSpringProperties) {
*/
export function getPageStartDefaultsForType(type: TransitionNavigationType) {
return {
- x: type === 'page' ? Screen.mainScreen.widthDIPs : 0,
- y: type === 'page' ? 0 : Screen.mainScreen.heightDIPs,
- width: Screen.mainScreen.widthDIPs,
- height: Screen.mainScreen.heightDIPs,
+ x: type === 'page' ? getPlatformWidth() : 0,
+ y: type === 'page' ? 0 : getPlatformHeight(),
+ width: getPlatformWidth(),
+ height: getPlatformHeight(),
};
}
+
+function getPlatformWidth() {
+ return global.isAndroid ? Screen.mainScreen.widthPixels : Screen.mainScreen.widthDIPs;
+}
+
+function getPlatformHeight() {
+ return global.isAndroid ? Screen.mainScreen.heightPixels : Screen.mainScreen.heightDIPs;
+}
diff --git a/packages/core/ui/transition/slide-transition.ios.ts b/packages/core/ui/transition/slide-transition.ios.ts
index ff4648750..d62a5b345 100644
--- a/packages/core/ui/transition/slide-transition.ios.ts
+++ b/packages/core/ui/transition/slide-transition.ios.ts
@@ -1,6 +1,6 @@
import { Transition } from '.';
import { Screen } from '../../platform';
-import { DEFAULT_DURATION } from './shared-transition';
+import { CORE_ANIMATION_DEFAULTS } from '../../utils/common';
export class SlideTransition extends Transition {
transitionController: SlideTransitionController;
@@ -40,7 +40,7 @@ export class SlideTransitionController extends NSObject implements UIViewControl
if (owner) {
return owner.getDuration();
}
- return DEFAULT_DURATION;
+ return CORE_ANIMATION_DEFAULTS.duration;
}
animateTransition(transitionContext: UIViewControllerContextTransitioning): void {
diff --git a/packages/core/utils/common.ts b/packages/core/utils/common.ts
index 20cadd7a3..85c8cf5c8 100644
--- a/packages/core/utils/common.ts
+++ b/packages/core/utils/common.ts
@@ -1,6 +1,5 @@
import * as types from './types';
import { dispatchToMainThread, dispatchToUIThread, isMainThread } from './mainthread-helper';
-import { sanitizeModuleName } from '../ui/builder/module-name-sanitizer';
import emojiRegex from 'emoji-regex';
import { GC } from './index';
@@ -43,6 +42,31 @@ export function getModuleName(path: string): string {
return sanitizeModuleName(moduleName);
}
+/**
+ * Helps sanitize a module name if it is prefixed with '~/', '~' or '/'
+ * @param moduleName the name
+ * @param removeExtension whether to remove extension
+ */
+export function sanitizeModuleName(moduleName: string, removeExtension = true): string {
+ moduleName = moduleName.trim();
+
+ if (moduleName.startsWith('~/')) {
+ moduleName = moduleName.substring(2);
+ } else if (moduleName.startsWith('~')) {
+ moduleName = moduleName.substring(1);
+ } else if (moduleName.startsWith('/')) {
+ moduleName = moduleName.substring(1);
+ }
+
+ if (removeExtension) {
+ const extToRemove = ['js', 'ts', 'xml', 'html', 'css', 'scss'];
+ const extensionRegEx = new RegExp(`(.*)\\.(?:${extToRemove.join('|')})`, 'i');
+ moduleName = moduleName.replace(extensionRegEx, '$1');
+ }
+
+ return moduleName;
+}
+
export function isFileOrResourcePath(path: string): boolean {
if (!types.isString(path)) {
return false;
@@ -210,3 +234,55 @@ export function isEmoji(value: string): boolean {
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions/Unicode_Property_Escapes
return emojiRegex().test(value);
}
+
+/**
+ * Default animation values used throughout core
+ */
+export const CORE_ANIMATION_DEFAULTS = {
+ duration: 0.35,
+ spring: {
+ tension: 140,
+ friction: 10,
+ mass: 1,
+ velocity: 0,
+ },
+};
+
+/**
+ * Get a duration with damping value from various spring related settings.
+ * Helpful when needing to convert spring settings to isolated duration value.
+ * @param springSettings various spring settings
+ * @returns calculated duration with damping from spring settings
+ */
+export function getDurationWithDampingFromSpring(springSettings?: { tension?: number; friction?: number; mass?: number; velocity?: number }) {
+ // for convenience, default spring settings are provided
+ const opt = {
+ ...CORE_ANIMATION_DEFAULTS.spring,
+ ...(springSettings || {}),
+ };
+ 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);
+ }
+ }
+ return {
+ duration,
+ damping,
+ };
+}
diff --git a/packages/core/utils/native-helper.d.ts b/packages/core/utils/native-helper.d.ts
index cf93366e2..65fadf36f 100644
--- a/packages/core/utils/native-helper.d.ts
+++ b/packages/core/utils/native-helper.d.ts
@@ -250,8 +250,9 @@ export namespace iOSNativeHelper {
* 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
+ * @param (optional) custom properties to copy between both views
*/
- export function copyLayerProperties(view: UIView, toView: UIView): void;
+ export function copyLayerProperties(view: UIView, toView: UIView, customProperties?: { view?: Array /* Array */; layer?: Array /* Array */ }): void;
/**
* Animate views with a configurable spring effect
diff --git a/packages/core/utils/native-helper.ios.ts b/packages/core/utils/native-helper.ios.ts
index f8a5c2a43..bd49f6ef7 100644
--- a/packages/core/utils/native-helper.ios.ts
+++ b/packages/core/utils/native-helper.ios.ts
@@ -1,5 +1,6 @@
import { Color } from '../color';
import { Trace } from '../trace';
+import { CORE_ANIMATION_DEFAULTS, getDurationWithDampingFromSpring } from './common';
import { getClass, isNullOrUndefined, numberHasDecimals, numberIs64Bit } from './types';
declare let UIImagePickerControllerSourceType: any;
@@ -374,9 +375,9 @@ export namespace iOSNativeHelper {
return image;
}
- export function copyLayerProperties(view: UIView, toView: UIView) {
- const viewPropertiesToMatch: Array = ['backgroundColor'];
- const layerPropertiesToMatch: Array = ['cornerRadius', 'borderWidth', 'borderColor'];
+ export function copyLayerProperties(view: UIView, toView: UIView, customProperties?: { view?: Array; layer?: Array }) {
+ const viewPropertiesToMatch: Array = customProperties?.view || ['backgroundColor'];
+ const layerPropertiesToMatch: Array = customProperties?.layer || ['cornerRadius', 'borderWidth', 'borderColor'];
viewPropertiesToMatch.forEach((property) => {
if (view[property] !== toView[property]) {
@@ -394,40 +395,16 @@ export namespace iOSNativeHelper {
}
export function animateWithSpring(options?: { tension?: number; friction?: number; mass?: number; delay?: number; velocity?: number; animateOptions?: UIViewAnimationOptions; animations?: () => void; completion?: (finished?: boolean) => void }) {
+ // for convenience, default spring settings are provided
const opt = {
- tension: 140,
- friction: 10,
- mass: 1.0,
+ ...CORE_ANIMATION_DEFAULTS.spring,
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);
- }
- }
+ const { duration, damping } = getDurationWithDampingFromSpring(opt);
if (duration === 0) {
UIView.animateWithDurationAnimationsCompletion(0, opt.animations, opt.completion);
diff --git a/tools/assets/App_Resources/iOS/Podfile b/tools/assets/App_Resources/iOS/Podfile
new file mode 100644
index 000000000..b3c1c495f
--- /dev/null
+++ b/tools/assets/App_Resources/iOS/Podfile
@@ -0,0 +1,10 @@
+platform :ios, '13.0'
+
+post_install do |installer|
+ installer.pods_project.targets.each do |target|
+ target.build_configurations.each do |config|
+ config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '13.0'
+ config.build_settings['CODE_SIGNING_ALLOWED'] = 'NO'
+ end
+ end
+end
\ No newline at end of file
diff --git a/tools/assets/App_Resources/iOS/build.xcconfig b/tools/assets/App_Resources/iOS/build.xcconfig
index f8e9da181..7da560c68 100644
--- a/tools/assets/App_Resources/iOS/build.xcconfig
+++ b/tools/assets/App_Resources/iOS/build.xcconfig
@@ -4,4 +4,4 @@
// To build for device with Xcode 8 you need to specify your development team. More info: https://developer.apple.com/library/prerelease/content/releasenotes/DeveloperTools/RN-Xcode/Introduction.html
// DEVELOPMENT_TEAM = YOUR_TEAM_ID;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
-IPHONEOS_DEPLOYMENT_TARGET = 12.0;
+IPHONEOS_DEPLOYMENT_TARGET = 13.0;