mirror of
https://github.com/NativeScript/NativeScript.git
synced 2025-08-14 18:12:09 +08:00
feat(core): Shared Element Transitions (#10022)
This commit is contained in:
@ -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 {}
|
||||
|
@ -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 {}
|
||||
|
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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" />
|
||||
|
32
apps/toolbox/src/pages/transitions.ts
Normal file
32
apps/toolbox/src/pages/transitions.ts
Normal 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,
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
13
apps/toolbox/src/pages/transitions.xml
Normal file
13
apps/toolbox/src/pages/transitions.xml
Normal 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>
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
111
apps/toolbox/src/pages/transitions/transition-example.ts
Normal file
111
apps/toolbox/src/pages/transitions/transition-example.ts
Normal 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,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
40
apps/toolbox/src/pages/transitions/transition-example.xml
Normal file
40
apps/toolbox/src/pages/transitions/transition-example.xml
Normal 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>
|
@ -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);
|
||||
}
|
||||
}
|
@ -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>
|
10
apps/toolbox/src/pages/transitions/transitions-detail.ts
Normal file
10
apps/toolbox/src/pages/transitions/transitions-detail.ts
Normal 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 {}
|
19
apps/toolbox/src/pages/transitions/transitions-detail.xml
Normal file
19
apps/toolbox/src/pages/transitions/transitions-detail.xml
Normal 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>
|
22
apps/toolbox/src/pages/transitions/transitions-modal.ts
Normal file
22
apps/toolbox/src/pages/transitions/transitions-modal.ts
Normal 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();
|
||||
}
|
||||
}
|
16
apps/toolbox/src/pages/transitions/transitions-modal.xml
Normal file
16
apps/toolbox/src/pages/transitions/transitions-modal.xml
Normal 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>
|
@ -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';
|
||||
|
@ -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> = {};
|
||||
|
||||
|
2
packages/core/global-types.d.ts
vendored
2
packages/core/global-types.d.ts
vendored
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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",
|
||||
|
544
packages/core/ui/core/view-base/index.d.ts
vendored
544
packages/core/ui/core/view-base/index.d.ts
vendored
@ -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;
|
@ -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();
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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);
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
@ -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) {
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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';
|
||||
|
@ -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() {
|
||||
|
45
packages/core/ui/page/page-common.d.ts
vendored
45
packages/core/ui/page/page-common.d.ts
vendored
@ -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;
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,3 @@
|
||||
import { Transition } from '.';
|
||||
|
||||
export class FadeTransition extends Transition {
|
||||
constructor(duration: number, nativeCurve: any);
|
||||
}
|
||||
export class FadeTransition extends Transition {}
|
||||
|
@ -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);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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}`;
|
||||
}
|
||||
}
|
||||
|
52
packages/core/ui/transition/index.d.ts
vendored
52
packages/core/ui/transition/index.d.ts
vendored
@ -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;
|
||||
}
|
||||
|
@ -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}`;
|
||||
}
|
||||
}
|
||||
|
8
packages/core/ui/transition/modal-transition.android.ts
Normal file
8
packages/core/ui/transition/modal-transition.android.ts
Normal 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.');
|
||||
}
|
||||
}
|
2
packages/core/ui/transition/modal-transition.d.ts
vendored
Normal file
2
packages/core/ui/transition/modal-transition.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
import { Transition } from '.';
|
||||
export declare class ModalTransition extends Transition {}
|
180
packages/core/ui/transition/modal-transition.ios.ts
Normal file
180
packages/core/ui/transition/modal-transition.ios.ts
Normal 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');
|
||||
}
|
||||
}
|
||||
}
|
196
packages/core/ui/transition/page-transition.android.ts
Normal file
196
packages/core/ui/transition/page-transition.android.ts
Normal 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,
|
||||
};
|
||||
}
|
2
packages/core/ui/transition/page-transition.d.ts
vendored
Normal file
2
packages/core/ui/transition/page-transition.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
import { Transition } from '.';
|
||||
export declare class PageTransition extends Transition {}
|
204
packages/core/ui/transition/page-transition.ios.ts
Normal file
204
packages/core/ui/transition/page-transition.ios.ts
Normal 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');
|
||||
}
|
||||
}
|
||||
}
|
@ -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 {}
|
||||
}
|
14
packages/core/ui/transition/shared-transition-helper.d.ts
vendored
Normal file
14
packages/core/ui/transition/shared-transition-helper.d.ts
vendored
Normal 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;
|
||||
}
|
546
packages/core/ui/transition/shared-transition-helper.ios.ts
Normal file
546
packages/core/ui/transition/shared-transition-helper.ios.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
73
packages/core/ui/transition/shared-transition.spec.ts
Normal file
73
packages/core/ui/transition/shared-transition.spec.ts
Normal 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() {}
|
||||
}
|
296
packages/core/ui/transition/shared-transition.ts
Normal file
296
packages/core/ui/transition/shared-transition.ts
Normal 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,
|
||||
};
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
35
packages/core/utils/native-helper.d.ts
vendored
35
packages/core/utils/native-helper.d.ts
vendored
@ -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 });
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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)
|
||||
|
Reference in New Issue
Block a user