feat(core): shared element transitions with api for ios

This commit is contained in:
Nathan Walker
2023-03-05 20:40:41 -08:00
parent 8f09e40e09
commit a29a809ad5
23 changed files with 638 additions and 130 deletions

View File

@@ -1,2 +1,2 @@
<Frame defaultPage="main-page">
<Frame defaultPage="pages/transitions">
</Frame>

View File

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

View File

@@ -0,0 +1,47 @@
import { Observable, EventData, Page, ShowModalOptions, SharedTransition, ModalTransition, TransitionType, Screen } from '@nativescript/core';
// import { DetailTransition } from './transitions/detail';
let page: Page;
export function navigatingTo(args: EventData) {
page = <Page>args.object;
page.bindingContext = new TransitionsModel();
}
// class SampleCustomModalTransition extends ModalTransition implements TransitionType {
// }
export class TransitionsModel extends Observable {
open() {
// const detailTransition = new DetailTransition(400, global.isIOS ? UIViewAnimationCurve.EaseInOut : null);
// detailTransition.page = page;
page.frame.navigate({
moduleName: `pages/transitions/transitions-detail`,
// transition: {
// instance: detailTransition,
// },
});
}
openModal() {
page.showModal('pages/transitions/transitions-modal', {
context: {},
transition: {
instance: SharedTransition.configure({
page,
instance: new ModalTransition(),
incomingViewStart: {
y: 200,
duration: 1000,
},
dismissViewEnd: {
y: 100,
duration: 500,
},
}),
},
closeCallback(args) {
console.log('close modal callback', args);
},
} as ShowModalOptions);
}
}

View File

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

View File

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

View File

@@ -0,0 +1,10 @@
<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="*" columns="auto,*" verticalAlignment="top">
<Image row="0" sharedTransitionTag="image" src="https://cdn.pixabay.com/photo/2012/08/27/14/19/mountains-55067__340.png" width="100%" />
</GridLayout>
</Page>

View File

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

View File

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

View File

@@ -1,5 +1,4 @@
import type { Page } from '../ui/page';
import type { ViewBase } from '../ui/core/view-base';
import type { View } from '../ui/core/view';
import type { AndroidAccessibilityEvent } from './accessibility-types';

View File

@@ -121,7 +121,7 @@ function ensureNativeClasses() {
});
}
export function setupAccessibleView(view: Partial<ViewBase>): void {
export function setupAccessibleView(view: View): void {
const uiView = view.nativeViewProtected as UIView;
if (!uiView) {
return;

View File

@@ -8,7 +8,7 @@ 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';
import type { CustomTransitionModal } from '../../transition';
import type { ModalTransition } from '../../transition/modal-transition';
/**
* Iterates through all child views (via visual tree) and executes a function.
@@ -43,9 +43,17 @@ export function getViewById(view: ViewBase, id: string): ViewBase;
*/
export function getViewByDomId(view: ViewBase, domId: number): ViewBase;
export interface ModalTransition {
/**
* 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: ViewBase, selector: string): Array<ViewBase>;
export interface ModalTransitionType {
name?: string;
instance?: CustomTransitionModal;
instance?: ModalTransition;
duration?: number;
curve?: any;
}
@@ -298,6 +306,11 @@ export abstract class ViewBase extends Observable {
*/
public className: string;
/**
* Gets or sets the shared transition tag for animated view transitions
*/
public sharedTransitionTag: string;
/**
* Gets owner page. This is a read-only property.
*/

View File

@@ -10,7 +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 { CustomTransitionModal } from '../../transition';
import type { ModalTransition } from '../../transition/modal-transition';
// TODO: Remove this import!
import { getClass } from '../../../utils/types';
@@ -38,9 +38,9 @@ function ensureStyleScopeModule() {
const defaultBindingSource = {};
export interface ModalTransition {
export interface ModalTransitionType {
name?: string;
instance?: CustomTransitionModal;
instance?: ModalTransition;
duration?: number;
curve?: any;
}
@@ -74,7 +74,7 @@ export interface ShowModalOptions {
/**
* An optional custom transition effect
*/
transition?: ModalTransition;
transition?: ModalTransitionType;
/**
* An optional parameter that specify options specific to iOS as an object.
@@ -181,6 +181,30 @@ export function getViewByDomId(view: ViewBaseDefinition, domId: number): ViewBas
return retVal;
}
// TODO: allow all selector types (just using attributes now)
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;
}
export function eachDescendant(view: ViewBaseDefinition, callback: (child: ViewBaseDefinition) => boolean) {
if (!callback || !view) {
return;
@@ -283,6 +307,7 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
public id: string;
public className: string;
public sharedTransitionTag: string;
public _domId: number;
public _context: any;
@@ -453,7 +478,7 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
return true;
});
setupAccessibleView(this);
setupAccessibleView(<any>this);
this._emit('loaded');
}

View File

@@ -932,41 +932,41 @@ class UIViewControllerTransitioningDelegateImpl extends NSObject implements UIVi
}
animationControllerForDismissedController?(dismissed: UIViewController): UIViewControllerAnimatedTransitioning {
const owner = this.owner?.get();
if (owner?.dismissedController) {
return owner.dismissedController(dismissed);
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?.get();
if (owner?.presentedController) {
return owner.presentedController(presented, presenting, source);
const owner = this.owner?.deref();
if (owner?.iosPresentedController) {
return owner.iosPresentedController(presented, presenting, source);
}
return null;
}
interactionControllerForDismissal?(animator: UIViewControllerAnimatedTransitioning): UIViewControllerInteractiveTransitioning {
const owner = this.owner?.get();
if (owner?.interactionDismiss) {
return owner.interactionDismiss(animator);
const owner = this.owner?.deref();
if (owner?.iosInteractionDismiss) {
return owner.iosInteractionDismiss(animator);
}
return null;
}
interactionControllerForPresentation?(animator: UIViewControllerAnimatedTransitioning): UIViewControllerInteractiveTransitioning {
const owner = this.owner?.get();
if (owner?.interactionPresented) {
return owner.interactionPresented(animator);
const owner = this.owner?.deref();
if (owner?.iosInteractionPresented) {
return owner.iosInteractionPresented(animator);
}
return null;
}
presentationControllerForPresentedViewControllerPresentingViewControllerSourceViewController?(presented: UIViewController, presenting: UIViewController, source: UIViewController): UIPresentationController {
const owner = this.owner?.get();
if (owner?.presentedViewController) {
return owner.presentedViewController(presented, presenting, source);
const owner = this.owner?.deref();
if (owner?.iosPresentedViewController) {
return owner.iosPresentedViewController(presented, presenting, source);
}
return null;
}

View File

@@ -11,7 +11,7 @@ export { ContentView } from './content-view';
export { Binding } from './core/bindable';
export type { BindingOptions } from './core/bindable';
export { ControlStateChangeListener } from './core/control-state-change';
export { ViewBase, eachDescendant, getAncestor, getViewById, booleanConverter } from './core/view-base';
export { ViewBase, eachDescendant, getAncestor, getViewById, booleanConverter, querySelectorAll } from './core/view-base';
export type { ShowModalOptions } from './core/view-base';
export { View, CSSType, ContainerView, ViewHelper, IOSHelper, isUserInteractionEnabledProperty, PseudoClassHandler, CustomLayoutView } from './core/view';
export type { Template, KeyedTemplate, ShownModallyData, AddArrayFromBuilder, AddChildFromBuilder, Size } from './core/view';
@@ -74,6 +74,9 @@ export { Span } from './text-base/span';
export { TextField } from './text-field';
export { TextView } from './text-view';
export { TimePicker } from './time-picker';
export { Transition, CustomTransitionModal } from './transition';
export { Transition } from './transition';
export { ModalTransition } from './transition/modal-transition';
export { SharedTransition, SharedTransitionAnimationType } from './transition/shared-transition';
export type { TransitionType } from './transition';
export { WebView } from './web-view';
export type { LoadEventData, WebViewNavigationType } from './web-view';

View File

@@ -8,6 +8,7 @@ import { PageBase, actionBarHiddenProperty, statusBarStyleProperty } from './pag
import { profile } from '../../profiling';
import { iOSNativeHelper, layout } from '../../utils';
import { getLastFocusedViewOnPage, isAccessibilityServiceEnabled } from '../../accessibility';
import { SharedTransition } from '../transition/shared-transition';
export * from './page-common';
@@ -430,6 +431,8 @@ export class Page extends PageBase {
// Make transitions look good
controller.view.backgroundColor = this._backgroundColor;
console.log('here for Page:', this.viewController);
SharedTransition.addPageToTop(this);
}
createNativeView() {
@@ -465,6 +468,7 @@ export class Page extends PageBase {
public onLoaded(): void {
super.onLoaded();
console.log('page loaded');
if (this.hasActionBar) {
this.actionBar.update();
}

View File

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

View File

@@ -1,25 +1,28 @@
// Types.
import { _resolveAnimationCurve } from '../animation';
import { _resolveAnimationCurve } from '../animation';
import lazy from '../../utils/lazy';
import type { Transition as TransitionType } from '.';
export type { 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 {
@@ -39,8 +42,6 @@ export class Transition {
}
public toString(): string {
return `Transition@${this._id}`;
return `Transition@${this.id}`;
}
}
export class CustomTransitionModal extends Transition {}

View File

@@ -1,21 +1,24 @@
export declare class Transition {
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 */);
public getDuration(): number;
public getCurve(): any;
public animateIOSTransition(transitionContext: any /*UIViewControllerContextTransitioning */, fromViewCtrl: any /* UIViewController */, toViewCtrl: any /* UIViewController */, operation: any /* UINavigationControllerOperation */): void;
public createAndroidAnimator(transitionType: string): any;
public toString(): string;
export interface TransitionType {
id: number;
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 */;
iosPresentedViewController?(presented: any /* UIViewController */, presenting: any /* UIViewController */, source: any /* UIViewController */): any /* UIPresentationController */;
}
export declare class CustomTransitionModal extends Transition {
dismissedController?(dismissed: any /* UIViewController */): any /* UIViewControllerAnimatedTransitioning */;
presentedController?(presented: UIViewController, presenting: UIViewController, source: UIViewController): UIViewControllerAnimatedTransitioning;
interactionDismiss?(animator: UIViewControllerAnimatedTransitioning): UIViewControllerInteractiveTransitioning;
interactionPresented?(animator: UIViewControllerAnimatedTransitioning): UIViewControllerInteractiveTransitioning;
presentedViewController?(presented: UIViewController, presenting: UIViewController, source: UIViewController): UIPresentationController;
export declare class Transition implements TransitionType {
id: number;
static AndroidTransitionType?: { enter?: string; exit?: string; popEnter?: string; popExit?: string };
constructor(duration?: number, nativeCurve?: any /* UIViewAnimationCurve | string | CubicBezierAnimationCurve | android.view.animation.Interpolator | android.view.animation.LinearInterpolator */);
getDuration(): number;
getCurve(): any;
animateIOSTransition(transitionContext: any /*UIViewControllerContextTransitioning */, fromViewCtrl: any /* UIViewController */, toViewCtrl: any /* UIViewController */, operation: any /* UINavigationControllerOperation */): void;
createAndroidAnimator(transitionType: string): any;
toString(): string;
}

View File

@@ -1,14 +1,19 @@
let transitionId = 0;
export class Transition {
import type { TransitionType } from '.';
export type { 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 {
@@ -28,29 +33,6 @@ export class Transition {
}
public toString(): string {
return `Transition@${this._id}`;
}
}
export class CustomTransitionModal extends Transition {
// Extend this class to create your own
dismissedController?(dismissed: UIViewController): UIViewControllerAnimatedTransitioning {
return null;
}
presentedController?(presented: UIViewController, presenting: UIViewController, source: UIViewController): UIViewControllerAnimatedTransitioning {
return null;
}
interactionDismiss?(animator: UIViewControllerAnimatedTransitioning): UIViewControllerInteractiveTransitioning {
return null;
}
interactionPresented?(animator: UIViewControllerAnimatedTransitioning): UIViewControllerInteractiveTransitioning {
return null;
}
presentedViewController?(presented: UIViewController, presenting: UIViewController, source: UIViewController): UIPresentationController {
return null;
return `Transition@${this.id}`;
}
}

View File

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

View File

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

View File

@@ -0,0 +1,292 @@
import { querySelectorAll } from '../core/view-base';
import type { View } from '../core/view';
import { Screen } from '../../platform';
import { Transition } from '.';
import type { TransitionType } from '.';
import { SharedTransition, SharedTransitionAnimationType } from './shared-transition';
const printRect = (r: CGRect) => `CGRect(${r.origin.x} ${r.origin.y} ${r.size.width} ${r.size.height})`;
const printPoint = (r: CGPoint) => `CGPoint(${r.x} ${r.y})`;
const printSize = (r: CGSize) => `CGPoint(${r.width} ${r.height})`;
export class ModalTransition extends Transition implements TransitionType {
transitionController: ModalTransitionController;
presented: UIViewController;
presenting: UIViewController;
sharedElements: { presented?: Array<{ view: View; startFrame?: CGRect; endFrame?: CGRect; snapshot?: UIImageView }>; presenting?: Array<{ view: View; startFrame: CGRect; endFrame?: CGRect; snapshot?: UIImageView }> };
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 --');
return null;
}
iosInteractionPresented(animator: UIViewControllerAnimatedTransitioning): UIViewControllerInteractiveTransitioning {
console.log('-- iosInteractionPresented --');
return null;
}
iosPresentedViewController(presented: UIViewController, presenting: UIViewController, source: UIViewController): UIPresentationController {
console.log('-- iosPresentedViewController --');
return null;
}
}
@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 0.35;
}
animateTransition(transitionContext: UIViewControllerContextTransitioning): void {
// console.log('FullScreenAnimationController animateTransition:', animationType);
const owner = this.owner.deref();
if (owner) {
// console.log('owner.id:', owner.id);
const state = SharedTransition.getState(owner.id);
if (!state) {
return;
}
// console.log('state.activeType:', state.activeType)
switch (state.activeType) {
case SharedTransitionAnimationType.present:
console.log('-- Transition present --');
transitionContext.containerView.addSubview(owner.presented.view);
owner.presented.view.layoutIfNeeded();
// 1. Presented view: gather all sharedTransitionTag views
const presentedSharedElements = <Array<View>>querySelectorAll(state.toPage, 'sharedTransitionTag');
// console.log('presented sharedTransitionTag total:', presentedSharedElements.length);
// 2. Presenting view: gather all sharedTransitionTag views
const presentingSharedElements = <Array<View>>querySelectorAll(state.page, 'sharedTransitionTag');
// console.log('presenting sharedTransitionTag total:', presentingSharedElements.length);
// 3. only handle sharedTransitionTag on presenting which match presented
const presentedTags = presentedSharedElements.map((v) => v.sharedTransitionTag);
const presentingViewsToHandle = presentingSharedElements.filter((v) => presentedTags.includes(v.sharedTransitionTag));
console.log(' ');
console.log(
`1. Found sharedTransitionTags to animate:`,
presentingViewsToHandle.map((v) => v.sharedTransitionTag)
);
console.log(`2. Take snapshots of shared elements and position them based on presenting view:`);
for (const presentingView of presentingViewsToHandle) {
if (!owner.sharedElements) {
owner.sharedElements = {
presented: [],
presenting: [],
};
}
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 sharedElementSnapshot = UIImageView.alloc().initWithImage(snapshotView(presentingSharedElement));
sharedElementSnapshot.clipsToBounds = true;
// console.log('---> snapshot: ', sharedElementSnapshot);
const presentedView = presentedSharedElements.find((v) => v.sharedTransitionTag === presentingView.sharedTransitionTag);
const presentedSharedElement = presentedView.ios;
const startFrame = presentingSharedElement.convertRectToView(presentingSharedElement.bounds, transitionContext.containerView);
const endFrame = presentedSharedElement.convertRectToView(presentedSharedElement.bounds, transitionContext.containerView);
sharedElementSnapshot.frame = startFrame;
console.log('---> ', presentingView.sharedTransitionTag, ' frame:', printRect(sharedElementSnapshot.frame));
owner.sharedElements.presenting.push({
view: presentingView,
startFrame,
endFrame,
snapshot: sharedElementSnapshot,
});
owner.sharedElements.presented.push({
view: presentedView,
startFrame: endFrame,
endFrame: startFrame,
});
// hide both while animating within the transition context
presentingView.opacity = 0;
presentedView.opacity = 0;
// add snapshot to animate
transitionContext.containerView.addSubview(sharedElementSnapshot);
}
const cleanupPresent = () => {
for (const presented of owner.sharedElements.presented) {
presented.view.opacity = 1;
}
// TODO: Discuss with Igor whether this is necessary
// potential this could help smooth some shared element transitions
UIView.animateWithDurationAnimationsCompletion(
0.3,
() => {
for (const presenting of owner.sharedElements.presenting) {
presenting.snapshot.alpha = 0;
}
},
() => {
for (const presenting of owner.sharedElements.presenting) {
presenting.snapshot.removeFromSuperview();
}
SharedTransition.updateState({
id: owner.id,
activeType: SharedTransitionAnimationType.dismiss,
});
transitionContext.completeTransition(true);
}
);
};
const updateFramePresent = () => {
// https://stackoverflow.com/a/27997678/1418981
// In order to have proper layout. Seems mostly needed when presenting.
// For instance during presentation, destination view doesn't account navigation bar height.
// Not sure if best to leave all the time?
// owner.presented.view.setNeedsLayout();
// owner.presented.view.layoutIfNeeded();
console.log('3. Animating shared elements:');
for (const presented of owner.sharedElements.presented) {
const presentingMatch = owner.sharedElements.presenting.find((v) => v.view.sharedTransitionTag === presented.view.sharedTransitionTag);
// Workaround wrong origin due ongoing layout process.
const updatedEndFrame = presented.view.ios.convertRectToView(presented.view.ios.bounds, transitionContext.containerView);
const correctedEndFrame = CGRectMake(updatedEndFrame.origin.x, updatedEndFrame.origin.y, presentingMatch.endFrame.size.width, presentingMatch.endFrame.size.height);
presentingMatch.snapshot.frame = correctedEndFrame;
console.log(`---> ${presentingMatch.view.sharedTransitionTag} animate to: `, printRect(correctedEndFrame));
}
console.log(' ');
};
// starting page properties
owner.presented.view.alpha = typeof state.incomingViewStart?.opacity === 'number' ? state.incomingViewStart?.opacity : 0;
const startX = typeof state.incomingViewStart?.x === 'number' ? state.incomingViewStart?.x : 0;
const startY = typeof state.incomingViewStart?.y === 'number' ? state.incomingViewStart?.y : Screen.mainScreen.heightDIPs;
const startWidth = typeof state.incomingViewStart?.width === 'number' ? state.incomingViewStart?.width : Screen.mainScreen.widthDIPs;
const startHeight = typeof state.incomingViewStart?.height === 'number' ? state.incomingViewStart?.height : Screen.mainScreen.heightDIPs;
owner.presented.view.frame = CGRectMake(startX, startY, startWidth, startHeight);
UIView.animateWithDurationDelayUsingSpringWithDampingInitialSpringVelocityOptionsAnimationsCompletion(
typeof state.incomingViewStart?.duration === 'number' ? state.incomingViewStart?.duration / 1000 : 0.35,
0,
0.5,
3,
UIViewAnimationOptions.CurveEaseInOut,
() => {
// animate page properties to the following:
owner.presented.view.alpha = typeof state.incomingViewEnd?.opacity === 'number' ? state.incomingViewEnd?.opacity : 1;
const endX = typeof state.incomingViewEnd?.x === 'number' ? state.incomingViewEnd?.x : 0;
const endY = typeof state.incomingViewEnd?.y === 'number' ? state.incomingViewEnd?.y : 0;
const endWidth = typeof state.incomingViewEnd?.width === 'number' ? state.incomingViewEnd?.width : Screen.mainScreen.widthDIPs;
const endHeight = typeof state.incomingViewEnd?.height === 'number' ? state.incomingViewEnd?.height : Screen.mainScreen.heightDIPs;
owner.presented.view.frame = CGRectMake(endX, endY, endWidth, endHeight);
updateFramePresent();
},
() => {
cleanupPresent();
}
);
break;
case SharedTransitionAnimationType.dismiss:
console.log('-- Transition dismiss --');
// console.log('transitionContext.containerView.subviews.count:', transitionContext.containerView.subviews.count);
console.log(' ');
console.log(
`1. Dismiss sharedTransitionTags to animate:`,
owner.sharedElements.presented.map((p) => p.view.sharedTransitionTag)
);
console.log(`2. Add back previously stored sharedElements to dismiss:`);
for (const p of owner.sharedElements.presented) {
p.view.opacity = 0;
}
for (const p of owner.sharedElements.presenting) {
p.snapshot.alpha = 1;
transitionContext.containerView.addSubview(p.snapshot);
}
const cleanupDismiss = () => {
for (const presenting of owner.sharedElements.presenting) {
presenting.view.opacity = 1;
}
SharedTransition.finishState(owner.id);
transitionContext.completeTransition(true);
};
const updateFrameDismiss = () => {
console.log('3. Dismissing shared elements:');
for (const presenting of owner.sharedElements.presenting) {
presenting.snapshot.frame = presenting.startFrame;
console.log(`---> ${presenting.view.sharedTransitionTag} animate to: `, printRect(presenting.startFrame));
}
console.log(' ');
};
UIView.animateWithDurationDelayUsingSpringWithDampingInitialSpringVelocityOptionsAnimationsCompletion(
typeof state.dismissViewEnd?.duration === 'number' ? state.dismissViewEnd?.duration / 1000 : 0.35,
0,
0.5,
3,
UIViewAnimationOptions.CurveEaseInOut,
() => {
owner.presented.view.alpha = typeof state.dismissViewEnd?.opacity === 'number' ? state.dismissViewEnd?.opacity : 0;
const endX = typeof state.dismissViewEnd?.x === 'number' ? state.dismissViewEnd?.x : 0;
const endY = typeof state.dismissViewEnd?.y === 'number' ? state.dismissViewEnd?.y : Screen.mainScreen.heightDIPs;
const endWidth = typeof state.dismissViewEnd?.width === 'number' ? state.dismissViewEnd?.width : Screen.mainScreen.widthDIPs;
const endHeight = typeof state.dismissViewEnd?.height === 'number' ? state.dismissViewEnd?.height : Screen.mainScreen.heightDIPs;
owner.presented.view.frame = CGRectMake(endX, endY, endWidth, endHeight);
updateFrameDismiss();
},
() => {
cleanupDismiss();
}
);
break;
}
}
}
}
function snapshotView(view: UIView): UIImage {
// console.log('snapshotView view.frame:', printRect(view.frame));
UIGraphicsBeginImageContextWithOptions(CGSizeMake(view.frame.size.width, view.frame.size.height), false, Screen.mainScreen.scale);
view.layer.renderInContext(UIGraphicsGetCurrentContext());
const image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}

View File

@@ -0,0 +1,94 @@
import type { Transition } from '.';
// import type { View } from '../../core/view';
import type { Page } from '../page';
// always increment when adding new transitions to be able to track their state
export enum SharedTransitionAnimationType {
present,
dismiss,
}
export interface SharedTransitionConfig {
/**
* Page which will start the transition
*/
page?: Page;
/**
* Preconfigured transition or your own custom configured one
*/
instance?: Transition;
/**
* View settings to start your transition from.
*/
incomingViewStart?: SharedTransitionPageProperties;
/**
* View settings to animate your page to.
*/
incomingViewEnd?: SharedTransitionPageProperties;
/**
* View settings to animate your page to when dismissed.
*/
dismissViewEnd?: SharedTransitionPageProperties;
}
export interface SharedTransitionState extends SharedTransitionConfig {
id?: number;
activeType?: SharedTransitionAnimationType;
toPage?: Page;
}
type SharedTransitionPageProperties = {
x?: number;
y?: number;
width?: number;
height?: number;
opacity?: number;
/**
* Duration in milliseconds
*/
duration?: number;
};
export class SharedTransition {
static configure(options: SharedTransitionConfig): Transition {
SharedTransition.updateState({
...options,
id: options.instance.id,
activeType: SharedTransitionAnimationType.present,
});
return options.instance;
}
static currentStack: Array<SharedTransitionState>;
static updateState(state: SharedTransitionState) {
if (!SharedTransition.currentStack) {
SharedTransition.currentStack = [];
}
const existingTransition = SharedTransition.getState(state.id);
if (existingTransition) {
// updating existing
for (const key in state) {
existingTransition[key] = state[key];
// console.log(' ... updating state: ', key, state[key])
}
} else {
SharedTransition.currentStack.push(state);
}
}
static addPageToTop(page: Page) {
if (SharedTransition.currentStack?.length) {
const top = SharedTransition.currentStack.slice(-1)[0];
if (top) {
SharedTransition.updateState({
id: top.id,
toPage: page,
});
}
}
}
static getState(id: number) {
return SharedTransition.currentStack.find((t) => t.id === id);
}
static finishState(id: number) {
const index = SharedTransition.currentStack.findIndex((t) => t.id === id);
if (index > -1) {
SharedTransition.currentStack.splice(index, 1);
}
}
}