feat(core): Shared Element Transitions (#10022)

This commit is contained in:
Nathan Walker
2023-03-28 11:04:29 -07:00
committed by GitHub
parent a11d577a14
commit 59369fbc19
59 changed files with 2989 additions and 927 deletions

View File

@ -1,4 +1,4 @@
import { Transition } from '@nativescript/core';
import { PageTransition, Transition } from '@nativescript/core';
export class CustomTransition extends Transition {
constructor(duration: number, curve: any) {
@ -34,3 +34,5 @@ export class CustomTransition extends Transition {
return animatorSet;
}
}
export class CustomSharedElementPageTransition extends PageTransition {}

View File

@ -1,6 +1,8 @@
import { Transition } from '@nativescript/core/ui/transition';
import { Transition, PageTransition } from '@nativescript/core';
export class CustomTransition extends Transition {
constructor();
constructor(duration: number, curve: any);
}
export class CustomSharedElementPageTransition extends PageTransition {}

View File

@ -1,20 +1,22 @@
import * as transition from '@nativescript/core/ui/transition';
import { PageTransition, SharedTransition, SharedTransitionHelper, Transition } from '@nativescript/core';
export class CustomTransition extends transition.Transition {
export class CustomTransition extends Transition {
constructor(duration: number, curve: any) {
super(duration, curve);
}
public animateIOSTransition(containerView: UIView, fromView: UIView, toView: UIView, operation: UINavigationControllerOperation, completion: (finished: boolean) => void): void {
animateIOSTransition(transitionContext: UIViewControllerContextTransitioning, fromViewCtrl: UIViewController, toViewCtrl: UIViewController, operation: UINavigationControllerOperation): void {
const toView = toViewCtrl.view;
const fromView = fromViewCtrl.view;
toView.transform = CGAffineTransformMakeScale(0, 0);
fromView.transform = CGAffineTransformIdentity;
switch (operation) {
case UINavigationControllerOperation.Push:
containerView.insertSubviewAboveSubview(toView, fromView);
transitionContext.containerView.insertSubviewAboveSubview(toView, fromView);
break;
case UINavigationControllerOperation.Pop:
containerView.insertSubviewBelowSubview(toView, fromView);
transitionContext.containerView.insertSubviewBelowSubview(toView, fromView);
break;
}
@ -27,7 +29,63 @@ export class CustomTransition extends transition.Transition {
toView.transform = CGAffineTransformIdentity;
fromView.transform = CGAffineTransformMakeScale(0, 0);
},
completion
(finished) => {
transitionContext.completeTransition(finished);
}
);
}
}
export class CustomSharedElementPageTransition extends PageTransition {
transitionController: PageTransitionController;
interactiveController: UIPercentDrivenInteractiveTransition;
presented: UIViewController;
presenting: UIViewController;
navigationController: UINavigationController;
operation: number;
iosNavigatedController(navigationController: UINavigationController, operation: number, fromVC: UIViewController, toVC: UIViewController): UIViewControllerAnimatedTransitioning {
this.navigationController = navigationController;
if (!this.transitionController) {
this.presented = toVC;
this.presenting = fromVC;
}
this.transitionController = PageTransitionController.initWithOwner(new WeakRef(this));
// console.log('iosNavigatedController presenting:', this.presenting);
this.operation = operation;
return this.transitionController;
}
}
@NativeClass()
class PageTransitionController extends NSObject implements UIViewControllerAnimatedTransitioning {
static ObjCProtocols = [UIViewControllerAnimatedTransitioning];
owner: WeakRef<PageTransition>;
static initWithOwner(owner: WeakRef<PageTransition>) {
const ctrl = <PageTransitionController>PageTransitionController.new();
ctrl.owner = owner;
return ctrl;
}
transitionDuration(transitionContext: UIViewControllerContextTransitioning): number {
const owner = this.owner.deref();
if (owner) {
return owner.getDuration();
}
return 0.35;
}
animateTransition(transitionContext: UIViewControllerContextTransitioning): void {
const owner = this.owner.deref();
if (owner) {
// console.log('--- PageTransitionController animateTransition');
const state = SharedTransition.getState(owner.id);
if (!state) {
return;
}
SharedTransitionHelper.animate(state, transitionContext, 'page');
}
}
}

View File

@ -1,10 +1,6 @@
import * as helper from '../ui-helper';
import * as platform from '@nativescript/core/platform';
import { Trace, CoreTypes } from '@nativescript/core';
import { Color } from '@nativescript/core/color';
import { NavigationEntry, NavigationTransition } from '@nativescript/core/ui/frame';
import { Page } from '@nativescript/core/ui/page';
import { CustomTransition } from './custom-transition';
import { Color, Device, CoreTypes, Trace, SharedTransition, NavigationEntry, NavigationTransition, Page, platformNames, GridLayout } from '@nativescript/core';
import { CustomTransition, CustomSharedElementPageTransition } from './custom-transition';
function _testTransition(navigationTransition: NavigationTransition) {
var testId = `Transition[${JSON.stringify(navigationTransition)}]`;
@ -36,10 +32,10 @@ export function test_Transitions() {
});
var transitions;
if (platform.Device.os === platform.platformNames.ios) {
if (Device.os === platformNames.ios) {
transitions = ['curl'];
} else {
const _sdkVersion = parseInt(platform.Device.sdkVersion);
const _sdkVersion = parseInt(Device.sdkVersion);
transitions = _sdkVersion >= 21 ? ['explode'] : [];
}
@ -55,3 +51,59 @@ export function test_Transitions() {
// helper.navigateWithEntry({ create: mainPageFactory, clearHistory: true, animated: false });
}
export function test_SharedElementTransitions() {
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));
const grid = new GridLayout();
const sharedElement = new GridLayout();
sharedElement.width = { unit: 'dip', value: 200 };
sharedElement.height = { unit: 'dip', value: 200 };
sharedElement.marginTop = 20;
sharedElement.borderRadius = 20;
sharedElement.verticalAlignment = 'top';
sharedElement.iosOverflowSafeArea = false;
sharedElement.backgroundColor = new Color('yellow');
sharedElement.sharedTransitionTag = 'testing';
grid.addChild(sharedElement);
page.content = grid;
return page;
});
const navigationTransition = SharedTransition.custom(new CustomSharedElementPageTransition());
var testId = `SharedElementTransition[${JSON.stringify(navigationTransition)}]`;
if (Trace.isEnabled()) {
Trace.write(`Testing ${testId}`, Trace.categories.Test);
}
var navigationEntry: NavigationEntry = {
create: function (): 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));
const grid = new GridLayout();
const sharedElement = new GridLayout();
sharedElement.width = { unit: 'dip', value: 60 };
sharedElement.height = { unit: 'dip', value: 60 };
sharedElement.marginTop = 20;
sharedElement.borderRadius = 30;
sharedElement.verticalAlignment = 'top';
sharedElement.iosOverflowSafeArea = false;
sharedElement.backgroundColor = new Color('purple');
sharedElement.sharedTransitionTag = 'testing';
grid.addChild(sharedElement);
page.content = grid;
return page;
},
animated: true,
transition: navigationTransition,
};
helper.navigateWithEntry(navigationEntry);
}

View File

@ -20,6 +20,7 @@
<Button text="scroll-view" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo" />
<Button text="switch" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo" />
<Button text="touch-animations" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo" />
<Button text="transitions" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo" />
<Button text="vector-image" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo" />
<Button text="visibility-vs-hidden" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo" />
<Button text="fs-helper" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo" />

View File

@ -0,0 +1,32 @@
import { Observable, EventData, Page, ShowModalOptions, SharedTransition, ModalTransition, PageTransition, FadeTransition, SlideTransition } from '@nativescript/core';
let page: Page;
export function navigatingTo(args: EventData) {
page = <Page>args.object;
page.bindingContext = new TransitionsModel();
}
export class TransitionsModel extends Observable {
open(args: EventData) {
const type = (<any>args.object).type;
let moduleName: string;
switch (type) {
case '1':
moduleName = `pages/transitions/transition-example`;
break;
case '2':
moduleName = `pages/transitions/transition-page-modal-example`;
break;
}
page.frame.navigate({
moduleName,
transition: SharedTransition.custom(new PageTransition(), {
interactive: {
dismiss: {
finishThreshold: 0.5,
},
},
}),
});
}
}

View File

@ -0,0 +1,13 @@
<Page xmlns="http://schemas.nativescript.org/tns.xsd" navigatingTo="navigatingTo" class="page">
<Page.actionBar>
<ActionBar title="Transitions" icon="" class="action-bar">
</ActionBar>
</Page.actionBar>
<GridLayout rows="*,auto,*" class="p-20">
<StackLayout row="1">
<Button text="Open Testing Examples" class="btn btn-primary btn-view-demo" tap="{{open}}" type="1" />
<Button text="Open Page and Modal Example" class="btn btn-primary btn-view-demo" tap="{{open}}" type="2" />
</StackLayout>
</GridLayout>
</Page>

View File

@ -0,0 +1,37 @@
import { Observable, EventData, Page, NavigatedData } from '@nativescript/core';
let page: Page;
export function navigatingTo(args: NavigatedData) {
bindPage(args, args.context);
}
let closeCallback;
export function onShownModally(args) {
bindPage(args, args.context);
closeCallback = args.closeCallback;
}
function bindPage(args, context: any) {
page = <Page>args.object;
page.bindingContext = new TransitionsModel(context);
}
export class TransitionsModel extends Observable {
example1 = true;
example2: boolean;
example3: boolean;
dynamicTag: string;
constructor(options: { isModal?: boolean; example2?: boolean; example3?: boolean; dynamicTag?: string }) {
super();
this.example1 = options.example2 || options.example3 ? false : true;
this.example2 = options.example2;
this.example3 = options.example3;
this.dynamicTag = options.dynamicTag;
}
close() {
if (closeCallback) {
closeCallback();
}
}
}

View File

@ -0,0 +1,22 @@
<Page xmlns="http://schemas.nativescript.org/tns.xsd" navigatingTo="navigatingTo" shownModally="onShownModally" class="page">
<Page.actionBar>
<ActionBar title="Transitions Example Detail" icon="" class="action-bar">
</ActionBar>
</Page.actionBar>
<GridLayout>
<Button text="Close" tap="{{close}}" visibility="{{ isModal ? 'visible' : 'collapsed' }}" horizontalAlignment="left" verticalAlignment="top" marginTop="20" marginLeft="20" />
<ContentView width="80%" height="300" borderRadius="20" backgroundColor="green" sharedTransitionTag="fab" horizontalAlignment="center" verticalAlignment="top" marginTop="100" visibility="{{ example1 ? 'visible' : 'collapsed' }}" />
<GridLayout visibility="{{ example2 ? 'visible' : 'collapsed' }}">
<ContentView width="200" height="200" borderRadius="100" backgroundColor="purple" sharedTransitionTag="shape1" verticalAlignment="top" horizontalAlignment="right" marginRight="20" marginTop="20" iosIgnoreSafeArea="true" />
<ContentView width="80" height="80" borderRadius="40" backgroundColor="orange" sharedTransitionTag="shape2" verticalAlignment="top" horizontalAlignment="left" marginLeft="20" marginTop="20" iosIgnoreSafeArea="true" />
<ContentView width="20" height="20" borderRadius="10" backgroundColor="brown" sharedTransitionTag="shape3" verticalAlignment="bottom" horizontalAlignment="right" marginRight="20" iosIgnoreSafeArea="true" />
<ContentView width="150" height="150" borderRadius="75" backgroundColor="yellow" sharedTransitionTag="shape4" verticalAlignment="bottom" horizontalAlignment="left" marginLeft="20" iosIgnoreSafeArea="true" />
</GridLayout>
<GridLayout visibility="{{ example3 ? 'visible' : 'collapsed' }}">
<ContentView width="80%" height="200" borderRadius="20" backgroundColor="purple" sharedTransitionTag="{{dynamicTag}}" verticalAlignment="top" horizontalAlignment="center" marginRight="20" marginTop="20" iosIgnoreSafeArea="true" />
</GridLayout>
</GridLayout>
</Page>

View File

@ -0,0 +1,111 @@
import { Observable, EventData, Page, ShowModalOptions, SharedTransition, ModalTransition, PageTransition, FadeTransition, SlideTransition, PropertyChangeData } from '@nativescript/core';
let page: Page;
// SharedTransition.DEBUG = true;
export function navigatingTo(args: EventData) {
page = <Page>args.object;
page.bindingContext = new TransitionsModel();
}
let updatedSegmentValue: number;
if (typeof updatedSegmentValue === 'undefined') {
updatedSegmentValue = 0;
}
export class TransitionsModel extends Observable {
segmentSelectedIndex = updatedSegmentValue;
items = [
{
title: 'Homer',
dynamicTag: 'dynamic1',
},
{
title: 'Marge',
dynamicTag: 'dynamic2',
},
{
title: 'Bart',
dynamicTag: 'dynamic3',
},
{
title: 'Lisa',
dynamicTag: 'dynamic4',
},
{
title: 'Maggie',
dynamicTag: 'dynamic5',
},
];
constructor() {
super();
this.on(Observable.propertyChangeEvent, (data: PropertyChangeData) => {
if (data.propertyName === 'segmentSelectedIndex') {
console.log('change segmentSelectedIndex--');
console.log(data.value);
updatedSegmentValue = data.value;
this.segmentSelectedIndex = data.value;
}
});
}
open(args) {
const moduleName = `pages/transitions/transition-example-detail`;
const context: any = {
example2: !!args.object.example2,
example3: !!args.object.example3,
dynamicTag: args.object.dynamicTag,
};
page.frame.navigate({
moduleName,
context,
transition: SharedTransition.custom(new PageTransition(), {
interactive: {
dismiss: {
finishThreshold: 0.5,
},
},
// pageEnd: {
// duration: 3000
// },
// pageReturn: {
// duration: 1000
// }
}),
});
// Try modals as well:
// context.isModal = true;
// page.showModal(moduleName, {
// context,
// transition: SharedTransition.custom(new ModalTransition(), {
// interactive: {
// dismiss: {
// finishThreshold: 0.5,
// },
// },
// pageStart: {
// y: 200,
// // duration: 400,
// },
// pageReturn: {
// y: 100,
// // duration: 500,
// },
// }),
// closeCallback(args) {
// // console.log('close modal callback', args);
// },
// } as ShowModalOptions);
}
onItemTap(args) {
const item = this.items[args.index];
console.log(item);
this.open({
object: {
example3: true,
dynamicTag: item.dynamicTag,
},
});
}
}

View File

@ -0,0 +1,40 @@
<Page xmlns="http://schemas.nativescript.org/tns.xsd" navigatingTo="navigatingTo" class="page">
<Page.actionBar>
<ActionBar title="Transitions Example 1" icon="" class="action-bar">
</ActionBar>
</Page.actionBar>
<GridLayout rows="auto,*">
<SegmentedBar sharedTransitionTag="segmentbar" horizontalAlignment="center" selectedIndex="{{ segmentSelectedIndex }}" marginTop="20">
<SegmentedBarItem title="Example A" />
<SegmentedBarItem title="Example B" />
<SegmentedBarItem title="Example C" />
</SegmentedBar>
<!-- Example A: Fab -->
<ContentView visibility="{{ segmentSelectedIndex === 0 ? 'visible' : 'collapsed' }}" row="1" width="75" height="75" borderRadius="38" backgroundColor="#65adf1" sharedTransitionTag="fab" horizontalAlignment="right" verticalAlignment="bottom" marginBottom="20" marginRight="30" tap="{{open}}" sharedTransitionIgnore="{{segmentSelectedIndex!==0}}" />
<!-- Example B: Multiple Shapes in Layout Containers -->
<GridLayout row="1" visibility="{{ segmentSelectedIndex === 1 ? 'visible' : 'collapsed' }}" tap="{{open}}" marginTop="20" example2="true">
<GridLayout rows="" columns="*,auto,*,auto,*,auto,*,auto,*" verticalAlignment="bottom" marginBottom="20">
<ContentView col="1" width="40" height="40" borderRadius="20" backgroundColor="#65adf1" sharedTransitionTag="shape1" sharedTransitionIgnore="{{segmentSelectedIndex!==1}}" />
<ContentView col="3" width="40" height="40" borderRadius="20" backgroundColor="#65adf1" sharedTransitionTag="shape2" sharedTransitionIgnore="{{segmentSelectedIndex!==1}}" />
<ContentView col="5" width="40" height="40" borderRadius="20" backgroundColor="#65adf1" sharedTransitionTag="shape3" sharedTransitionIgnore="{{segmentSelectedIndex!==1}}" />
<ContentView col="7" width="40" height="40" borderRadius="20" backgroundColor="#65adf1" sharedTransitionTag="shape4" sharedTransitionIgnore="{{segmentSelectedIndex!==1}}" />
</GridLayout>
</GridLayout>
<!-- Example C: Dynamic sharedTransitionTags passed around -->
<GridLayout row="2" visibility="{{ segmentSelectedIndex === 2 ? 'visible' : 'collapsed' }}" marginTop="20" example3="true">
<ListView items="{{ items }}" itemTap="{{onItemTap}}" separatorColor="transparent">
<ListView.itemTemplate>
<GridLayout columns="auto,*" padding="8">
<ContentView marginLeft="10" width="40" height="40" borderRadius="20" backgroundColor="#65adf1" sharedTransitionTag="{{dynamicTag}}" />
<Label col="1" marginLeft="10" text="{{ title }}" />
</GridLayout>
</ListView.itemTemplate>
</ListView>
</GridLayout>
</GridLayout>
</Page>

View File

@ -0,0 +1,56 @@
import { Observable, EventData, Page, ShowModalOptions, SharedTransition, ModalTransition, PageTransition, FadeTransition, SlideTransition } from '@nativescript/core';
let page: Page;
export function navigatingTo(args: EventData) {
page = <Page>args.object;
page.bindingContext = new TransitionsModel();
}
// Could create a complete custom example which extends some of the built in options
// class SampleCustomModalTransition extends ModalTransition implements TransitionType {
// }
// SharedTransition.DEBUG = true;
export class TransitionsModel extends Observable {
open() {
page.frame.navigate({
moduleName: `pages/transitions/transitions-detail`,
transition: SharedTransition.custom(new PageTransition(), {
interactive: {
dismiss: {
finishThreshold: 0.5,
},
},
// toPageStart: {
// duration: 1000,
// },
// fromPageEnd: {
// duration: 500,
// },
}),
});
}
openModal() {
page.showModal('pages/transitions/transitions-modal', {
transition: SharedTransition.custom(new ModalTransition(), {
interactive: {
dismiss: {
finishThreshold: 0.5,
},
},
pageStart: {
y: 200,
// duration: 400,
},
pageReturn: {
y: 100,
// duration: 500,
},
}),
closeCallback(args) {
// console.log('close modal callback', args);
},
} as ShowModalOptions);
}
}

View File

@ -0,0 +1,23 @@
<Page xmlns="http://schemas.nativescript.org/tns.xsd" navigatingTo="navigatingTo" class="page">
<Page.actionBar>
<ActionBar title="Transitions" icon="" class="action-bar">
</ActionBar>
</Page.actionBar>
<GridLayout rows="*,auto,auto,*">
<GridLayout row="1" rows="auto,auto" tap="{{ open }}">
<Image sharedTransitionTag="image" src="https://cdn.pixabay.com/photo/2012/08/27/14/19/mountains-55067__340.png" width="100" />
<Label row="1" text="Open Page" class="text-center" color="black" />
</GridLayout>
<GridLayout row="2" rows="auto,auto,auto,auto" tap="{{ openModal }}" marginTop="50">
<Image sharedTransitionTag="image-modal" src="https://cdn.pixabay.com/photo/2012/08/27/14/19/mountains-55067__340.png" width="100" />
<Label row="1" text="NativeScript Rocks!" sharedTransitionTag="open-modal-label" class="text-center" color="black" />
<ContentView row="2" sharedTransitionTag="open-modal-box1" borderWidth="5" borderColor="yellow" marginTop="20" width="50" height="50" borderRadius="999" backgroundColor="purple" />
<ContentView row="3" sharedTransitionTag="open-modal-box2" marginTop="20" width="20" height="20" borderRadius="999" backgroundColor="orange" />
<ContentView row="3" sharedTransitionTag="open-modal-box3" marginTop="20" width="20" height="20" borderRadius="999" backgroundColor="red" horizontalAlignment="left" marginLeft="100" />
<ContentView row="4" sharedTransitionTag="open-modal-box4" marginTop="20" width="20" height="20" borderRadius="999" backgroundColor="pink" horizontalAlignment="right" marginRight="100" />
</GridLayout>
</GridLayout>
</Page>

View File

@ -0,0 +1,10 @@
import { Observable, EventData, Page } from '@nativescript/core';
let page: Page;
export function navigatingTo(args: EventData) {
page = <Page>args.object;
page.bindingContext = new TransitionsModel();
}
export class TransitionsModel extends Observable {}

View File

@ -0,0 +1,19 @@
<Page xmlns="http://schemas.nativescript.org/tns.xsd" navigatingTo="navigatingTo" class="page">
<Page.actionBar>
<ActionBar title="Transition Detail" icon="" class="action-bar">
</ActionBar>
</Page.actionBar>
<GridLayout rows="auto,auto,*" columns="*" verticalAlignment="top">
<Image row="1" sharedTransitionTag="image" src="https://cdn.pixabay.com/photo/2012/08/27/14/19/mountains-55067__340.png" height="230" verticalAlignment="top" marginTop="10" />
<GridLayout row="2" rows="auto,auto,auto">
<Label text="Opened Navigated Page" verticalAlignment="top" marginTop="20" class="text-center" fontSize="28" color="black" />
<ContentView row="2" sharedTransitionTag="open-modal-box1" marginTop="20" width="200" height="200" borderRadius="100" backgroundColor="purple" />
<ContentView row="1" sharedTransitionTag="open-modal-box2" marginTop="20" width="75" height="75" borderRadius="37" backgroundColor="orange" />
<ContentView row="2" sharedTransitionTag="open-modal-box3" marginTop="20" width="30" height="30" borderRadius="15" backgroundColor="red" horizontalAlignment="right" marginRight="50" />
<ContentView row="1" sharedTransitionTag="open-modal-box4" marginTop="20" width="30" height="30" borderRadius="15" backgroundColor="pink" horizontalAlignment="left" marginLeft="50" />
</GridLayout>
</GridLayout>
</Page>

View File

@ -0,0 +1,22 @@
import { Observable, ShownModallyData, LoadEventData, Page, ShowModalOptions } from '@nativescript/core';
let page: Page;
let closeCallback: Function;
export function onShownModally(args: ShownModallyData) {
closeCallback = args.closeCallback;
if (args.context) {
args.context.shownModally = true;
}
}
export function onLoaded(args: LoadEventData) {
page = args.object as Page;
page.bindingContext = new TransitionModalPage();
}
export class TransitionModalPage extends Observable {
close() {
closeCallback();
}
}

View File

@ -0,0 +1,16 @@
<Page xmlns="http://schemas.nativescript.org/tns.xsd" loaded="onLoaded" shownModally="onShownModally">
<GridLayout rows="auto,auto,*" columns="*" verticalAlignment="top">
<Button text="Close" tap="{{close}}" horizontalAlignment="right" marginRight="10" />
<Image row="1" sharedTransitionTag="image-modal" src="https://cdn.pixabay.com/photo/2012/08/27/14/19/mountains-55067__340.png" height="230" verticalAlignment="top" marginTop="10" />
<GridLayout row="2" rows="auto,auto,auto">
<Label text="Opened Modal" verticalAlignment="top" marginTop="20" class="text-center" fontSize="28" color="black" />
<ContentView row="1" sharedTransitionTag="open-modal-box2" marginTop="20" width="75" height="75" borderRadius="37" backgroundColor="orange" />
<ContentView row="2" sharedTransitionTag="open-modal-box1" marginTop="20" width="200" height="200" borderRadius="100" backgroundColor="purple" />
<ContentView row="2" sharedTransitionTag="open-modal-box3" marginTop="20" width="30" height="30" borderRadius="15" backgroundColor="red" horizontalAlignment="right" marginRight="50" />
<ContentView row="1" sharedTransitionTag="open-modal-box4" marginTop="20" width="30" height="30" borderRadius="15" backgroundColor="pink" horizontalAlignment="left" marginLeft="50" />
</GridLayout>
</GridLayout>
</Page>

View File

@ -1,4 +1,5 @@
import * as Application from '../application';
import type { ViewBase } from '../ui/core/view-base';
import type { View } from '../ui/core/view';
import { notifyAccessibilityFocusState } from './accessibility-common';
import { AccessibilityLiveRegion, AccessibilityRole, AccessibilityState, AccessibilityTrait } from './accessibility-types';

View File

@ -108,10 +108,16 @@ export class Observable {
private readonly _observers: { [eventName: string]: ListenerEntry[] } = {};
/**
* Gets the value of the specified property.
*/
public get(name: string): any {
return this[name];
}
/**
* Updates the specified property with the provided value.
*/
public set(name: string, value: any): void {
// TODO: Parameter validation
const oldValue = this[name];
@ -124,6 +130,9 @@ export class Observable {
this.notifyPropertyChange(name, newValue, oldValue);
}
/**
* Updates the specified property with the provided value and raises a property change event and a specific change event based on the property name.
*/
public setProperty(name: string, value: any): void {
const oldValue = this[name];
if (this[name] === value) {
@ -474,28 +483,6 @@ export class Observable {
}
}
export interface Observable {
/**
* Raised when a propertyChange occurs.
*/
on(event: 'propertyChange', callback: (data: EventData) => void, thisArg?: any): void;
/**
* Updates the specified property with the provided value.
*/
set(name: string, value: any): void;
/**
* Updates the specified property with the provided value and raises a property change event and a specific change event based on the property name.
*/
setProperty(name: string, value: any): void;
/**
* Gets the value of the specified property.
*/
get(name: string): any;
}
class ObservableFromObject extends Observable {
public readonly _map: Record<string, any> = {};

View File

@ -213,7 +213,7 @@ interface RequireContext {
interface WeakRef<T extends object> {
/**
* @deprecated Use deref instead with 8.4+
* @deprecated Use deref instead with 8.5+
*/
get(): T;

View File

@ -83,6 +83,36 @@ global.CFRunLoopGetMain = function () {
global.kCFRunLoopDefaultMode = 1;
global.CFRunLoopPerformBlock = function (runloop, kCFRunLoopDefaultMode, func) {};
global.CFRunLoopWakeUp = function (runloop) {};
global.NativeScriptGlobals = {
events: {
on: (args) => {},
off: (args) => {},
notify: (args) => {},
hasListeners: (args) => {},
},
};
global.CADisplayLink = function () {};
global.NSNotification = function () {};
global.UIApplicationDelegate = function () {};
global.UIResponder = function () {};
global.UIResponder.extend = function () {};
global.UIViewController = function () {};
global.UIAdaptivePresentationControllerDelegate = function () {};
global.UIPopoverPresentationControllerDelegate = function () {};
global.UIContentSizeCategoryExtraSmall = 0.5;
global.UIContentSizeCategorySmall = 0.7;
global.UIContentSizeCategoryMedium = 0.85;
global.UIContentSizeCategoryLarge = 1;
global.UIContentSizeCategoryExtraLarge = 1.15;
global.UIContentSizeCategoryExtraExtraLarge = 1.3;
global.UIContentSizeCategoryExtraExtraExtraLarge = 1.5;
global.UIContentSizeCategoryAccessibilityMedium = 2;
global.UIContentSizeCategoryAccessibilityLarge = 2.5;
global.UIContentSizeCategoryAccessibilityExtraLarge = 3;
global.UIContentSizeCategoryAccessibilityExtraExtraLarge = 3.5;
global.UIContentSizeCategoryAccessibilityExtraExtraExtraLarge = 4;
// global.UIDocumentInteractionController = {
// interactionControllerWithURL(url: any) {
// return null;

View File

@ -1,6 +1,6 @@
{
"name": "@nativescript/core",
"version": "8.4.8",
"version": "8.5.0-rc.0",
"description": "A JavaScript library providing an easy to use api for interacting with iOS and Android platform APIs.",
"main": "index",
"types": "index.d.ts",

View File

@ -1,544 +0,0 @@
import { Property, CssProperty, CssAnimationProperty, InheritedProperty } from '../properties';
import { BindingOptions } from '../bindable';
import { Observable } from '../../../data/observable';
import { Style } from '../../styling/style';
import { CoreTypes } from '../../../core-types';
import { Page } from '../../page';
import { Order, FlexGrow, FlexShrink, FlexWrapBefore, AlignSelf } from '../../layouts/flexbox-layout';
import { Length } from '../../styling/style-properties';
import { DOMNode } from '../../../debugger/dom-node';
/**
* Iterates through all child views (via visual tree) and executes a function.
* @param view - Starting view (parent container).
* @param callback - A function to execute on every child. If function returns false it breaks the iteration.
*/
export function eachDescendant(view: ViewBase, callback: (child: ViewBase) => boolean);
/**
* Gets an ancestor from a given type.
* @param view - Starting view (child view).
* @param criterion - The type of ancestor view we are looking for. Could be a string containing a class name or an actual type.
* Returns an instance of a view (if found), otherwise undefined.
*/
export function getAncestor(view: ViewBase, criterion: string | Function): ViewBase;
export function isEventOrGesture(name: string, view: ViewBase): boolean;
/**
* Gets a child view by id.
* @param view - The parent (container) view of the view to look for.
* @param id - The id of the view to look for.
* Returns an instance of a view (if found), otherwise undefined.
*/
export function getViewById(view: ViewBase, id: string): ViewBase;
/**
* Gets a child view by domId.
* @param view - The parent (container) view of the view to look for.
* @param domId - The id of the view to look for.
* Returns an instance of a view (if found), otherwise undefined.
*/
export function getViewByDomId(view: ViewBase, domId: number): ViewBase;
export interface ShowModalOptions {
/**
* Any context you want to pass to the modally shown view. This same context will be available in the arguments of the shownModally event handler.
*/
context: any;
/**
* A function that will be called when the view is closed. Any arguments provided when calling ShownModallyData.closeCallback will be available here.
*/
closeCallback: Function;
/**
* An optional parameter specifying whether to show the modal view in full-screen mode.
*/
fullscreen?: boolean;
/**
* An optional parameter specifying whether to show the modal view with animation.
*/
animated?: boolean;
/**
* An optional parameter specifying whether to stretch the modal view when not in full-screen mode.
*/
stretched?: boolean;
/**
* An optional parameter that specify options specific to iOS as an object.
*/
ios?: {
/**
* The UIModalPresentationStyle to be used when showing the dialog in iOS .
*/
presentationStyle?: any /* UIModalPresentationStyle */;
/**
* width of the popup dialog
*/
width?: number;
/**
* height of the popup dialog
*/
height?: number;
};
android?: {
/**
* @deprecated Use ShowModalOptions.cancelable instead.
* An optional parameter specifying whether the modal view can be dismissed when not in full-screen mode.
*/
cancelable?: boolean;
/**
* An optional parameter specifying the windowSoftInputMode of the dialog window
* For possible values see https://developer.android.com/reference/android/view/WindowManager.LayoutParams#softInputMode
*/
windowSoftInputMode?: number;
};
/**
* An optional parameter specifying whether the modal view can be dismissed when not in full-screen mode.
*/
cancelable?: boolean;
}
export abstract class ViewBase extends Observable {
// Dynamic properties.
left: CoreTypes.LengthType;
top: CoreTypes.LengthType;
effectiveLeft: number;
effectiveTop: number;
dock: 'left' | 'top' | 'right' | 'bottom';
row: number;
col: number;
/**
* Setting `column` property is the same as `col`
*/
column: number;
rowSpan: number;
colSpan: number;
/**
* Setting `columnSpan` property is the same as `colSpan`
*/
columnSpan: number;
domNode: DOMNode;
order: Order;
flexGrow: FlexGrow;
flexShrink: FlexShrink;
flexWrapBefore: FlexWrapBefore;
alignSelf: AlignSelf;
/**
* @private
* Module name when the view is a module root. Otherwise, it is undefined.
*/
_moduleName?: string;
//@private
/**
* @private
*/
_oldLeft: number;
/**
* @private
*/
_oldTop: number;
/**
* @private
*/
_oldRight: number;
/**
* @private
*/
_oldBottom: number;
/**
* @private
*/
_defaultPaddingTop: number;
/**
* @private
*/
_defaultPaddingRight: number;
/**
* @private
*/
_defaultPaddingBottom: number;
/**
* @private
*/
_defaultPaddingLeft: number;
/**
* A property bag holding suspended native updates.
* Native setters that had to execute while there was no native view,
* or the view was detached from the visual tree etc. will accumulate in this object,
* and will be applied when all prerequisites are met.
* @private
*/
_suspendedUpdates: {
[propertyName: string]: Property<any, any> | CssProperty<Style, any> | CssAnimationProperty<Style, any>;
};
//@endprivate
/**
* Shows the View contained in moduleName as a modal view.
* @param moduleName - The name of the module to load starting from the application root.
* @param modalOptions - A ShowModalOptions instance
*/
showModal(moduleName: string, modalOptions?: ShowModalOptions): ViewBase;
/**
* Shows the view passed as parameter as a modal view.
* @param view - View instance to be shown modally.
* @param modalOptions - A ShowModalOptions instance
*/
showModal(view: ViewBase, modalOptions?: ShowModalOptions): ViewBase;
/**
* Closes the current modal view that this page is showing.
* @param context - Any context you want to pass back to the host when closing the modal view.
*/
closeModal(context?: any): void;
public effectiveMinWidth: number;
public effectiveMinHeight: number;
public effectiveWidth: number;
public effectiveHeight: number;
public effectiveMarginTop: number;
public effectiveMarginRight: number;
public effectiveMarginBottom: number;
public effectiveMarginLeft: number;
public effectivePaddingTop: number;
public effectivePaddingRight: number;
public effectivePaddingBottom: number;
public effectivePaddingLeft: number;
public effectiveBorderTopWidth: number;
public effectiveBorderRightWidth: number;
public effectiveBorderBottomWidth: number;
public effectiveBorderLeftWidth: number;
/**
* String value used when hooking to loaded event.
*/
public static loadedEvent: string;
/**
* String value used when hooking to unloaded event.
*/
public static unloadedEvent: string;
/**
* String value used when hooking to creation event
*/
public static createdEvent: string;
/**
* String value used when hooking to disposeNativeView event
*/
public static disposeNativeViewEvent: string;
public ios: any;
public android: any;
/**
* returns the native UIViewController.
*/
public viewController: any;
/**
* read-only. If you want to set out-of-band the nativeView use the setNativeView method.
*/
public nativeViewProtected: any;
public nativeView: any;
public bindingContext: any;
/**
* Gets or sets if the view is reusable.
* Reusable views are not automatically destroyed when removed from the View tree.
*/
public reusable: boolean;
/**
* Gets the name of the constructor function for this instance. E.g. for a Button class this will return "Button".
*/
public typeName: string;
/**
* Gets the parent view. This property is read-only.
*/
public readonly parent: ViewBase;
/**
* Gets the template parent or the native parent. Sets the template parent.
*/
public parentNode: ViewBase;
/**
* Gets or sets the id for this view.
*/
public id: string;
/**
* Gets or sets the CSS class name for this view.
*/
public className: string;
/**
* Gets owner page. This is a read-only property.
*/
public readonly page: Page;
/**
* Gets the style object associated to this view.
*/
public readonly style: Style;
/**
* Returns true if visibility is set to 'collapse'.
* Readonly property
*/
public isCollapsed: boolean;
public readonly isLoaded: boolean;
/**
* Returns the child view with the specified id.
*/
public getViewById<T extends ViewBase>(id: string): T;
/**
* Returns the child view with the specified domId.
*/
public getViewByDomId<T extends ViewBase>(id: number): T;
/**
* Load view.
* @param view to load.
*/
public loadView(view: ViewBase): void;
/**
* Unload view.
* @param view to unload.
*/
public unloadView(view: ViewBase): void;
public onLoaded(): void;
public onUnloaded(): void;
public onResumeNativeUpdates(): void;
public bind(options: BindingOptions, source?: Object): void;
public unbind(property: string): void;
/**
* Invalidates the layout of the view and triggers a new layout pass.
*/
public requestLayout(): void;
/**
* Iterates over children of type ViewBase.
* @param callback Called for each child of type ViewBase. Iteration stops if this method returns falsy value.
*/
public eachChild(callback: (child: ViewBase) => boolean): void;
public _addView(view: ViewBase, atIndex?: number): void;
/**
* Method is intended to be overridden by inheritors and used as "protected"
*/
public _addViewCore(view: ViewBase, atIndex?: number): void;
public _removeView(view: ViewBase): void;
/**
* Method is intended to be overridden by inheritors and used as "protected"
*/
public _removeViewCore(view: ViewBase): void;
public _parentChanged(oldParent: ViewBase): void;
/**
* Method is intended to be overridden by inheritors and used as "protected"
*/
public _dialogClosed(): void;
/**
* Method is intended to be overridden by inheritors and used as "protected"
*/
public _onRootViewReset(): void;
_domId: number;
_cssState: any /* "ui/styling/style-scope" */;
/**
* @private
* Notifies each child's css state for change, recursively.
* Either the style scope, className or id properties were changed.
*/
_onCssStateChange(): void;
public cssClasses: Set<string>;
public cssPseudoClasses: Set<string>;
public _goToVisualState(state: string): void;
public setInlineStyle(style: string): void;
_context: any /* android.content.Context */;
/**
* Setups the UI for ViewBase and all its children recursively.
* This method should *not* be overridden by derived views.
*/
_setupUI(context: any /* android.content.Context */, atIndex?: number): void;
/**
* Tears down the UI for ViewBase and all its children recursively.
* This method should *not* be overridden by derived views.
*/
_tearDownUI(force?: boolean): void;
/**
* Tears down the UI of a reusable node by making it no longer reusable.
* @see _tearDownUI
* @param forceDestroyChildren Force destroy the children (even if they are reusable)
*/
destroyNode(forceDestroyChildren?: boolean): void;
/**
* Creates a native view.
* Returns either android.view.View or UIView.
*/
createNativeView(): Object;
/**
* Initializes properties/listeners of the native view.
*/
initNativeView(): void;
/**
* Clean up references to the native view.
*/
disposeNativeView(): void;
/**
* Resets properties/listeners set to the native view.
*/
resetNativeView(): void;
/**
* Set the nativeView field performing extra checks and updates to the native properties on the new view.
* Use in cases where the createNativeView is not suitable.
* As an example use in item controls where the native parent view will create the native views for child items.
*/
setNativeView(view: any): void;
_isAddedToNativeVisualTree: boolean;
/**
* Performs the core logic of adding a child view to the native visual tree. Returns true if the view's native representation has been successfully added, false otherwise.
*/
_addViewToNativeVisualTree(view: ViewBase, atIndex?: number): boolean;
_removeViewFromNativeVisualTree(view: ViewBase): void;
_childIndexToNativeChildIndex(index?: number): number;
/**
* @protected
* @unstable
* A widget can call this method to add a matching css pseudo class.
*/
public addPseudoClass(name: string): void;
/**
* @protected
* @unstable
* A widget can call this method to discard matching css pseudo class.
*/
public deletePseudoClass(name: string): void;
/**
* @unstable
* Ensures a dom-node for this view.
*/
public ensureDomNode();
public recycleNativeView: 'always' | 'never' | 'auto';
/**
* @private
*/
public _isPaddingRelative: boolean;
/**
* @private
*/
public _ignoreFlexMinWidthHeightReset: boolean;
public _styleScope: any;
/**
* @private
*/
public _automaticallyAdjustsScrollViewInsets: boolean;
/**
* @private
*/
_isStyleScopeHost: boolean;
/**
* @private
*/
public _layoutParent(): void;
/**
* Determines the depth of suspended updates.
* When the value is 0 the current property updates are not batched nor scoped and must be immediately applied.
* If the value is 1 or greater, the current updates are batched and does not have to provide immediate update.
* Do not set this field, the _batchUpdate method is responsible to keep the count up to date,
* as well as adding/rmoving the view to/from the visual tree.
*/
public _suspendNativeUpdatesCount: number;
/**
* Allow multiple updates to be performed on the instance at once.
*/
public _batchUpdate<T>(callback: () => T): T;
/**
* @private
*/
_setupAsRootView(context: any): void;
/**
* When returning true the callLoaded method will be run in setTimeout
* Method is intended to be overridden by inheritors and used as "protected"
*/
_shouldDelayLayout(): boolean;
/**
* @private
*/
_inheritStyleScope(styleScope: any /* StyleScope */): void;
/**
* @private
*/
callLoaded(): void;
/**
* @private
*/
callUnloaded(): void;
//@endprivate
}
export class Binding {
constructor(target: ViewBase, options: BindingOptions);
public bind(source: Object): void;
public unbind();
}
export const idProperty: Property<any, string>;
export const classNameProperty: Property<any, string>;
export const bindingContextProperty: InheritedProperty<any, any>;
/**
* Converts string into boolean value.
* Throws error if value is not 'true' or 'false'.
*/
export function booleanConverter(v: string): boolean;

View File

@ -1,10 +1,8 @@
// Definitions.
import { AlignSelf, FlexGrow, FlexShrink, FlexWrapBefore, Order } from '../../layouts/flexbox-layout';
import { Page } from '../../page';
// Types.
import { CoreTypes } from '../../../core-types';
import { Property, CssProperty, CssAnimationProperty, InheritedProperty, clearInheritedProperties, propagateInheritableProperties, propagateInheritableCssProperties, initNativeView } from '../properties';
import { setupAccessibleView } from '../../../accessibility';
import { CSSUtils } from '../../../css/system-classes';
import { Source } from '../../../utils/debug';
import { Binding, BindingOptions } from '../bindable';
@ -12,6 +10,7 @@ import { Trace } from '../../../trace';
import { Observable, PropertyChangeData, WrappedValue } from '../../../data/observable';
import { Style } from '../../styling/style';
import { paddingTopProperty, paddingRightProperty, paddingBottomProperty, paddingLeftProperty } from '../../styling/style-properties';
import type { ModalTransition } from '../../transition/modal-transition';
// TODO: Remove this import!
import { getClass } from '../../../utils/types';
@ -39,6 +38,13 @@ function ensureStyleScopeModule() {
const defaultBindingSource = {};
export interface ModalTransitionType {
name?: string;
instance?: ModalTransition;
duration?: number;
curve?: any;
}
export interface ShowModalOptions {
/**
* Any context you want to pass to the modally shown view. This same context will be available in the arguments of the shownModally event handler.
@ -65,6 +71,11 @@ export interface ShowModalOptions {
*/
stretched?: boolean;
/**
* An optional custom transition effect
*/
transition?: ModalTransitionType;
/**
* An optional parameter that specify options specific to iOS as an object.
*/
@ -101,6 +112,12 @@ export interface ShowModalOptions {
cancelable?: boolean;
}
/**
* Gets an ancestor from a given type.
* @param view - Starting view (child view).
* @param criterion - The type of ancestor view we are looking for. Could be a string containing a class name or an actual type.
* Returns an instance of a view (if found), otherwise undefined.
*/
export function getAncestor(view: ViewBaseDefinition, criterion: string | { new () }): ViewBaseDefinition {
let matcher: (view: ViewBaseDefinition) => boolean = null;
if (typeof criterion === 'string') {
@ -118,6 +135,12 @@ export function getAncestor(view: ViewBaseDefinition, criterion: string | { new
return null;
}
/**
* Gets a child view by id.
* @param view - The parent (container) view of the view to look for.
* @param id - The id of the view to look for.
* Returns an instance of a view (if found), otherwise undefined.
*/
export function getViewById(view: ViewBaseDefinition, id: string): ViewBaseDefinition {
if (!view) {
return undefined;
@ -144,6 +167,12 @@ export function getViewById(view: ViewBaseDefinition, id: string): ViewBaseDefin
return retVal;
}
/**
* Gets a child view by domId.
* @param view - The parent (container) view of the view to look for.
* @param domId - The id of the view to look for.
* Returns an instance of a view (if found), otherwise undefined.
*/
export function getViewByDomId(view: ViewBaseDefinition, domId: number): ViewBaseDefinition {
if (!view) {
return undefined;
@ -170,6 +199,41 @@ export function getViewByDomId(view: ViewBaseDefinition, domId: number): ViewBas
return retVal;
}
// TODO: allow all selector types (just using attributes now)
/**
* Gets a child view by selector.
* @param view - The parent (container) view of the view to look for.
* @param selector - The selector of the view to look for.
* Returns an instance of a view (if found), otherwise undefined.
*/
export function querySelectorAll(view: ViewBaseDefinition, selector: string): Array<ViewBaseDefinition> {
if (!view) {
return undefined;
}
const retVal: Array<ViewBaseDefinition> = [];
if (view[selector]) {
retVal.push(view);
}
const descendantsCallback = function (child: ViewBaseDefinition): boolean {
if (child[selector]) {
retVal.push(child);
}
return true;
};
eachDescendant(view, descendantsCallback);
return retVal;
}
/**
* Iterates through all child views (via visual tree) and executes a function.
* @param view - Starting view (parent container).
* @param callback - A function to execute on every child. If function returns false it breaks the iteration.
*/
export function eachDescendant(view: ViewBaseDefinition, callback: (child: ViewBaseDefinition) => boolean) {
if (!callback || !view) {
return;
@ -245,9 +309,21 @@ namespace SuspendType {
}
export abstract class ViewBase extends Observable implements ViewBaseDefinition {
/**
* String value used when hooking to loaded event.
*/
public static loadedEvent = 'loaded';
/**
* String value used when hooking to unloaded event.
*/
public static unloadedEvent = 'unloaded';
/**
* String value used when hooking to creation event
*/
public static createdEvent = 'created';
/**
* String value used when hooking to disposeNativeView event
*/
public static disposeNativeViewEvent = 'disposeNativeView';
private _onLoadedCalled = false;
@ -264,23 +340,66 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
public domNode: dnm.DOMNode;
public recycleNativeView: 'always' | 'never' | 'auto';
/**
* returns the native UIViewController.
*/
public viewController: any;
public bindingContext: any;
/**
* read-only. If you want to set out-of-band the nativeView use the setNativeView method.
*/
public nativeViewProtected: any;
/**
* Gets the parent view. This property is read-only.
*/
public parent: ViewBase;
public isCollapsed; // Default(false) set in prototype
/**
* Returns true if visibility is set to 'collapse'.
* Default(false) set in prototype
* Readonly property
*/
public isCollapsed;
/**
* Gets or sets the id for this view.
*/
public id: string;
/**
* Gets or sets the CSS class name for this view.
*/
public className: string;
/**
* Gets or sets the shared transition tag for animated view transitions
*/
public sharedTransitionTag: string;
/**
* Opt out of shared transition under different binding conditions
*/
public sharedTransitionIgnore: boolean;
public _domId: number;
public _context: any;
public _context: any /* android.content.Context */;
public _isAddedToNativeVisualTree: boolean;
public _cssState: ssm.CssState = new ssm.CssState(new WeakRef(this));
/* "ui/styling/style-scope" */ public _cssState: ssm.CssState = new ssm.CssState(new WeakRef(this));
public _styleScope: ssm.StyleScope;
/**
* A property bag holding suspended native updates.
* Native setters that had to execute while there was no native view,
* or the view was detached from the visual tree etc. will accumulate in this object,
* and will be applied when all prerequisites are met.
* @private
*/
public _suspendedUpdates: {
[propertyName: string]: Property<ViewBase, any> | CssProperty<Style, any> | CssAnimationProperty<Style, any>;
};
//@endprivate
/**
* Determines the depth of suspended updates.
* When the value is 0 the current property updates are not batched nor scoped and must be immediately applied.
* If the value is 1 or greater, the current updates are batched and does not have to provide immediate update.
* Do not set this field, the _batchUpdate method is responsible to keep the count up to date,
* as well as adding/rmoving the view to/from the visual tree.
*/
public _suspendNativeUpdatesCount: number;
public _isStyleScopeHost: boolean;
public _automaticallyAdjustsScrollViewInsets: boolean;
@ -333,8 +452,16 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
public _defaultPaddingLeft: number;
public _isPaddingRelative: boolean;
/**
* @private
* Module name when the view is a module root. Otherwise, it is undefined.
*/
public _moduleName: string;
/**
* Gets or sets if the view is reusable.
* Reusable views are not automatically destroyed when removed from the View tree.
*/
public reusable: boolean;
constructor() {
@ -344,7 +471,10 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
this.notify({ eventName: ViewBase.createdEvent, type: this.constructor.name, object: this });
}
// Used in Angular.
// Used in Angular. TODO: remove from here
/**
* Gets the template parent or the native parent. Sets the template parent.
*/
get parentNode() {
return this._templateParent || this.parent;
}
@ -361,10 +491,16 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
}
// TODO: Use Type.prototype.typeName instead.
/**
* Gets the name of the constructor function for this instance. E.g. for a Button class this will return "Button".
*/
get typeName(): string {
return getClass(this);
}
/**
* Gets the style object associated to this view.
*/
get style(): Style {
return this._style;
}
@ -397,14 +533,23 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
this.className = v;
}
/**
* Returns the child view with the specified id.
*/
getViewById<T extends ViewBaseDefinition>(id: string): T {
return <T>getViewById(this, id);
}
/**
* Returns the child view with the specified domId.
*/
getViewByDomId<T extends ViewBaseDefinition>(domId: number): T {
return <T>getViewByDomId(this, domId);
}
/**
* Gets owner page. This is a read-only property.
*/
get page(): Page {
if (this.parent) {
return this.parent.page;
@ -413,6 +558,10 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
return null;
}
/**
* @unstable
* Ensures a dom-node for this view.
*/
public ensureDomNode() {
if (!this.domNode) {
ensuredomNodeModule();
@ -442,6 +591,7 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
return true;
});
setupAccessibleView(<any>this);
this._emit('loaded');
}
@ -494,6 +644,9 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
}
}
/**
* Allow multiple updates to be performed on the instance at once.
*/
public _batchUpdate<T>(callback: () => T): T {
try {
this._suspendNativeUpdates(SuspendType.Incremental);
@ -565,6 +718,11 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
return allStates;
}
/**
* @protected
* @unstable
* A widget can call this method to add a matching css pseudo class.
*/
@profile
public addPseudoClass(name: string): void {
const allStates = this.getAllAliasedStates(name);
@ -576,6 +734,11 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
}
}
/**
* @protected
* @unstable
* A widget can call this method to discard matching css pseudo class.
*/
@profile
public deletePseudoClass(name: string): void {
const allStates = this.getAllAliasedStates(name);
@ -659,6 +822,9 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
}
}
/**
* Invalidates the layout of the view and triggers a new layout pass.
*/
@profile
public requestLayout(): void {
// Default implementation for non View instances (like TabViewItem).
@ -668,6 +834,10 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
}
}
/**
* Iterates over children of type ViewBase.
* @param callback Called for each child of type ViewBase. Iteration stops if this method returns falsy value.
*/
public eachChild(callback: (child: ViewBase) => boolean) {
//
}
@ -697,6 +867,9 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
}
}
/**
* Method is intended to be overridden by inheritors and used as "protected"
*/
public _addViewCore(view: ViewBase, atIndex?: number) {
propagateInheritableProperties(this, view);
view._inheritStyleScope(this._styleScope);
@ -711,16 +884,28 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
}
}
/**
* Load view.
* @param view to load.
*/
public loadView(view: ViewBase): void {
if (view && !view.isLoaded) {
view.callLoaded();
}
}
/**
* When returning true the callLoaded method will be run in setTimeout
* Method is intended to be overridden by inheritors and used as "protected"
*/
public _shouldDelayLayout(): boolean {
return false;
}
/**
* Unload view.
* @param view to unload.
*/
public unloadView(view: ViewBase): void {
if (view && view.isLoaded) {
view.callUnloaded();
@ -759,10 +944,17 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
}
}
/**
* Creates a native view.
* Returns either android.view.View or UIView.
*/
public createNativeView(): Object {
return undefined;
}
/**
* Clean up references to the native view.
*/
public disposeNativeView() {
this.notify({
eventName: ViewBase.disposeNativeViewEvent,
@ -770,10 +962,16 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
});
}
/**
* Initializes properties/listeners of the native view.
*/
public initNativeView(): void {
//
}
/**
* Resets properties/listeners set to the native view.
*/
public resetNativeView(): void {
//
}
@ -801,8 +999,12 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
this._setupUI(context);
}
/**
* Setups the UI for ViewBase and all its children recursively.
* This method should *not* be overridden by derived views.
*/
@profile
public _setupUI(context: any, atIndex?: number, parentIsLoaded?: boolean): void {
public _setupUI(context: any /* android.content.Context */, atIndex?: number, parentIsLoaded?: boolean): void {
if (this._context === context) {
// this check is unnecessary as this function should never be called when this._context === context as it means the view was somehow detached,
// which is only possible by setting reusable = true. Adding it either way for feature flag safety
@ -889,6 +1091,11 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
});
}
/**
* Set the nativeView field performing extra checks and updates to the native properties on the new view.
* Use in cases where the createNativeView is not suitable.
* As an example use in item controls where the native parent view will create the native views for child items.
*/
setNativeView(value: any): void {
if (this.__nativeView === value) {
return;
@ -907,12 +1114,21 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
}
}
/**
* Tears down the UI of a reusable node by making it no longer reusable.
* @see _tearDownUI
* @param forceDestroyChildren Force destroy the children (even if they are reusable)
*/
public destroyNode(forceDestroyChildren?: boolean): void {
this.reusable = false;
this.callUnloaded();
this._tearDownUI(forceDestroyChildren);
}
/**
* Tears down the UI for ViewBase and all its children recursively.
* This method should *not* be overridden by derived views.
*/
@profile
public _tearDownUI(force?: boolean): void {
// No context means we are already teared down.
@ -977,6 +1193,7 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
}
/**
* Performs the core logic of adding a child view to the native visual tree. Returns true if the view's native representation has been successfully added, false otherwise.
* Method is intended to be overridden by inheritors and used as "protected".
*/
public _addViewToNativeVisualTree(view: ViewBase, atIndex?: number): boolean {
@ -1070,6 +1287,11 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
return str;
}
/**
* @private
* Notifies each child's css state for change, recursively.
* Either the style scope, className or id properties were changed.
*/
_onCssStateChange(): void {
this._cssState.onChange();
eachDescendant(this, (child: ViewBase) => {
@ -1097,12 +1319,29 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
}
}
public showModal(...args): ViewBase {
/**
* Shows the view passed as parameter as a modal view.
* @param view - View instance to be shown modally.
* @param modalOptions - A ShowModalOptions instance
*/
public showModal(view: ViewBase, modalOptions?: ShowModalOptions): ViewBase;
/**
* Shows the View contained in moduleName as a modal view.
* @param moduleName - The name of the module to load starting from the application root.
* @param modalOptions - A ShowModalOptions instance
*/
public showModal(moduleName: string, modalOptions?: ShowModalOptions): ViewBase;
public showModal(moduleOrView: string | ViewBase, modalOptions?: ShowModalOptions): ViewBase {
const parent = this.parent;
return parent && parent.showModal(...args);
return parent && parent.showModal(<ViewBase>moduleOrView, modalOptions);
}
/**
* Closes the current modal view that this page is showing.
* @param context - Any context you want to pass back to the host when closing the modal view.
*/
public closeModal(...args): void {
const parent = this.parent;
if (parent) {
@ -1110,6 +1349,9 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
}
}
/**
* Method is intended to be overridden by inheritors and used as "protected"
*/
public _dialogClosed(): void {
eachDescendant(this, (child: ViewBase) => {
child._dialogClosed();
@ -1118,6 +1360,9 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
});
}
/**
* Method is intended to be overridden by inheritors and used as "protected"
*/
public _onRootViewReset(): void {
eachDescendant(this, (child: ViewBase) => {
child._onRootViewReset();

View File

@ -21,10 +21,11 @@ import { AndroidActivityBackPressedEventData, android as androidApp } from '../.
import { Device } from '../../../platform';
import lazy from '../../../utils/lazy';
import { accessibilityEnabledProperty, accessibilityHiddenProperty, accessibilityHintProperty, accessibilityIdentifierProperty, accessibilityLabelProperty, accessibilityLanguageProperty, accessibilityLiveRegionProperty, accessibilityMediaSessionProperty, accessibilityRoleProperty, accessibilityStateProperty, accessibilityValueProperty } from '../../../accessibility/accessibility-properties';
import { AccessibilityLiveRegion, AccessibilityRole, AndroidAccessibilityEvent, setupAccessibleView, isAccessibilityServiceEnabled, sendAccessibilityEvent, updateAccessibilityProperties, updateContentDescription, AccessibilityState } from '../../../accessibility';
import { AccessibilityLiveRegion, AccessibilityRole, AndroidAccessibilityEvent, isAccessibilityServiceEnabled, sendAccessibilityEvent, updateAccessibilityProperties, updateContentDescription, AccessibilityState } from '../../../accessibility';
import * as Utils from '../../../utils';
import { SDK_VERSION } from '../../../utils/constants';
import { CSSShadow } from '../../styling/css-shadow';
import { _setAndroidFragmentTransitions, _getAnimatedEntries, _updateTransitions, _reverseTransitions, _clearEntry, _clearFragment, addNativeTransitionListener } from '../../frame/fragment.transitions';
export * from './view-common';
// helpers (these are okay re-exported here)
@ -320,20 +321,6 @@ export class View extends ViewCommon {
nativeViewProtected: android.view.View;
constructor() {
super();
const weakRef = new WeakRef(this);
const handler = () => {
const owner = weakRef.get();
if (owner) {
setupAccessibleView(owner);
owner.off(View.loadedEvent, handler);
}
};
this.on(View.loadedEvent, handler);
}
// TODO: Implement unobserve that detach the touchListener.
_observe(type: GestureTypes, callback: (args: GestureEventData) => void, thisArg?: any): void {
super._observe(type, callback, thisArg);

View File

@ -6,13 +6,17 @@ import { ViewCommon, isEnabledProperty, originXProperty, originYProperty, isUser
import { ShowModalOptions, hiddenProperty } from '../view-base';
import { Trace } from '../../../trace';
import { layout, iOSNativeHelper } from '../../../utils';
import { isNumber } from '../../../utils/types';
import { IOSHelper } from './view-helper';
import { ios as iosBackground, Background } from '../../styling/background';
import { perspectiveProperty, visibilityProperty, opacityProperty, rotateProperty, rotateXProperty, rotateYProperty, scaleXProperty, scaleYProperty, translateXProperty, translateYProperty, zIndexProperty, backgroundInternalProperty, clipPathProperty } from '../../styling/style-properties';
import { profile } from '../../../profiling';
import { accessibilityEnabledProperty, accessibilityHiddenProperty, accessibilityHintProperty, accessibilityIdentifierProperty, accessibilityLabelProperty, accessibilityLanguageProperty, accessibilityLiveRegionProperty, accessibilityMediaSessionProperty, accessibilityRoleProperty, accessibilityStateProperty, accessibilityValueProperty, accessibilityIgnoresInvertColorsProperty } from '../../../accessibility/accessibility-properties';
import { setupAccessibleView, IOSPostAccessibilityNotificationType, isAccessibilityServiceEnabled, updateAccessibilityProperties, AccessibilityEventOptions, AccessibilityRole, AccessibilityState } from '../../../accessibility';
import { IOSPostAccessibilityNotificationType, isAccessibilityServiceEnabled, updateAccessibilityProperties, AccessibilityEventOptions, AccessibilityRole, AccessibilityState } from '../../../accessibility';
import { CoreTypes } from '../../../core-types';
import type { ModalTransition } from '../../transition/modal-transition';
import { SharedTransition } from '../../transition/shared-transition';
import { GestureStateTypes, PanGestureEventData } from '../../gestures';
export * from './view-common';
// helpers (these are okay re-exported here)
@ -31,6 +35,7 @@ export class View extends ViewCommon implements ViewDefinition {
viewController: UIViewController;
private _popoverPresentationDelegate: IOSHelper.UIPopoverPresentationControllerDelegateImp;
private _adaptivePresentationDelegate: IOSHelper.UIAdaptivePresentationControllerDelegateImp;
private _transitioningDelegate: UIViewControllerTransitioningDelegateImpl;
/**
* Track modal open animated options to use same option upon close
@ -58,12 +63,6 @@ export class View extends ViewCommon implements ViewDefinition {
return (this._privateFlags & PFLAG_FORCE_LAYOUT) === PFLAG_FORCE_LAYOUT;
}
constructor() {
super();
this.once(View.loadedEvent, () => setupAccessibleView(this));
}
disposeNativeView() {
super.disposeNativeView();
@ -468,7 +467,21 @@ export class View extends ViewCommon implements ViewDefinition {
this.viewController = controller;
}
if (options.fullscreen) {
if (options.transition) {
controller.modalPresentationStyle = UIModalPresentationStyle.Custom;
if (options.transition.instance) {
this._transitioningDelegate = UIViewControllerTransitioningDelegateImpl.initWithOwner(new WeakRef(options.transition.instance));
controller.transitioningDelegate = this._transitioningDelegate;
this.transitionId = options.transition.instance.id;
const transitionState = SharedTransition.getState(options.transition.instance.id);
if (transitionState?.interactive?.dismiss) {
// interactive transitions via gestures
// TODO - these could be typed as: boolean | (view: View) => void
// to allow users to define their own custom gesture dismissals
options.transition.instance.setupInteractiveGesture(this._closeModalCallback.bind(this), this);
}
}
} else if (options.fullscreen) {
controller.modalPresentationStyle = UIModalPresentationStyle.FullScreen;
} else {
controller.modalPresentationStyle = UIModalPresentationStyle.FormSheet;
@ -563,9 +576,22 @@ export class View extends ViewCommon implements ViewDefinition {
}
const parentController = parent.viewController;
const animated = this._modalAnimatedOptions ? !!this._modalAnimatedOptions.pop() : true;
let animated = true;
if (this._modalAnimatedOptions?.length) {
animated = this._modalAnimatedOptions.slice(-1)[0];
}
parentController.dismissViewControllerAnimatedCompletion(animated, whenClosedCallback);
parentController.dismissViewControllerAnimatedCompletion(animated, () => {
const transitionState = SharedTransition.getState(this.transitionId);
if (!transitionState?.interactiveCancelled) {
this._transitioningDelegate = null;
// this.off('pan', this._interactiveDismissGesture);
if (this._modalAnimatedOptions) {
this._modalAnimatedOptions.pop();
}
}
whenClosedCallback();
});
}
[isEnabledProperty.getDefault](): boolean {
@ -906,11 +932,60 @@ export class View extends ViewCommon implements ViewDefinition {
private _setupAdaptiveControllerDelegate(controller: UIViewController) {
this._adaptivePresentationDelegate = IOSHelper.UIAdaptivePresentationControllerDelegateImp.initWithOwnerAndCallback(new WeakRef(this), this._closeModalCallback);
if (controller?.presentationController) {
controller.presentationController.delegate = <UIAdaptivePresentationControllerDelegate>this._adaptivePresentationDelegate;
}
}
}
View.prototype._nativeBackgroundState = 'unset';
@NativeClass
class UIViewControllerTransitioningDelegateImpl extends NSObject implements UIViewControllerTransitioningDelegate {
owner: WeakRef<ModalTransition>;
static ObjCProtocols = [UIViewControllerTransitioningDelegate];
static initWithOwner(owner: WeakRef<ModalTransition>) {
const delegate = <UIViewControllerTransitioningDelegateImpl>UIViewControllerTransitioningDelegateImpl.new();
delegate.owner = owner;
return delegate;
}
animationControllerForDismissedController?(dismissed: UIViewController): UIViewControllerAnimatedTransitioning {
const owner = this.owner?.deref();
if (owner?.iosDismissedController) {
return owner.iosDismissedController(dismissed);
}
return null;
}
animationControllerForPresentedControllerPresentingControllerSourceController?(presented: UIViewController, presenting: UIViewController, source: UIViewController): UIViewControllerAnimatedTransitioning {
const owner = this.owner?.deref();
if (owner?.iosPresentedController) {
return owner.iosPresentedController(presented, presenting, source);
}
return null;
}
interactionControllerForDismissal?(animator: UIViewControllerAnimatedTransitioning): UIViewControllerInteractiveTransitioning {
const owner = this.owner?.deref();
if (owner?.iosInteractionDismiss) {
const transitionState = SharedTransition.getState(owner.id);
if (transitionState?.interactiveBegan) {
return owner.iosInteractionDismiss(animator);
}
}
return null;
}
interactionControllerForPresentation?(animator: UIViewControllerAnimatedTransitioning): UIViewControllerInteractiveTransitioning {
const owner = this.owner?.deref();
if (owner?.iosInteractionPresented) {
return owner.iosInteractionPresented(animator);
}
return null;
}
}
export class ContainerView extends View {
public iosOverflowSafeArea: boolean;

View File

@ -27,6 +27,7 @@ import { AccessibilityEventOptions, AccessibilityLiveRegion, AccessibilityRole,
import { accessibilityHintProperty, accessibilityIdentifierProperty, accessibilityLabelProperty, accessibilityValueProperty, accessibilityIgnoresInvertColorsProperty } from '../../../accessibility/accessibility-properties';
import { accessibilityBlurEvent, accessibilityFocusChangedEvent, accessibilityFocusEvent, accessibilityPerformEscapeEvent, getCurrentFontScale } from '../../../accessibility';
import { CSSShadow } from '../../styling/css-shadow';
import { SharedTransition, SharedTransitionInteractiveOptions } from '../../transition/shared-transition';
// helpers (these are okay re-exported here)
export * from './view-helper';
@ -68,6 +69,8 @@ export function PseudoClassHandler(...pseudoClasses: string[]): MethodDecorator
export const _rootModalViews = new Array<ViewBase>();
type InteractiveTransitionState = { began?: boolean; cancelled?: boolean; options?: SharedTransitionInteractiveOptions };
export abstract class ViewCommon extends ViewBase implements ViewDefinition {
public static layoutChangedEvent = 'layoutChanged';
public static shownModallyEvent = 'shownModally';
@ -94,6 +97,11 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition {
private _modalContext: any;
private _modal: ViewCommon;
/**
* Active transition instance id for tracking state
*/
transitionId: number;
private _measuredWidth: number;
private _measuredHeight: number;
@ -362,7 +370,12 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition {
public showModal(...args): ViewDefinition {
const { view, options } = this.getModalOptions(args);
if (options.transition?.instance) {
SharedTransition.updateState(options.transition?.instance.id, {
page: this,
toPage: view,
});
}
view._showNativeModalView(this, options);
return view;
@ -395,27 +408,43 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition {
this.style._fontScale = getCurrentFontScale();
this._modalParent = parent;
this._modalContext = options.context;
const that = this;
this._closeModalCallback = function (...originalArgs) {
if (that._closeModalCallback) {
const modalIndex = _rootModalViews.indexOf(that);
this._closeModalCallback = (...originalArgs) => {
const cleanupModalViews = () => {
const modalIndex = _rootModalViews.indexOf(this);
_rootModalViews.splice(modalIndex);
that._modalParent = null;
that._modalContext = null;
that._closeModalCallback = null;
that._dialogClosed();
this._modalParent = null;
this._modalContext = null;
this._closeModalCallback = null;
this._dialogClosed();
parent._modal = null;
};
const whenClosedCallback = () => {
const transitionState = SharedTransition.getState(this.transitionId);
if (transitionState?.interactiveBegan) {
SharedTransition.updateState(this.transitionId, {
interactiveBegan: false,
});
if (!transitionState?.interactiveCancelled) {
cleanupModalViews();
}
}
if (!transitionState?.interactiveCancelled) {
if (typeof options.closeCallback === 'function') {
options.closeCallback.apply(undefined, originalArgs);
}
that._tearDownUI(true);
this._tearDownUI(true);
}
};
that._hideNativeModalView(parent, whenClosedCallback);
const transitionState = SharedTransition.getState(this.transitionId);
if (!transitionState?.interactiveBegan) {
cleanupModalViews();
}
this._hideNativeModalView(parent, whenClosedCallback);
};
}

View File

@ -34,10 +34,6 @@ export function _clearEntry(entry: BackstackEntry): void;
* in order to reapply them when new fragment is created for the same entry.
*/
export function _clearFragment(entry: BackstackEntry): void;
/**
* @private
*/
export function _createIOSAnimatedTransitioning(navigationTransition: NavigationTransition, nativeCurve: any, operation: number, fromVC: any, toVC: any): any;
/**
* @private

View File

@ -1,96 +0,0 @@
import { NavigationTransition } from '.';
import { Transition } from '../transition';
import { SlideTransition } from '../transition/slide-transition';
import { FadeTransition } from '../transition/fade-transition';
import { Trace } from '../../trace';
namespace UIViewControllerAnimatedTransitioningMethods {
const methodSignature = NSMethodSignature.signatureWithObjCTypes('v@:c');
const invocation = NSInvocation.invocationWithMethodSignature(methodSignature);
invocation.selector = 'completeTransition:';
export function completeTransition(didComplete: boolean) {
const didCompleteReference = new interop.Reference(interop.types.bool, didComplete);
invocation.setArgumentAtIndex(didCompleteReference, 2);
invocation.invokeWithTarget(this);
}
}
@NativeClass
class AnimatedTransitioning extends NSObject implements UIViewControllerAnimatedTransitioning {
public static ObjCProtocols = [UIViewControllerAnimatedTransitioning];
private _transition: Transition;
private _operation: UINavigationControllerOperation;
private _fromVC: UIViewController;
private _toVC: UIViewController;
private _transitionType: string;
public static init(transition: Transition, operation: UINavigationControllerOperation, fromVC: UIViewController, toVC: UIViewController): AnimatedTransitioning {
const impl = <AnimatedTransitioning>AnimatedTransitioning.new();
impl._transition = transition;
impl._operation = operation;
impl._fromVC = fromVC;
impl._toVC = toVC;
return impl;
}
public animateTransition(transitionContext: any): void {
const containerView = transitionContext.valueForKey('containerView');
const completion = UIViewControllerAnimatedTransitioningMethods.completeTransition.bind(transitionContext);
switch (this._operation) {
case UINavigationControllerOperation.Push:
this._transitionType = 'push';
break;
case UINavigationControllerOperation.Pop:
this._transitionType = 'pop';
break;
case UINavigationControllerOperation.None:
this._transitionType = 'none';
break;
}
if (Trace.isEnabled()) {
Trace.write(`START ${this._transition} ${this._transitionType}`, Trace.categories.Transition);
}
this._transition.animateIOSTransition(containerView, this._fromVC.view, this._toVC.view, this._operation, completion);
}
public transitionDuration(transitionContext: UIViewControllerContextTransitioning): number {
return this._transition.getDuration();
}
public animationEnded(transitionCompleted: boolean): void {
if (transitionCompleted) {
if (Trace.isEnabled()) {
Trace.write(`END ${this._transition} ${this._transitionType}`, Trace.categories.Transition);
}
} else {
if (Trace.isEnabled()) {
Trace.write(`CANCEL ${this._transition} ${this._transitionType}`, Trace.categories.Transition);
}
}
}
}
export function _createIOSAnimatedTransitioning(navigationTransition: NavigationTransition, nativeCurve: UIViewAnimationCurve, operation: UINavigationControllerOperation, fromVC: UIViewController, toVC: UIViewController): UIViewControllerAnimatedTransitioning {
const instance = <Transition>navigationTransition.instance;
let transition: Transition;
if (instance) {
// Instance transition should take precedence even if the given name match existing transition.
transition = instance;
} else if (navigationTransition.name) {
const name = navigationTransition.name.toLowerCase();
if (name.indexOf('slide') === 0) {
const direction = name.substr('slide'.length) || 'left'; //Extract the direction from the string
transition = new SlideTransition(direction, navigationTransition.duration, nativeCurve);
} else if (name === 'fade') {
transition = new FadeTransition(navigationTransition.duration, nativeCurve);
}
}
return transition ? AnimatedTransitioning.init(transition, operation, fromVC, toVC) : undefined;
}

View File

@ -11,6 +11,7 @@ import { Builder } from '../builder';
import { sanitizeModuleName } from '../builder/module-name-sanitizer';
import { profile } from '../../profiling';
import { FRAME_SYMBOL } from './frame-helpers';
import { SharedTransition } from '../transition/shared-transition';
export { NavigationType } from './frame-interfaces';
export type { AndroidActivityCallbacks, AndroidFragmentCallbacks, AndroidFrame, BackstackEntry, NavigationContext, NavigationEntry, NavigationTransition, TransitionState, ViewEntry, iOSFrame } from './frame-interfaces';
@ -39,7 +40,7 @@ export class FrameBase extends CustomLayoutView {
private _animated: boolean;
private _transition: NavigationTransition;
private _backStack = new Array<BackstackEntry>();
private _navigationQueue = new Array<NavigationContext>();
_navigationQueue = new Array<NavigationContext>();
public actionBarVisibility: 'auto' | 'never' | 'always';
public _currentEntry: BackstackEntry;
@ -398,11 +399,18 @@ export class FrameBase extends CustomLayoutView {
const backstackEntry = navigationContext.entry;
const isBackNavigation = navigationContext.navigationType === NavigationType.back;
this._onNavigatingTo(backstackEntry, isBackNavigation);
const navigationTransition = this._getNavigationTransition(backstackEntry.entry);
if (navigationTransition?.instance) {
SharedTransition.updateState(navigationTransition?.instance.id, {
page: this.currentPage,
toPage: this,
});
}
this._navigateCore(backstackEntry);
}
@profile
private performGoBack(navigationContext: NavigationContext) {
performGoBack(navigationContext: NavigationContext) {
let backstackEntry = navigationContext.entry;
const backstack = this._backStack;
if (!backstackEntry) {

View File

@ -19,9 +19,10 @@ import { Builder } from '../builder';
import { CSSUtils } from '../../css/system-classes';
import { Device } from '../../platform';
import { profile } from '../../profiling';
import { android as androidApplication } from '../../application';
import { setSuspended } from '../../application/application-common';
import { ad } from '../../utils/native-helper';
import type { ExpandedEntry } from './fragment.transitions.android';
import { SharedTransition, SharedTransitionAnimationType } from '../transition/shared-transition';
export * from './frame-common';
@ -459,14 +460,23 @@ export class Frame extends FrameBase {
if (currentEntry && animated && !navigationTransition) {
//TODO: Check whether or not this is still necessary. For Modal views?
//transaction.setTransition(androidx.fragment.app.FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
// transaction.setTransition(androidx.fragment.app.FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
}
transaction.replace(this.containerViewId, newFragment, newFragmentTag);
navigationTransition?.instance?.androidFragmentTransactionCallback?.(transaction, currentEntry, newEntry);
transaction.commitAllowingStateLoss();
if (navigationTransition?.instance) {
SharedTransition.updateState(navigationTransition?.instance?.id, {
activeType: SharedTransitionAnimationType.dismiss,
});
}
}
public _goBackCore(backstackEntry: BackstackEntry) {
public _goBackCore(backstackEntry: BackstackEntry & ExpandedEntry) {
super._goBackCore(backstackEntry);
navDepth = backstackEntry.navDepth;
@ -486,7 +496,13 @@ export class Frame extends FrameBase {
transaction.replace(this.containerViewId, backstackEntry.fragment, backstackEntry.fragmentTag);
backstackEntry.transition?.androidFragmentTransactionCallback?.(transaction, this._currentEntry, backstackEntry);
transaction.commitAllowingStateLoss();
if (backstackEntry?.transition) {
SharedTransition.finishState(backstackEntry.transition.id);
}
}
public _removeEntry(removed: BackstackEntry): void {
@ -591,7 +607,7 @@ export class Frame extends FrameBase {
}
export function reloadPage(context?: ModuleContext): void {
console.log('reloadPage() is deprecated. Use Frame.reloadPage() instead.');
console.warn('reloadPage() is deprecated. Use Frame.reloadPage() instead.');
return Frame.reloadPage(context);
}
@ -1018,7 +1034,10 @@ class FragmentCallbacksImplementation implements AndroidFragmentCallbacks {
const page = entry.resolvedPage;
if (!page) {
Trace.error(`${fragment}.onDestroy: entry has no resolvedPage`);
// todo: check why this happens when using shared element transition!!!
// commented out the Trace.error to prevent a crash (the app will still work interestingly)
console.log(`${fragment}.onDestroy: entry has no resolvedPage`);
// Trace.error(`${fragment}.onDestroy: entry has no resolvedPage`);
return null;
}
@ -1077,7 +1096,7 @@ class FragmentCallbacksImplementation implements AndroidFragmentCallbacks {
}
private loadBitmapFromView(view: android.view.View): android.graphics.Bitmap {
// Don't try to creat bitmaps with no dimensions as this causes a crash
// Don't try to create bitmaps with no dimensions as this causes a crash
// This might happen when showing and closing dialogs fast.
if (!(view && view.getWidth() > 0 && view.getHeight() > 0)) {
return undefined;

View File

@ -3,13 +3,14 @@ import { iOSFrame as iOSFrameDefinition, BackstackEntry, NavigationTransition }
import { FrameBase, NavigationType } from './frame-common';
import { Page } from '../page';
import { View } from '../core/view';
// Requires
import { _createIOSAnimatedTransitioning } from './fragment.transitions';
import { IOSHelper } from '../core/view/view-helper';
import { profile } from '../../profiling';
import { iOSNativeHelper, layout } from '../../utils';
import { Trace } from '../../trace';
import type { PageTransition } from '../transition/page-transition';
import { SlideTransition } from '../transition/slide-transition';
import { FadeTransition } from '../transition/fade-transition';
import { SharedTransition } from '../transition/shared-transition';
export * from './frame-common';
@ -24,8 +25,8 @@ const NON_ANIMATED_TRANSITION = 'non-animated';
let navDepth = -1;
export class Frame extends FrameBase {
public viewController: UINavigationControllerImpl;
public _animatedDelegate = <UINavigationControllerDelegate>UINavigationControllerAnimatedDelegate.new();
viewController: UINavigationControllerImpl;
_animatedDelegate: UINavigationControllerDelegate;
public _ios: iOSFrame;
constructor() {
@ -41,9 +42,12 @@ export class Frame extends FrameBase {
public disposeNativeView() {
this._removeFromFrameStack();
this.viewController = null;
this._ios.controller = null;
this._animatedDelegate = null;
if (this._ios) {
this._ios.controller = null;
this._ios = null;
}
super.disposeNativeView();
}
@ -97,8 +101,22 @@ export class Frame extends FrameBase {
const nativeTransition = _getNativeTransition(navigationTransition, true);
if (!nativeTransition && navigationTransition) {
if (!this._animatedDelegate) {
this._animatedDelegate = <UINavigationControllerDelegate>UINavigationControllerAnimatedDelegate.initWithOwner(new WeakRef(this));
}
this._ios.controller.delegate = this._animatedDelegate;
viewController[DELEGATE] = this._animatedDelegate;
if (navigationTransition.instance) {
this.transitionId = navigationTransition.instance.id;
const transitionState = SharedTransition.getState(this.transitionId);
if (transitionState?.interactive?.dismiss) {
// interactive transitions via gestures
// TODO - allow users to define their own custom gesture dismissals
navigationTransition.instance.setupInteractiveGesture(() => {
this._ios.controller.popViewControllerAnimated(true);
}, this);
}
}
} else {
viewController[DELEGATE] = null;
this._ios.controller.delegate = null;
@ -202,6 +220,7 @@ export class Frame extends FrameBase {
const animated = this._currentEntry ? this._getIsAnimatedNavigation(this._currentEntry.entry) : false;
this._updateActionBar(backstackEntry.resolvedPage);
if (Trace.isEnabled()) {
Trace.write(`${this}.popToViewControllerAnimated(${controller}, ${animated}); depth = ${navDepth}`, Trace.categories.Navigation);
}
@ -377,6 +396,14 @@ const _defaultTransitionDuration = 0.35;
@NativeClass
class UINavigationControllerAnimatedDelegate extends NSObject implements UINavigationControllerDelegate {
public static ObjCProtocols = [UINavigationControllerDelegate];
owner: WeakRef<Frame>;
transition: PageTransition;
static initWithOwner(owner: WeakRef<Frame>) {
const delegate = <UINavigationControllerAnimatedDelegate>UINavigationControllerAnimatedDelegate.new();
delegate.owner = owner;
return delegate;
}
navigationControllerAnimationControllerForOperationFromViewControllerToViewController(navigationController: UINavigationController, operation: number, fromVC: UIViewController, toVC: UIViewController): UIViewControllerAnimatedTransitioning {
let viewController: UIViewController;
@ -401,11 +428,39 @@ class UINavigationControllerAnimatedDelegate extends NSObject implements UINavig
if (Trace.isEnabled()) {
Trace.write(`UINavigationControllerImpl.navigationControllerAnimationControllerForOperationFromViewControllerToViewController(${operation}, ${fromVC}, ${toVC}), transition: ${JSON.stringify(navigationTransition)}`, Trace.categories.NativeLifecycle);
}
this.transition = navigationTransition.instance;
if (!this.transition) {
if (navigationTransition.name) {
const curve = _getNativeCurve(navigationTransition);
const animationController = _createIOSAnimatedTransitioning(navigationTransition, curve, operation, fromVC, toVC);
const name = navigationTransition.name.toLowerCase();
if (name.indexOf('slide') === 0) {
const direction = name.substring('slide'.length) || 'left'; //Extract the direction from the string
this.transition = new SlideTransition(direction, navigationTransition.duration, curve);
} else if (name === 'fade') {
this.transition = new FadeTransition(navigationTransition.duration, curve);
}
}
}
return animationController;
if (this.transition?.iosNavigatedController) {
return this.transition.iosNavigatedController(navigationController, operation, fromVC, toVC);
}
return null;
}
navigationControllerInteractionControllerForAnimationController(navigationController: UINavigationController, animationController: UIViewControllerAnimatedTransitioning): UIViewControllerInteractiveTransitioning {
const owner = this.owner?.deref();
if (owner) {
const state = SharedTransition.getState(owner.transitionId);
if (state?.instance?.iosInteractionDismiss) {
if (state?.interactiveBegan) {
return state?.instance?.iosInteractionDismiss(null);
}
}
}
return null;
}
}

View File

@ -11,7 +11,7 @@ export { ContentView } from './content-view';
export { Binding } from './core/bindable';
export type { BindingOptions } from './core/bindable';
export { ControlStateChangeListener } from './core/control-state-change';
export { ViewBase, eachDescendant, getAncestor, getViewById, booleanConverter } from './core/view-base';
export { ViewBase, eachDescendant, getAncestor, getViewById, booleanConverter, querySelectorAll } from './core/view-base';
export type { ShowModalOptions } from './core/view-base';
export { View, CSSType, ContainerView, ViewHelper, IOSHelper, isUserInteractionEnabledProperty, PseudoClassHandler, CustomLayoutView } from './core/view';
export type { Template, KeyedTemplate, ShownModallyData, AddArrayFromBuilder, AddChildFromBuilder, Size } from './core/view';
@ -75,5 +75,12 @@ export { TextField } from './text-field';
export { TextView } from './text-view';
export { TimePicker } from './time-picker';
export { Transition } from './transition';
export { ModalTransition } from './transition/modal-transition';
export { PageTransition } from './transition/page-transition';
export { 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 { WebView } from './web-view';
export type { LoadEventData, WebViewNavigationType } from './web-view';

View File

@ -8,6 +8,7 @@ import { PageBase, actionBarHiddenProperty, statusBarStyleProperty } from './pag
import { profile } from '../../profiling';
import { iOSNativeHelper, layout } from '../../utils';
import { getLastFocusedViewOnPage, isAccessibilityServiceEnabled } from '../../accessibility';
import { SharedTransition } from '../transition/shared-transition';
export * from './page-common';
@ -211,8 +212,12 @@ class UIViewControllerImpl extends UIViewController {
// _processNavigationQueue will shift navigationQueue. Check canGoBack after that.
// Workaround for disabled backswipe on second custom native transition
if (frame.canGoBack()) {
const transitionState = SharedTransition.getState(owner.transitionId);
if (!transitionState?.interactive) {
// only consider when interactive transitions are not enabled
navigationController.interactivePopGestureRecognizer.delegate = navigationController;
navigationController.interactivePopGestureRecognizer.enabled = owner.enableSwipeBackNavigation;
}
} else {
navigationController.interactivePopGestureRecognizer.enabled = false;
}
@ -401,10 +406,8 @@ export class Page extends PageBase {
constructor() {
super();
const controller = UIViewControllerImpl.initWithOwner(new WeakRef(this));
this.viewController = this._ios = controller;
// Make transitions look good
controller.view.backgroundColor = this._backgroundColor;
this.viewController = this._ios = controller;
}
createNativeView() {

View File

@ -1,45 +0,0 @@
import { Page as PageDefinition } from '.';
import { ContentView } from '../content-view';
import { Frame } from '../frame';
import { KeyframeAnimationInfo } from '../animation';
import { NavigatedData } from '.';
import { Color } from '../../color';
import { ActionBar } from '../action-bar';
import { View } from '../core/view';
export declare class PageBase extends ContentView {
static navigatingToEvent: string;
static navigatedToEvent: string;
static navigatingFromEvent: string;
static navigatedFromEvent: string;
_frame: Frame;
actionBarHidden: boolean;
enableSwipeBackNavigation: boolean;
backgroundSpanUnderStatusBar: boolean;
hasActionBar: boolean;
readonly navigationContext: any;
actionBar: ActionBar;
statusBarStyle: 'light' | 'dark';
androidStatusBarBackground: Color;
readonly page: PageDefinition;
_addChildFromBuilder(name: string, value: any): void;
getKeyframeAnimationWithName(animationName: string): KeyframeAnimationInfo;
readonly frame: Frame;
createNavigatedData(eventName: string, isBackNavigation: boolean): NavigatedData;
onNavigatingTo(context: any, isBackNavigation: boolean, bindingContext?: any): void;
onNavigatedTo(isBackNavigation: boolean): void;
onNavigatingFrom(isBackNavigation: boolean): void;
onNavigatedFrom(isBackNavigation: boolean): void;
eachChildView(callback: (child: View) => boolean): void;
}

View File

@ -170,17 +170,18 @@ export class ScrollView extends ScrollViewBase {
}
public onLayout(left: number, top: number, right: number, bottom: number): void {
if (!this.nativeViewProtected) {
return;
}
const insets = this.getSafeAreaInsets();
let width = right - left - insets.right - insets.left;
let height = bottom - top - insets.bottom - insets.top;
const nativeView = this.nativeViewProtected;
if (majorVersion > 10) {
// Disable automatic adjustment of scroll view insets
// Consider exposing this as property with all 4 modes
// https://developer.apple.com/documentation/uikit/uiscrollview/contentinsetadjustmentbehavior
nativeView.contentInsetAdjustmentBehavior = 2;
this.nativeViewProtected.contentInsetAdjustmentBehavior = 2;
}
let scrollWidth = width + insets.left + insets.right;
@ -193,7 +194,7 @@ export class ScrollView extends ScrollViewBase {
height = Math.max(this._contentMeasuredHeight, height);
}
nativeView.contentSize = CGSizeMake(layout.toDeviceIndependentPixels(scrollWidth), layout.toDeviceIndependentPixels(scrollHeight));
this.nativeViewProtected.contentSize = CGSizeMake(layout.toDeviceIndependentPixels(scrollWidth), layout.toDeviceIndependentPixels(scrollHeight));
View.layoutChild(this, this.layoutView, insets.left, insets.top, insets.left + width, insets.top + height);
}

View File

@ -1,5 +1,3 @@
import { Transition } from '.';
export class FadeTransition extends Transition {
constructor(duration: number, nativeCurve: any);
}
export class FadeTransition extends Transition {}

View File

@ -1,24 +1,64 @@
import { Transition } from '.';
import { DEFAULT_DURATION } from './shared-transition';
export class FadeTransition extends Transition {
public animateIOSTransition(containerView: UIView, fromView: UIView, toView: UIView, operation: UINavigationControllerOperation, completion: (finished: boolean) => void): void {
transitionController: FadeTransitionController;
presented: UIViewController;
presenting: UIViewController;
operation: number;
iosNavigatedController(navigationController: UINavigationController, operation: number, fromVC: UIViewController, toVC: UIViewController): UIViewControllerAnimatedTransitioning {
this.transitionController = FadeTransitionController.initWithOwner(new WeakRef(this));
this.presented = toVC;
this.presenting = fromVC;
this.operation = operation;
// console.log('presenting:', this.presenting);
return this.transitionController;
}
}
@NativeClass()
export class FadeTransitionController extends NSObject implements UIViewControllerAnimatedTransitioning {
static ObjCProtocols = [UIViewControllerAnimatedTransitioning];
owner: WeakRef<FadeTransition>;
static initWithOwner(owner: WeakRef<FadeTransition>) {
const ctrl = <FadeTransitionController>FadeTransitionController.new();
ctrl.owner = owner;
return ctrl;
}
transitionDuration(transitionContext: UIViewControllerContextTransitioning): number {
const owner = this.owner.deref();
if (owner) {
return owner.getDuration();
}
return DEFAULT_DURATION;
}
animateTransition(transitionContext: UIViewControllerContextTransitioning): void {
const owner = this.owner.deref();
if (owner) {
// console.log('FadeTransitionController animateTransition:', owner.operation);
const toView = owner.presented.view;
const originalToViewAlpha = toView.alpha;
const fromView = owner.presenting.view;
const originalFromViewAlpha = fromView.alpha;
toView.alpha = 0.0;
fromView.alpha = 1.0;
switch (operation) {
switch (owner.operation) {
case UINavigationControllerOperation.Push:
containerView.insertSubviewAboveSubview(toView, fromView);
transitionContext.containerView.insertSubviewAboveSubview(toView, fromView);
break;
case UINavigationControllerOperation.Pop:
containerView.insertSubviewBelowSubview(toView, fromView);
transitionContext.containerView.insertSubviewBelowSubview(toView, fromView);
break;
}
const duration = this.getDuration();
const curve = this.getCurve();
const duration = owner.getDuration();
const curve = owner.getCurve();
UIView.animateWithDurationAnimationsCompletion(
duration,
() => {
@ -29,8 +69,9 @@ export class FadeTransition extends Transition {
(finished: boolean) => {
toView.alpha = originalToViewAlpha;
fromView.alpha = originalFromViewAlpha;
completion(finished);
transitionContext.completeTransition(finished);
}
);
}
}
}

View File

@ -1,36 +1,41 @@
// Types.
import { _resolveAnimationCurve } from '../animation';
import { _resolveAnimationCurve } from '../animation';
import lazy from '../../utils/lazy';
import type { Transition as TransitionType } from '.';
const _defaultInterpolator = lazy(() => new android.view.animation.AccelerateDecelerateInterpolator());
let transitionId = 0;
export class Transition {
export class Transition implements TransitionType {
static AndroidTransitionType = {
enter: 'enter',
exit: 'exit',
popEnter: 'popEnter',
popExit: 'popExit',
};
id: number;
private _duration: number;
private _interpolator: android.view.animation.Interpolator;
private _id: number;
constructor(duration: number, curve: any) {
constructor(duration: number = 350, curve?: any) {
this._duration = duration;
this._interpolator = curve ? _resolveAnimationCurve(curve) : _defaultInterpolator();
this._id = transitionId++;
transitionId++;
this.id = transitionId;
}
public getDuration(): number {
return this._duration;
}
public setDuration(value: number) {
this._duration = value;
}
public getCurve(): android.view.animation.Interpolator {
return this._interpolator;
}
public animateIOSTransition(containerView: any, fromView: any, toView: any, operation: any, completion: (finished: boolean) => void): void {
public animateIOSTransition(transitionContext: any, fromViewCtrl: any, toViewCtrl: any, operation: any): void {
throw new Error('Abstract method call');
}
@ -39,6 +44,6 @@ export class Transition {
}
public toString(): string {
return `Transition@${this._id}`;
return `Transition@${this.id}`;
}
}

View File

@ -1,9 +1,45 @@
export class Transition {
static AndroidTransitionType: { enter: string; exit: string; popEnter: string; popExit: string };
constructor(duration: number, nativeCurve: any);
public getDuration(): number;
public getCurve(): any;
public animateIOSTransition(containerView: any, fromView: any, toView: any, operation: any, completion: (finished: boolean) => void): void;
public createAndroidAnimator(transitionType: string): any;
public toString(): string;
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 TransitionNavigationType = 'page' | 'modal';
export interface TransitionInteractiveState {
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<SharedElementSettings>;
presenting?: Array<SharedElementSettings>;
// independent sharedTransitionTags which are part of the shared transition but only on one page
independent?: Array<SharedElementSettings & { isPresented?: boolean }>;
};
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;
setDuration(value: number): void;
getCurve(): any;
animateIOSTransition(transitionContext: any /*UIViewControllerContextTransitioning */, fromViewCtrl: any /* UIViewController */, toViewCtrl: any /* UIViewController */, operation: any /* UINavigationControllerOperation */): void;
createAndroidAnimator(transitionType: string): any;
setupInteractiveGesture?(startCallback: () => void, view: View): void;
iosDismissedController?(dismissed: any /* UIViewController */): any /* UIViewControllerAnimatedTransitioning */;
iosPresentedController?(presented: any /* UIViewController */, presenting: any /* UIViewController */, source: any /* UIViewController */): any /* UIViewControllerAnimatedTransitioning */;
iosInteractionDismiss?(animator: any /* UIViewControllerAnimatedTransitioning */): any /* UIViewControllerInteractiveTransitioning */;
iosInteractionPresented?(animator: any /* UIViewControllerAnimatedTransitioning */): any /* UIViewControllerInteractiveTransitioning */;
iosNavigatedController?(navigationController: any /* UINavigationController */, operation: number, fromVC: any /* UIViewController */, toVC: any /* UIViewController */): any /* UIViewControllerAnimatedTransitioning */;
androidFragmentTransactionCallback?(fragmentTransaction: any /* androidx.fragment.app.FragmentTransaction */, currentEntry: BackstackEntry, newEntry: BackstackEntry): void;
}

View File

@ -1,25 +1,32 @@
let transitionId = 0;
export class Transition {
import type { Transition as TransitionType } from '.';
let transitionId = 0;
export class Transition implements TransitionType {
static AndroidTransitionType = {};
id: number;
private _duration: number;
private _curve: UIViewAnimationCurve;
private _id: number;
constructor(duration: number, curve: UIViewAnimationCurve = UIViewAnimationCurve.EaseInOut) {
constructor(duration: number = 350, nativeCurve: UIViewAnimationCurve = UIViewAnimationCurve.EaseInOut) {
this._duration = duration ? duration / 1000 : 0.35;
this._curve = curve;
this._id = transitionId++;
this._curve = nativeCurve;
transitionId++;
this.id = transitionId;
}
public getDuration(): number {
return this._duration;
}
public setDuration(value: number) {
this._duration = value;
}
public getCurve(): UIViewAnimationCurve {
return this._curve;
}
public animateIOSTransition(containerView: UIView, fromView: UIView, toView: UIView, operation: UINavigationControllerOperation, completion: (finished: boolean) => void): void {
public animateIOSTransition(transitionContext: UIViewControllerContextTransitioning, fromViewCtrl: UIViewController, toViewCtrl: UIViewController, operation: UINavigationControllerOperation): void {
throw new Error('Abstract method call');
}
@ -28,6 +35,6 @@ export class Transition {
}
public toString(): string {
return `Transition@${this._id}`;
return `Transition@${this.id}`;
}
}

View File

@ -0,0 +1,8 @@
import { BackstackEntry } from '../frame';
import { FadeTransition } from './fade-transition';
export class ModalTransition extends FadeTransition {
androidFragmentTransactionCallback(fragmentTransaction: androidx.fragment.app.FragmentTransaction, currentEntry: BackstackEntry, newEntry: BackstackEntry) {
console.log('Not currently supported on Android.');
}
}

View File

@ -0,0 +1,2 @@
import { Transition } from '.';
export declare class ModalTransition extends Transition {}

View File

@ -0,0 +1,180 @@
import type { View } from '../core/view';
import { isNumber } from '../../utils/types';
import { Transition, SharedElementSettings, TransitionInteractiveState } from '.';
import { SharedTransition, DEFAULT_DURATION } from './shared-transition';
import { SharedTransitionHelper } from './shared-transition-helper';
import { PanGestureEventData, GestureStateTypes } from '../gestures';
export class ModalTransition extends Transition {
transitionController: ModalTransitionController;
interactiveController: UIPercentDrivenInteractiveTransition;
interactiveGestureRecognizer: UIScreenEdgePanGestureRecognizer;
presented: UIViewController;
presenting: UIViewController;
sharedElements: {
presented?: Array<SharedElementSettings>;
presenting?: Array<SharedElementSettings>;
// independent sharedTransitionTags which are part of the shared transition but only on one page
independent?: Array<SharedElementSettings & { isPresented?: boolean }>;
};
private _interactiveStartCallback: () => void;
private _interactiveDismissGesture: (args: any /*PanGestureEventData*/) => void;
iosPresentedController(presented: UIViewController, presenting: UIViewController, source: UIViewController): UIViewControllerAnimatedTransitioning {
this.transitionController = ModalTransitionController.initWithOwner(new WeakRef(this));
this.presented = presented;
// console.log('presenting:', presenting)
return this.transitionController;
}
iosDismissedController(dismissed: UIViewController): UIViewControllerAnimatedTransitioning {
this.transitionController = ModalTransitionController.initWithOwner(new WeakRef(this));
this.presented = dismissed;
return this.transitionController;
}
iosInteractionDismiss(animator: UIViewControllerAnimatedTransitioning): UIViewControllerInteractiveTransitioning {
// console.log('-- iosInteractionDismiss --');
this.interactiveController = PercentInteractiveController.initWithOwner(new WeakRef(this));
return this.interactiveController;
}
iosInteractionPresented(animator: UIViewControllerAnimatedTransitioning): UIViewControllerInteractiveTransitioning {
// console.log('-- iosInteractionPresented --');
return null;
}
setupInteractiveGesture(startCallback: () => void, view: View) {
this._interactiveStartCallback = startCallback;
this._interactiveDismissGesture = this._interactiveDismissGestureHandler.bind(this);
view.on('pan', this._interactiveDismissGesture);
// this.interactiveGestureRecognizer = UIScreenEdgePanGestureRecognizer.alloc().initWithTargetAction()
// let edgeSwipeGestureRecognizer = UIScreenEdgePanGestureRecognizer(target: self, action: #selector(handleSwipe(_:)))
// edgeSwipeGestureRecognizer.edges = .left
// view.addGestureRecognizer(edgeSwipeGestureRecognizer)
}
private _interactiveDismissGestureHandler(args: PanGestureEventData) {
if (args?.ios?.view) {
const state = SharedTransition.getState(this.id);
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);
}
switch (args.state) {
case GestureStateTypes.began:
SharedTransition.updateState(this.id, {
interactiveBegan: true,
interactiveCancelled: false,
});
if (this._interactiveStartCallback) {
this._interactiveStartCallback();
}
break;
case GestureStateTypes.changed:
if (percent < 1) {
if (this.interactiveController) {
this.interactiveController.updateInteractiveTransition(percent);
}
}
break;
case GestureStateTypes.cancelled:
case GestureStateTypes.ended:
if (this.interactiveController) {
const finishThreshold = isNumber(state.interactive?.dismiss?.finishThreshold) ? state.interactive.dismiss.finishThreshold : 0.5;
if (percent > finishThreshold) {
this.interactiveController.finishInteractiveTransition();
} else {
SharedTransition.updateState(this.id, {
interactiveCancelled: true,
});
this.interactiveController.cancelInteractiveTransition();
}
}
break;
}
}
}
}
@NativeClass()
class PercentInteractiveController extends UIPercentDrivenInteractiveTransition implements UIViewControllerInteractiveTransitioning {
static ObjCProtocols = [UIViewControllerInteractiveTransitioning];
owner: WeakRef<ModalTransition>;
interactiveState: TransitionInteractiveState;
static initWithOwner(owner: WeakRef<ModalTransition>) {
const ctrl = <PercentInteractiveController>PercentInteractiveController.new();
ctrl.owner = owner;
return ctrl;
}
startInteractiveTransition(transitionContext: UIViewControllerContextTransitioning) {
// console.log('startInteractiveTransition');
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) {
const state = SharedTransition.getState(owner.id);
SharedTransitionHelper.interactiveUpdate(state, this.interactiveState, 'modal', percentComplete);
}
}
cancelInteractiveTransition() {
// console.log('cancelInteractiveTransition');
const owner = this.owner?.deref();
if (owner) {
const state = SharedTransition.getState(owner.id);
SharedTransitionHelper.interactiveCancel(state, this.interactiveState, 'modal');
}
}
finishInteractiveTransition() {
// console.log('finishInteractiveTransition');
const owner = this.owner?.deref();
if (owner) {
const state = SharedTransition.getState(owner.id);
SharedTransitionHelper.interactiveFinish(state, this.interactiveState, 'modal');
}
}
}
@NativeClass()
class ModalTransitionController extends NSObject implements UIViewControllerAnimatedTransitioning {
static ObjCProtocols = [UIViewControllerAnimatedTransitioning];
owner: WeakRef<ModalTransition>;
static initWithOwner(owner: WeakRef<ModalTransition>) {
const ctrl = <ModalTransitionController>ModalTransitionController.new();
ctrl.owner = owner;
return ctrl;
}
transitionDuration(transitionContext: UIViewControllerContextTransitioning): number {
return DEFAULT_DURATION;
}
animateTransition(transitionContext: UIViewControllerContextTransitioning): void {
// console.log('ModalTransitionController animateTransition');
const owner = this.owner.deref();
if (owner) {
// console.log('owner.id:', owner.id);
const state = SharedTransition.getState(owner.id);
if (!state) {
return;
}
SharedTransitionHelper.animate(state, transitionContext, 'modal');
}
}
}

View File

@ -0,0 +1,196 @@
import type { View } from '../core/view';
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 { ImageSource } from '../../image-source';
import { ContentView } from '../content-view';
import { GridLayout } from '../layouts/grid-layout';
import { ad } from '../../utils';
// import { Image } from '../image';
@NativeClass
class SnapshotViewGroup extends android.view.ViewGroup {
constructor(context: android.content.Context) {
super(context);
return global.__native(this);
}
public onMeasure(): void {
this.setMeasuredDimension(0, 0);
}
public onLayout(): void {
//
}
}
class ContentViewSnapshot extends ContentView {
createNativeView() {
return new SnapshotViewGroup(this._context);
}
}
@NativeClass
class CustomSpringInterpolator extends android.view.animation.AnticipateOvershootInterpolator {
getInterpolation(input: number) {
// Note: we speed up the interpolation by 10% to fix the issue with the transition not being finished
// and the views shifting from their intended final position...
// this is really just a workaround and should be fixed properly once we
// can figure out the root cause of the issue.
const res = super.getInterpolation(input) * 1.1;
if (res > 1) {
return float(1);
}
return float(res);
}
}
@NativeClass
class CustomLinearInterpolator extends android.view.animation.LinearInterpolator {
getInterpolation(input: number) {
// Note: we speed up the interpolation by 10% to fix the issue with the transition not being finished
// and the views shifting from their intended final position...
// this is really just a workaround and should be fixed properly once we
// can figure out the root cause of the issue.
const res = super.getInterpolation(input) * 1.1;
if (res > 1) {
return float(1);
}
return float(res);
}
}
function setTransitionName(view: ViewBase) {
if (!view?.sharedTransitionTag) {
return;
}
try {
androidx.core.view.ViewCompat.setTransitionName(view.nativeView, view.sharedTransitionTag);
} catch (err) {
// ignore
}
}
export class PageTransition extends FadeTransition {
constructor(duration?: number, curve?: any) {
// disable custom curves until we can fix the issue with the animation not completing
if (curve) {
console.warn('PageTransition does not support custom curves at the moment. The passed in curve will be ignored.');
}
if (typeof duration !== 'number') {
duration = 500;
}
super(duration);
}
androidFragmentTransactionCallback(fragmentTransaction: androidx.fragment.app.FragmentTransaction, currentEntry: BackstackEntry, newEntry: BackstackEntry) {
const fromPage = currentEntry.resolvedPage;
const toPage = newEntry.resolvedPage;
const newFragment: androidx.fragment.app.Fragment = newEntry.fragment;
const state = SharedTransition.getState(this.id);
const pageEnd = state.pageEnd;
const { sharedElements, presented, presenting } = SharedTransition.getSharedElements(fromPage, toPage);
const sharedElementTags = sharedElements.map((v) => v.sharedTransitionTag);
if (SharedTransition.DEBUG) {
console.log(` Page: ${state.activeType === SharedTransitionAnimationType.present ? 'Present' : 'Dismiss'}`);
console.log(`1. Found sharedTransitionTags to animate:`, sharedElementTags);
}
// Note: we can enhance android more over time with element targeting across different screens
// const pageStart = state.pageStart;
// const pageEndIndependentTags = Object.keys(pageEnd?.sharedTransitionTags || {});
// console.log('pageEndIndependentTags:', pageEndIndependentTags);
// for (const tag of pageEndIndependentTags) {
// // only consider start when there's a matching end
// const pageStartIndependentProps = pageStart?.sharedTransitionTags[tag];
// if (pageStartIndependentProps) {
// console.log('pageStartIndependentProps:', 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;
// }
// if (independentView) {
// console.log('independentView:', independentView);
// const imageSource = renderToImageSource(independentView);
// const image = new Image();
// image.src = imageSource;
// const { hostView } = loadViewInBackground(image);
// (<any>fromPage).addChild(hostView);
// independentView.opacity = 0;
// }
// }
toPage.once('loaded', () => {
presented.filter((v) => sharedElementTags.includes(v.sharedTransitionTag)).forEach(setTransitionName);
newFragment.startPostponedEnterTransition();
});
sharedElements.forEach((v) => {
setTransitionName(v);
fragmentTransaction.addSharedElement(v.nativeView, v.sharedTransitionTag);
});
fragmentTransaction.setReorderingAllowed(true);
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 transitionSet = new androidx.transition.TransitionSet();
transitionSet.setDuration(customDuration || this.getDuration());
transitionSet.addTransition(new androidx.transition.ChangeBounds());
transitionSet.addTransition(new androidx.transition.ChangeTransform());
if (customDuration) {
// duration always overrides default spring
transitionSet.setInterpolator(new CustomLinearInterpolator());
} else {
transitionSet.setInterpolator(new CustomSpringInterpolator());
}
// postpone enter until we call "loaded" on the new page
newFragment.postponeEnterTransition();
newFragment.setSharedElementEnterTransition(transitionSet);
newFragment.setSharedElementReturnTransition(transitionSet);
}
}
function renderToImageSource(hostView: View): ImageSource {
const bitmap = android.graphics.Bitmap.createBitmap(hostView.android.getWidth(), hostView.android.getHeight(), android.graphics.Bitmap.Config.ARGB_8888);
const canvas = new android.graphics.Canvas(bitmap);
// ensure we start with a blank transparent canvas
canvas.drawARGB(0, 0, 0, 0);
hostView.android.draw(canvas);
return new ImageSource(bitmap);
}
function loadViewInBackground(view: View) {
const hiddenHost = new ContentViewSnapshot();
const hostView = new GridLayout(); // use a host view to ensure margins are respected
hiddenHost.content = hostView;
hiddenHost.visibility = 'collapse';
hostView.addChild(view);
hiddenHost._setupAsRootView(ad.getApplicationContext());
hiddenHost.callLoaded();
ad.getCurrentActivity().addContentView(hiddenHost.android, new android.view.ViewGroup.LayoutParams(0, 0));
return {
hiddenHost,
hostView,
};
}

View File

@ -0,0 +1,2 @@
import { Transition } from '.';
export declare class PageTransition extends Transition {}

View File

@ -0,0 +1,204 @@
import type { View } from '../core/view';
import { SharedElementSettings, TransitionInteractiveState, Transition } from '.';
import { isNumber } from '../../utils/types';
import { PanGestureEventData, GestureStateTypes } from '../gestures';
import { SharedTransition, DEFAULT_DURATION } from './shared-transition';
import { SharedTransitionHelper } from './shared-transition-helper';
export class PageTransition extends Transition {
transitionController: PageTransitionController;
interactiveController: UIPercentDrivenInteractiveTransition;
presented: UIViewController;
presenting: UIViewController;
navigationController: UINavigationController;
operation: number;
sharedElements: {
presented?: Array<SharedElementSettings>;
presenting?: Array<SharedElementSettings>;
// independent sharedTransitionTags which are part of the shared transition but only on one page
independent?: Array<SharedElementSettings & { isPresented?: boolean }>;
};
private _interactiveStartCallback: () => void;
private _interactiveDismissGesture: (args: any /*PanGestureEventData*/) => void;
private _interactiveGestureTeardown: () => void;
iosNavigatedController(navigationController: UINavigationController, operation: number, fromVC: UIViewController, toVC: UIViewController): UIViewControllerAnimatedTransitioning {
this.navigationController = navigationController;
if (!this.transitionController) {
this.presented = toVC;
this.presenting = fromVC;
}
this.transitionController = PageTransitionController.initWithOwner(new WeakRef(this));
// console.log('iosNavigatedController presenting:', this.presenting);
this.operation = operation;
return this.transitionController;
}
iosInteractionDismiss(animator: UIViewControllerAnimatedTransitioning): UIViewControllerInteractiveTransitioning {
// console.log('-- iosInteractionDismiss --');
this.interactiveController = PercentInteractiveController.initWithOwner(new WeakRef(this));
return this.interactiveController;
}
setupInteractiveGesture(startCallback: () => void, view: View): () => void {
// console.log(' -- setupInteractiveGesture --');
this._interactiveStartCallback = startCallback;
if (!this._interactiveDismissGesture) {
// console.log('setup but tearing down first!');
view.off('pan', this._interactiveDismissGesture);
this._interactiveDismissGesture = this._interactiveDismissGestureHandler.bind(this);
}
view.on('pan', this._interactiveDismissGesture);
this._interactiveGestureTeardown = () => {
// console.log(`-- TEARDOWN setupInteractiveGesture --`);
if (view) {
view.off('pan', this._interactiveDismissGesture);
}
this._interactiveDismissGesture = null;
};
return this._interactiveGestureTeardown;
}
private _interactiveDismissGestureHandler(args: PanGestureEventData) {
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) {
console.log('Interactive dismissal percentage:', percent);
}
switch (args.state) {
case GestureStateTypes.began:
SharedTransition.updateState(this.id, {
interactiveBegan: true,
interactiveCancelled: false,
});
if (this._interactiveStartCallback) {
this._interactiveStartCallback();
}
break;
case GestureStateTypes.changed:
if (percent < 1) {
if (this.interactiveController) {
this.interactiveController.updateInteractiveTransition(percent);
}
}
break;
case GestureStateTypes.cancelled:
case GestureStateTypes.ended:
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.interactiveController.finishInteractiveTransition();
} else {
SharedTransition.updateState(this.id, {
interactiveCancelled: true,
});
this.interactiveController.cancelInteractiveTransition();
}
}
break;
}
}
}
}
@NativeClass()
class PercentInteractiveController extends UIPercentDrivenInteractiveTransition implements UIViewControllerInteractiveTransitioning {
static ObjCProtocols = [UIViewControllerInteractiveTransitioning];
owner: WeakRef<PageTransition>;
interactiveState: TransitionInteractiveState;
static initWithOwner(owner: WeakRef<PageTransition>) {
const ctrl = <PercentInteractiveController>PercentInteractiveController.new();
ctrl.owner = owner;
return ctrl;
}
startInteractiveTransition(transitionContext: UIViewControllerContextTransitioning) {
// console.log('startInteractiveTransition');
if (!this.interactiveState) {
this.interactiveState = {
transitionContext,
};
const owner = this.owner?.deref();
if (owner) {
const state = SharedTransition.getState(owner.id);
SharedTransitionHelper.interactiveStart(state, this.interactiveState, 'page');
}
}
}
updateInteractiveTransition(percentComplete: number) {
const owner = this.owner?.deref();
if (owner) {
const state = SharedTransition.getState(owner.id);
SharedTransitionHelper.interactiveUpdate(state, this.interactiveState, 'page', percentComplete);
}
}
cancelInteractiveTransition() {
// console.log('cancelInteractiveTransition');
const owner = this.owner?.deref();
if (owner) {
const state = SharedTransition.getState(owner.id);
SharedTransitionHelper.interactiveCancel(state, this.interactiveState, 'page');
}
}
finishInteractiveTransition() {
// console.log('finishInteractiveTransition');
const owner = this.owner?.deref();
if (owner) {
const state = SharedTransition.getState(owner.id);
SharedTransitionHelper.interactiveFinish(state, this.interactiveState, 'page');
}
}
}
@NativeClass()
class PageTransitionController extends NSObject implements UIViewControllerAnimatedTransitioning {
static ObjCProtocols = [UIViewControllerAnimatedTransitioning];
owner: WeakRef<PageTransition>;
static initWithOwner(owner: WeakRef<PageTransition>) {
const ctrl = <PageTransitionController>PageTransitionController.new();
ctrl.owner = owner;
return ctrl;
}
transitionDuration(transitionContext: UIViewControllerContextTransitioning): number {
const owner = this.owner.deref();
if (owner) {
return owner.getDuration();
}
return DEFAULT_DURATION;
}
animateTransition(transitionContext: UIViewControllerContextTransitioning): void {
const owner = this.owner.deref();
if (owner) {
// console.log('--- PageTransitionController animateTransition');
const state = SharedTransition.getState(owner.id);
if (!state) {
return;
}
SharedTransitionHelper.animate(state, transitionContext, 'page');
}
}
}

View File

@ -0,0 +1,12 @@
import type { TransitionInteractiveState, TransitionNavigationType } from '.';
import { SharedTransitionState } from './shared-transition';
export class SharedTransitionHelper {
static animate(state: SharedTransitionState, transitionContext: any, type: TransitionNavigationType) {
// may be able to consolidate android handling here in future
}
static interactiveStart(state: SharedTransitionState, interactiveState: TransitionInteractiveState, type: TransitionNavigationType): void {}
static interactiveUpdate(state: SharedTransitionState, interactiveState: TransitionInteractiveState, type: TransitionNavigationType, percent: number): void {}
static interactiveCancel(state: SharedTransitionState, interactiveState: TransitionInteractiveState, type: TransitionNavigationType): void {}
static interactiveFinish(state: SharedTransitionState, interactiveState: TransitionInteractiveState, type: TransitionNavigationType): void {}
}

View File

@ -0,0 +1,14 @@
import type { TransitionInteractiveState, TransitionNavigationType } from '.';
import type { SharedTransitionState } from './shared-transition';
/**
* Platform helper to aid in creating your own custom Shared Element Transition classes.
* (iOS Only)
*/
export declare class SharedTransitionHelper {
static animate(state: SharedTransitionState, transitionContext: any /* iOS: UIViewControllerContextTransitioning */, type: TransitionNavigationType): void;
static interactiveStart(state: SharedTransitionState, interactiveState: TransitionInteractiveState, type: TransitionNavigationType): void;
static interactiveUpdate(state: SharedTransitionState, interactiveState: TransitionInteractiveState, type: TransitionNavigationType, percent: number): void;
static interactiveCancel(state: SharedTransitionState, interactiveState: TransitionInteractiveState, type: TransitionNavigationType): void;
static interactiveFinish(state: SharedTransitionState, interactiveState: TransitionInteractiveState, type: TransitionNavigationType): void;
}

View File

@ -0,0 +1,546 @@
import type { TransitionInteractiveState, TransitionNavigationType } from '.';
import { getPageStartDefaultsForType, getRectFromProps, getSpringFromProps, SharedTransition, SharedTransitionAnimationType, SharedTransitionEventData, SharedTransitionState } from './shared-transition';
import { isNumber } from '../../utils/types';
import { Screen } from '../../platform';
import { iOSNativeHelper } from '../../utils/native-helper';
interface PlatformTransitionInteractiveState extends TransitionInteractiveState {
transitionContext?: UIViewControllerContextTransitioning;
propertyAnimator?: UIViewPropertyAnimator;
}
export class SharedTransitionHelper {
static animate(state: SharedTransitionState, transitionContext: UIViewControllerContextTransitioning, type: TransitionNavigationType) {
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<SharedTransitionEventData>({
eventName: SharedTransition.startedEvent,
data: {
id: transition.id,
type,
action: 'present',
},
});
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 (!transition.sharedElements) {
transition.sharedElements = {
presented: [],
presenting: [],
independent: [],
};
}
if (SharedTransition.DEBUG) {
console.log(` ${type}: 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) {
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 ? 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;
// 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<SharedTransitionEventData>({
eventName: SharedTransition.finishedEvent,
data: {
id: transition?.id,
type,
action: 'present',
},
});
};
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<SharedTransitionEventData>({
eventName: SharedTransition.startedEvent,
data: {
id: transition?.id,
type,
action: 'dismiss',
},
});
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(` ${type}: 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<SharedTransitionEventData>({
eventName: SharedTransition.finishedEvent,
data: {
id: transition?.id,
type,
action: 'dismiss',
},
});
};
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: PlatformTransitionInteractiveState, type: TransitionNavigationType) {
SharedTransition.events().notify<SharedTransitionEventData>({
eventName: SharedTransition.startedEvent,
data: {
id: state?.instance?.id,
type,
action: 'interactiveStart',
},
});
switch (type) {
case 'page':
interactiveState.transitionContext.containerView.insertSubviewBelowSubview(state.instance.presenting.view, state.instance.presented.view);
break;
}
}
static interactiveUpdate(state: SharedTransitionState, interactiveState: PlatformTransitionInteractiveState, type: TransitionNavigationType, 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;
SharedTransition.events().notify<SharedTransitionEventData>({
eventName: SharedTransition.interactiveUpdateEvent,
data: {
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;
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<SharedTransitionEventData>({
eventName: SharedTransition.interactiveCancelledEvent,
data: {
id: state?.instance?.id,
type,
},
});
}, duration * 1000);
}
}
static interactiveFinish(state: SharedTransitionState, interactiveState: PlatformTransitionInteractiveState, type: TransitionNavigationType) {
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<SharedTransitionEventData>({
eventName: SharedTransition.finishedEvent,
data: {
id: state?.instance?.id,
type,
action: 'interactiveFinish',
},
});
}, duration * 1000);
}
}
}

View File

@ -0,0 +1,73 @@
import { SharedTransition, SharedTransitionAnimationType } from './shared-transition';
describe('SharedTransition', () => {
it('custom should create a Transition instance', () => {
const transition = SharedTransition.custom(new CustomTransition());
expect(transition.instance.id).toBe(1);
});
it('custom should create a Transition with default state and options', () => {
const transition = SharedTransition.custom(new CustomTransition());
const state = SharedTransition.getState(transition.instance.id);
expect(state.activeType).toBe(SharedTransitionAnimationType.present);
expect(state.pageStart).toBeUndefined();
expect(state.pageEnd).toBeUndefined();
expect(state.pageReturn).toBeUndefined();
expect(state.interactive).toBeUndefined();
});
it('custom should create a Transition with custom options', () => {
const transition = SharedTransition.custom(new CustomTransition(), {
interactive: {
dismiss: {
finishThreshold: 0.6,
percentFormula: (args) => {
return args.deltaX - 0.2;
},
},
},
pageStart: {
x: 200,
y: 100,
spring: {
friction: 40,
},
},
pageEnd: {
x: 0,
y: 0,
},
pageReturn: {
x: -200,
y: -100,
width: 25,
},
});
const state = SharedTransition.getState(transition.instance.id);
expect(state.activeType).toBe(SharedTransitionAnimationType.present);
expect(state.interactive.dismiss.finishThreshold).toBe(0.6);
expect(state.interactive.dismiss.percentFormula({ deltaX: 0.9, deltaY: 0, state: 0, android: null, eventName: 'pan', ios: null, object: null, type: 3, view: null })).toBe(0.7);
expect(state.pageStart.x).toBe(200);
expect(state.pageStart.y).toBe(100);
expect(state.pageStart.spring.friction).toBe(40);
expect(state.pageEnd.x).toBe(0);
expect(state.pageEnd.y).toBe(0);
expect(state.pageReturn.x).toBe(-200);
expect(state.pageReturn.y).toBe(-100);
expect(state.pageReturn.width).toBe(25);
});
});
class CustomTransition {
id: number;
constructor() {
this.id = 1;
}
getDuration() {
return 0.35;
}
setDuration(value: number) {}
getCurve() {}
animateIOSTransition() {}
createAndroidAnimator() {}
}

View File

@ -0,0 +1,296 @@
import type { Transition, TransitionNavigationType } from '.';
import { Observable } from '../../data/observable';
import { Screen } from '../../platform';
import { isNumber } from '../../utils/types';
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 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 & {
/**
* (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 };
/**
* Spring animation settings.
* Defaults to 140 tension with 10 friction.
*/
spring?: SharedSpringProperties;
};
type SharedTransitionPageWithDurationProperties = SharedTransitionPageProperties & {
/**
* Linear duration in milliseconds
* Note: When this is defined, it will override spring options and use only linear animation.
*/
duration?: number | undefined | null;
};
export interface SharedTransitionInteractiveOptions {
/**
* When the pan exceeds this percentage and you let go, finish the transition.
* Default 0.5
*/
finishThreshold?: number;
/**
* You can create your own percent formula used for determing the interactive value.
* By default, we handle this via a formula like this for an interactive page back transition:
* - return eventData.deltaX / (eventData.ios.view.bounds.size.width / 2);
* @param eventData PanGestureEventData
* @returns The percentage value to be used as the finish/cancel threshold
*/
percentFormula?: (eventData: PanGestureEventData) => number;
}
export interface SharedTransitionConfig {
/**
* Interactive transition settings. (iOS only at the moment)
*/
interactive?: {
/**
* Whether you want to allow interactive dismissal.
* Defaults to using 'pan' gesture for dismissal however you can customize your own.
*/
dismiss?: SharedTransitionInteractiveOptions;
};
/**
* View settings to start your transition with.
*/
pageStart?: SharedTransitionPageProperties;
/**
* View settings to end your transition with.
*/
pageEnd?: SharedTransitionPageWithDurationProperties;
/**
* View settings to return to the original page with.
*/
pageReturn?: SharedTransitionPageWithDurationProperties;
}
export interface SharedTransitionState extends SharedTransitionConfig {
/**
* (Internally used) Preconfigured transition or your own custom configured one.
*/
instance?: Transition;
/**
* Page which will start the transition.
*/
page?: ViewBase;
activeType?: SharedTransitionAnimationType;
toPage?: ViewBase;
/**
* Whether interactive transition has began.
*/
interactiveBegan?: boolean;
/**
* Whether interactive transition was cancelled.
*/
interactiveCancelled?: boolean;
}
class SharedTransitionObservable extends Observable {
// @ts-ignore
on(eventNames: string, callback: (data: SharedTransitionEventData) => void, thisArg?: any) {
super.on(eventNames, <any>callback, thisArg);
}
}
let sharedTransitionEvents: SharedTransitionObservable;
let currentStack: Array<SharedTransitionState>;
/**
* Shared Element Transitions (preview)
* Allows you to auto animate between shared elements on two different screesn to create smooth navigational experiences.
* View components can define sharedTransitionTag="name" alone with a transition through this API.
*/
export class SharedTransition {
/**
* Configure a custom transition with presentation/dismissal options.
* @param transition The custom Transition instance.
* @param options
* @returns a configured SharedTransition instance for use with navigational APIs.
*/
static custom(transition: Transition, options?: SharedTransitionConfig): { instance: Transition } {
SharedTransition.updateState(transition.id, {
...(options || {}),
instance: transition,
activeType: SharedTransitionAnimationType.present,
});
const pageEnd = options?.pageEnd;
if (isNumber(pageEnd?.duration)) {
transition.setDuration(pageEnd?.duration);
}
return { instance: transition };
}
/**
* Listen to various shared element transition events.
* @returns Observable
*/
static events(): SharedTransitionObservable {
if (!sharedTransitionEvents) {
sharedTransitionEvents = new SharedTransitionObservable();
}
return sharedTransitionEvents;
}
/**
* When the transition starts.
*/
static startedEvent = 'SharedTransitionStartedEvent';
/**
* When the transition finishes.
*/
static finishedEvent = 'SharedTransitionFinishedEvent';
/**
* When the interactive transition cancels.
*/
static interactiveCancelledEvent = 'SharedTransitionInteractiveCancelledEvent';
/**
* When the interactive transition updates with the percent value.
*/
static interactiveUpdateEvent = 'SharedTransitionInteractiveUpdateEvent';
/**
* Enable to see various console logging output of Shared Element Transition behavior.
*/
static DEBUG = false;
/**
* Update transition state.
* @param id Transition instance id
* @param state SharedTransitionState
*/
static updateState(id: number, state: SharedTransitionState) {
if (!currentStack) {
currentStack = [];
}
const existingTransition = SharedTransition.getState(id);
if (existingTransition) {
// updating existing
for (const key in state) {
existingTransition[key] = state[key];
// console.log(' ... updating state: ', key, state[key])
}
} else {
currentStack.push(state);
}
}
/**
* Get current state for any transition.
* @param id Transition instance id
*/
static getState(id: number) {
return currentStack?.find((t) => t.instance?.id === id);
}
/**
* Finish transition state.
* @param id Transition instance id
*/
static finishState(id: number) {
const index = currentStack?.findIndex((t) => t.instance?.id === id);
if (index > -1) {
currentStack.splice(index, 1);
}
}
/**
* Gather view collections based on sharedTransitionTag details.
* @param fromPage Page moving away from
* @param toPage Page moving to
* @returns Collections of views pertaining to shared elements or particular pages
*/
static getSharedElements(
fromPage: ViewBase,
toPage: ViewBase
): {
sharedElements: Array<View>;
presented: Array<View>;
presenting: Array<View>;
} {
// 1. Presented view: gather all sharedTransitionTag views
const presentedSharedElements = <Array<View>>querySelectorAll(toPage, 'sharedTransitionTag').filter((v) => !v.sharedTransitionIgnore);
// console.log('presented sharedTransitionTag total:', presentedSharedElements.length);
// 2. Presenting view: gather all sharedTransitionTag views
const presentingSharedElements = <Array<View>>querySelectorAll(fromPage, 'sharedTransitionTag').filter((v) => !v.sharedTransitionIgnore);
// 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);
return {
sharedElements: presentingSharedElements.filter((v) => presentedTags.includes(v.sharedTransitionTag)),
presented: presentedSharedElements,
presenting: presentingSharedElements,
};
}
}
/**
* Get dimensional rectangle (x,y,width,height) from properties with fallbacks for any undefined values.
* @param props combination of properties conformed to SharedTransitionPageProperties
* @param defaults fallback properties when props doesn't contain a value for it
* @returns { x,y,width,height }
*/
export function getRectFromProps(props: SharedTransitionPageProperties, defaults?: SharedRect): SharedRect {
defaults = {
x: 0,
y: 0,
width: Screen.mainScreen.widthDIPs,
height: Screen.mainScreen.heightDIPs,
...(defaults || {}),
};
return {
x: isNumber(props?.x) ? props?.x : defaults.x,
y: isNumber(props?.y) ? props?.y : defaults.y,
width: isNumber(props?.width) ? props?.width : defaults.width,
height: isNumber(props?.height) ? props?.height : defaults.height,
};
}
/**
* Get spring properties with default fallbacks for any undefined values.
* @param props various spring related properties conforming to SharedSpringProperties
* @returns
*/
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,
};
}
/**
* Page starting defaults for provided type.
* @param type TransitionNavigationType
* @returns { x,y,width,height }
*/
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,
};
}

View File

@ -8,7 +8,7 @@ const screenHeight = lazy(() => Screen.mainScreen.heightPixels);
export class SlideTransition extends Transition {
private _direction: string;
constructor(direction: string, duration: number, curve: any) {
constructor(direction: string, duration: number = 350, curve?: any) {
super(duration, curve);
this._direction = direction;
}

View File

@ -1,5 +1,5 @@
import { Transition } from '.';
export class SlideTransition extends Transition {
constructor(direction: string, duration: number, nativeCurve: any);
constructor(direction: string, duration?: number, nativeCurve?: any);
}

View File

@ -1,28 +1,67 @@
import { Transition } from '.';
import { Screen } from '../../platform';
const leftEdge = CGAffineTransformMakeTranslation(-Screen.mainScreen.widthDIPs, 0);
const rightEdge = CGAffineTransformMakeTranslation(Screen.mainScreen.widthDIPs, 0);
const topEdge = CGAffineTransformMakeTranslation(0, -Screen.mainScreen.heightDIPs);
const bottomEdge = CGAffineTransformMakeTranslation(0, Screen.mainScreen.heightDIPs);
import { DEFAULT_DURATION } from './shared-transition';
export class SlideTransition extends Transition {
private _direction: string;
transitionController: SlideTransitionController;
presented: UIViewController;
presenting: UIViewController;
operation: number;
direction: string;
constructor(direction: string, duration: number, curve: any) {
super(duration, curve);
this._direction = direction;
this.direction = direction;
}
public animateIOSTransition(containerView: UIView, fromView: UIView, toView: UIView, operation: UINavigationControllerOperation, completion: (finished: boolean) => void): void {
iosNavigatedController(navigationController: UINavigationController, operation: number, fromVC: UIViewController, toVC: UIViewController): UIViewControllerAnimatedTransitioning {
this.transitionController = SlideTransitionController.initWithOwner(new WeakRef(this));
this.presented = toVC;
this.presenting = fromVC;
this.operation = operation;
// console.log('presenting:', presenting)
return this.transitionController;
}
}
@NativeClass()
export class SlideTransitionController extends NSObject implements UIViewControllerAnimatedTransitioning {
static ObjCProtocols = [UIViewControllerAnimatedTransitioning];
owner: WeakRef<SlideTransition>;
static initWithOwner(owner: WeakRef<SlideTransition>) {
const ctrl = <SlideTransitionController>SlideTransitionController.new();
ctrl.owner = owner;
return ctrl;
}
transitionDuration(transitionContext: UIViewControllerContextTransitioning): number {
const owner = this.owner.deref();
if (owner) {
return owner.getDuration();
}
return DEFAULT_DURATION;
}
animateTransition(transitionContext: UIViewControllerContextTransitioning): void {
// console.log('SlideTransitionController animateTransition');
const owner = this.owner.deref();
if (owner) {
const toView = owner.presented.view;
const originalToViewTransform = toView.transform;
const fromView = owner.presenting.view;
const originalFromViewTransform = fromView.transform;
let fromViewEndTransform: CGAffineTransform;
let toViewBeginTransform: CGAffineTransform;
const push = operation === UINavigationControllerOperation.Push;
const push = owner.operation === UINavigationControllerOperation.Push;
switch (this._direction) {
const leftEdge = CGAffineTransformMakeTranslation(-Screen.mainScreen.widthDIPs, 0);
const rightEdge = CGAffineTransformMakeTranslation(Screen.mainScreen.widthDIPs, 0);
const topEdge = CGAffineTransformMakeTranslation(0, -Screen.mainScreen.heightDIPs);
const bottomEdge = CGAffineTransformMakeTranslation(0, Screen.mainScreen.heightDIPs);
switch (owner.direction) {
case 'left':
toViewBeginTransform = push ? rightEdge : leftEdge;
fromViewEndTransform = push ? leftEdge : rightEdge;
@ -44,17 +83,17 @@ export class SlideTransition extends Transition {
toView.transform = toViewBeginTransform;
fromView.transform = CGAffineTransformIdentity;
switch (operation) {
switch (owner.operation) {
case UINavigationControllerOperation.Push:
containerView.insertSubviewAboveSubview(toView, fromView);
transitionContext.containerView.insertSubviewAboveSubview(toView, fromView);
break;
case UINavigationControllerOperation.Pop:
containerView.insertSubviewBelowSubview(toView, fromView);
transitionContext.containerView.insertSubviewBelowSubview(toView, fromView);
break;
}
const duration = this.getDuration();
const curve = this.getCurve();
const duration = owner.getDuration();
const curve = owner.getCurve();
UIView.animateWithDurationAnimationsCompletion(
duration,
() => {
@ -65,8 +104,9 @@ export class SlideTransition extends Transition {
(finished: boolean) => {
toView.transform = originalToViewTransform;
fromView.transform = originalFromViewTransform;
completion(finished);
transitionContext.completeTransition(finished);
}
);
}
}
}

View File

@ -231,4 +231,39 @@ export namespace iOSNativeHelper {
* Checks whether the application is running on real device and not on simulator.
*/
export function isRealDevice(): boolean;
/**
* Debug utility to insert CGRect values into logging output.
* Note: when printing a CGRect directly it will print blank so this helps show the values.
* @param rect CGRect
*/
export function printCGRect(rect: CGRect): void;
/**
* Take a snapshot of a View on screen.
* @param view view to snapshot
* @param scale screen scale
*/
export function snapshotView(view: UIView, scale: number): UIImage;
/**
* Copy layer properties from one view to another.
* @param view a view to copy layer properties to
* @param toView a view to copy later properties from
*/
export function copyLayerProperties(view: UIView, toView: UIView): void;
/**
* Animate views with a configurable spring effect
* @param options various animation settings for the spring
* - tension: number
* - friction: number
* - mass: number
* - delay: number
* - velocity: number
* - animateOptions: UIViewAnimationOptions
* - animations: () => void, Callback containing the property changes you want animated
* - completion: (finished: boolean) => void, Callback when animation is finished
*/
export function animateWithSpring(options?: { tension?: number; friction?: number; mass?: number; delay?: number; velocity?: number; animateOptions?: UIViewAnimationOptions; animations?: () => void; completion?: (finished?: boolean) => void });
}

View File

@ -355,4 +355,84 @@ export namespace iOSNativeHelper {
return true;
}
}
export function printCGRect(rect: CGRect) {
if (rect) {
return `CGRect(${rect.origin.x} ${rect.origin.y} ${rect.size.width} ${rect.size.height})`;
}
}
export function snapshotView(view: UIView, scale: number): UIImage {
if (view instanceof UIImageView) {
return view.image;
}
// console.log('snapshotView view.frame:', printRect(view.frame));
UIGraphicsBeginImageContextWithOptions(CGSizeMake(view.frame.size.width, view.frame.size.height), false, scale);
view.layer.renderInContext(UIGraphicsGetCurrentContext());
const image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}
export function copyLayerProperties(view: UIView, toView: UIView) {
const viewPropertiesToMatch: Array<keyof UIView> = ['backgroundColor'];
const layerPropertiesToMatch: Array<keyof CALayer> = ['cornerRadius', 'borderWidth', 'borderColor'];
viewPropertiesToMatch.forEach((property) => {
if (view[property] !== toView[property]) {
// console.log('| -- matching view property:', property);
view[property as any] = toView[property];
}
});
layerPropertiesToMatch.forEach((property) => {
if (view.layer[property] !== toView.layer[property]) {
// console.log('| -- matching layer property:', property);
view.layer[property as any] = toView.layer[property];
}
});
}
export function animateWithSpring(options?: { tension?: number; friction?: number; mass?: number; delay?: number; velocity?: number; animateOptions?: UIViewAnimationOptions; animations?: () => void; completion?: (finished?: boolean) => void }) {
const opt = {
tension: 140,
friction: 10,
mass: 1.0,
delay: 0,
velocity: 0,
animateOptions: null,
animations: null,
completion: null,
...(options || {}),
};
// console.log('createSpringAnimator', opt);
const damping = opt.friction / Math.sqrt(2 * opt.tension);
const undampedFrequency = Math.sqrt(opt.tension / opt.mass);
// console.log({
// damping,
// undampedFrequency
// })
const epsilon = 0.001;
let duration = 0;
if (damping < 1) {
// console.log('damping < 1');
const a = Math.sqrt(1 - Math.pow(damping, 2));
const b = opt.velocity / (a * undampedFrequency);
const c = damping / a;
const d = -((b - c) / epsilon);
if (d > 0) {
duration = Math.log(d) / (damping * undampedFrequency);
}
}
if (duration === 0) {
UIView.animateWithDurationAnimationsCompletion(0, opt.animations, opt.completion);
return;
}
UIView.animateWithDurationDelayUsingSpringWithDampingInitialSpringVelocityOptionsAnimationsCompletion(duration, opt.delay, damping, opt.velocity, opt.animateOptions, opt.animations, opt.completion);
}
}

View File

@ -31,3 +31,16 @@ export function notNegative(value: Object): boolean {
export const radiansToDegrees = (a: number) => a * (180 / Math.PI);
export const degreesToRadians = (a: number) => a * (Math.PI / 180);
/**
* Map value changes across a set of criteria
* @param val value to map
* @param in_min minimum
* @param in_max maximum
* @param out_min starting value
* @param out_max ending value
* @returns
*/
export function valueMap(val: number, in_min: number, in_max: number, out_min: number, out_max: number) {
return ((val - in_min) * (out_max - out_min)) / (in_max - in_min) + out_min;
}

View File

@ -2606,7 +2606,7 @@ export interface TraceWriter {
export class Transition {
constructor(duration: number, nativeCurve: any);
// (undocumented)
public animateIOSTransition(containerView: any, fromView: any, toView: any, operation: any, completion: (finished: boolean) => void): void;
public animateIOSTransition(transitionContext: UIViewControllerContextTransitioning, fromViewCtrl: UIViewController, toViewCtrl: UIViewController, operation: UINavigationControllerOperation): void;
// (undocumented)
public createAndroidAnimator(transitionType: string): any;
// (undocumented)