feat: ios page push/pop shared element transitions

This commit is contained in:
Nathan Walker
2023-03-08 21:08:16 -08:00
parent 653f58b1ad
commit 8b654a0b3b
15 changed files with 548 additions and 83 deletions

View File

@@ -1,5 +1,4 @@
import { Observable, EventData, Page, ShowModalOptions, SharedTransition, ModalTransition, TransitionType, Screen } from '@nativescript/core';
// import { DetailTransition } from './transitions/detail';
import { Observable, EventData, Page, ShowModalOptions, SharedTransition, ModalTransition, PageTransition, Screen } from '@nativescript/core';
let page: Page;
export function navigatingTo(args: EventData) {
@@ -12,13 +11,22 @@ export function navigatingTo(args: EventData) {
// }
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,
// },
transition: {
instance: SharedTransition.configure({
page,
instance: new PageTransition(),
incomingViewStart: {
y: 200,
duration: 1000,
},
dismissViewEnd: {
y: 100,
duration: 500,
},
}),
},
});
}

View File

@@ -8,7 +8,7 @@
<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" />
<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" />

View File

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

View File

@@ -13,7 +13,7 @@ import { profile } from '../../../profiling';
import { accessibilityEnabledProperty, accessibilityHiddenProperty, accessibilityHintProperty, accessibilityIdentifierProperty, accessibilityLabelProperty, accessibilityLanguageProperty, accessibilityLiveRegionProperty, accessibilityMediaSessionProperty, accessibilityRoleProperty, accessibilityStateProperty, accessibilityValueProperty, accessibilityIgnoresInvertColorsProperty } from '../../../accessibility/accessibility-properties';
import { IOSPostAccessibilityNotificationType, isAccessibilityServiceEnabled, updateAccessibilityProperties, AccessibilityEventOptions, AccessibilityRole, AccessibilityState } from '../../../accessibility';
import { CoreTypes } from '../../../core-types';
import { CustomTransitionModal } from '../../transition';
import type { ModalTransition } from '../../transition/modal-transition';
export * from './view-common';
// helpers (these are okay re-exported here)
@@ -922,10 +922,10 @@ View.prototype._nativeBackgroundState = 'unset';
@NativeClass
class UIViewControllerTransitioningDelegateImpl extends NSObject implements UIViewControllerTransitioningDelegate {
owner: WeakRef<CustomTransitionModal>;
owner: WeakRef<ModalTransition>;
static ObjCProtocols = [UIViewControllerTransitioningDelegate];
static initWithOwner(owner: WeakRef<CustomTransitionModal>) {
static initWithOwner(owner: WeakRef<ModalTransition>) {
const delegate = <UIViewControllerTransitioningDelegateImpl>UIViewControllerTransitioningDelegateImpl.new();
delegate.owner = owner;
return delegate;

View File

@@ -402,11 +402,23 @@ class UINavigationControllerAnimatedDelegate extends NSObject implements UINavig
Trace.write(`UINavigationControllerImpl.navigationControllerAnimationControllerForOperationFromViewControllerToViewController(${operation}, ${fromVC}, ${toVC}), transition: ${JSON.stringify(navigationTransition)}`, Trace.categories.NativeLifecycle);
}
// Shared element transitions
if (operation === UINavigationControllerOperation.Push && navigationTransition.instance?.iosPresentedController) {
return navigationTransition.instance?.iosPresentedController(toVC, fromVC, navigationController);
} else if (operation === UINavigationControllerOperation.Pop && navigationTransition.instance?.iosDismissedController) {
return navigationTransition.instance?.iosDismissedController(toVC);
}
const curve = _getNativeCurve(navigationTransition);
const animationController = _createIOSAnimatedTransitioning(navigationTransition, curve, operation, fromVC, toVC);
return animationController;
}
// TODO: wire this up for interactive gesture control of transition
// navigationControllerInteractionControllerForAnimationController(navigationController: UINavigationController, animationController: UIViewControllerAnimatedTransitioning): UIViewControllerInteractiveTransitioning {
// }
}
@NativeClass

View File

@@ -76,7 +76,7 @@ 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 { 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

@@ -2,8 +2,6 @@
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;
@@ -45,3 +43,11 @@ export class Transition implements TransitionType {
return `Transition@${this.id}`;
}
}
export function iosSnapshotView(view: any) {
return null;
}
export function iosMatchLayerProperties(view: any, toView: any) {}
export function iosPrintRect(r: any) {}

View File

@@ -1,6 +1,11 @@
export interface TransitionType {
export declare class Transition {
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;
iosDismissedController?(dismissed: any /* UIViewController */): any /* UIViewControllerAnimatedTransitioning */;
iosPresentedController?(presented: any /* UIViewController */, presenting: any /* UIViewController */, source: any /* UIViewController */): any /* UIViewControllerAnimatedTransitioning */;
@@ -12,13 +17,9 @@
iosPresentedViewController?(presented: any /* UIViewController */, presenting: any /* UIViewController */, source: any /* UIViewController */): any /* 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;
}
// TODO: move to native-helper.{ios|android}
export declare function iosSnapshotView(view: any /* UIView */): any; /* UIImage */
export declare function iosMatchLayerProperties(view: any /* UIView */, toView: any /* UIView */);
export declare function iosPrintRect(r: any /* CGRect */): void;

View File

@@ -1,6 +1,5 @@
import type { TransitionType } from '.';
export type { TransitionType } from '.';
import type { Transition as TransitionType } from '.';
import { Screen } from '../../platform';
let transitionId = 0;
export class Transition implements TransitionType {
@@ -36,3 +35,40 @@ export class Transition implements TransitionType {
return `Transition@${this.id}`;
}
}
export function iosSnapshotView(view: UIView): 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, Screen.mainScreen.scale);
view.layer.renderInContext(UIGraphicsGetCurrentContext());
const image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}
// todo: figure out all the properties/layer properties that we want to transition automatically
// note: some need to be done at various stages of the animation and are not in here. (e.g. alpha)
export function iosMatchLayerProperties(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 iosPrintRect(r: CGRect) {
return `CGRect(${r.origin.x} ${r.origin.y} ${r.size.width} ${r.size.height})`;
}

View File

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

View File

@@ -1,17 +1,10 @@
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';
import { Transition, iosMatchLayerProperties, iosPrintRect, iosSnapshotView } from '.';
import { SharedTransition, SharedTransitionAnimationType, DEFAULT_DURATION } 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})`;
const DEFAULT_DURATION = 0.35;
export class ModalTransition extends Transition implements TransitionType {
export class ModalTransition extends Transition {
transitionController: ModalTransitionController;
presented: UIViewController;
presenting: UIViewController;
@@ -65,7 +58,7 @@ class ModalTransitionController extends NSObject implements UIViewControllerAnim
}
animateTransition(transitionContext: UIViewControllerContextTransitioning): void {
// console.log('FullScreenAnimationController animateTransition:', animationType);
// console.log('ModalTransitionController animateTransition:', animationType);
const owner = this.owner.deref();
if (owner) {
// console.log('owner.id:', owner.id);
@@ -125,7 +118,7 @@ class ModalTransitionController extends NSObject implements UIViewControllerAnim
// in case the image is loaded async, we need to update the snapshot when it changes
// todo: remove listener on transition end
presentedView.on('imageSourceChange', () => {
sharedElementSnapshot.image = snapshotView(presentedSharedElement);
sharedElementSnapshot.image = iosSnapshotView(presentedSharedElement);
sharedElementSnapshot.tintColor = presentedSharedElement.tintColor;
});
@@ -133,14 +126,14 @@ class ModalTransitionController extends NSObject implements UIViewControllerAnim
sharedElementSnapshot.contentMode = presentedSharedElement.contentMode;
}
matchLayerProperties(sharedElementSnapshot, presentingSharedElement);
iosMatchLayerProperties(sharedElementSnapshot, presentingSharedElement);
sharedElementSnapshot.clipsToBounds = true;
// console.log('---> snapshot: ', sharedElementSnapshot);
const startFrame = presentingSharedElement.convertRectToView(presentingSharedElement.bounds, transitionContext.containerView);
const endFrame = presentedSharedElement.convertRectToView(presentedSharedElement.bounds, transitionContext.containerView);
sharedElementSnapshot.frame = startFrame;
console.log('---> ', presentingView.sharedTransitionTag, ' frame:', printRect(sharedElementSnapshot.frame));
console.log('---> ', presentingView.sharedTransitionTag, ' frame:', iosPrintRect(sharedElementSnapshot.frame));
owner.sharedElements.presenting.push({
view: presentingView,
@@ -209,13 +202,13 @@ class ModalTransitionController extends NSObject implements UIViewControllerAnim
presentingMatch.snapshot.frame = correctedEndFrame;
// apply view and layer properties to the snapshot view to match the source/presented view
matchLayerProperties(presentingMatch.snapshot, presented.view.ios);
iosMatchLayerProperties(presentingMatch.snapshot, presented.view.ios);
// create a snapshot of the presented view
presentingMatch.snapshot.image = snapshotView(presented.view.ios);
presentingMatch.snapshot.image = iosSnapshotView(presented.view.ios);
// apply correct alpha
presentingMatch.snapshot.alpha = presentingMatch.endOpacity;
console.log(`---> ${presentingMatch.view.sharedTransitionTag} animate to: `, printRect(correctedEndFrame));
console.log(`---> ${presentingMatch.view.sharedTransitionTag} animate to: `, iosPrintRect(correctedEndFrame));
}
console.log(' ');
};
@@ -284,11 +277,11 @@ class ModalTransitionController extends NSObject implements UIViewControllerAnim
const updateFrameDismiss = () => {
console.log('3. Dismissing shared elements:');
for (const presenting of owner.sharedElements.presenting) {
matchLayerProperties(presenting.snapshot, presenting.view.ios);
iosMatchLayerProperties(presenting.snapshot, presenting.view.ios);
presenting.snapshot.frame = presenting.startFrame;
presenting.snapshot.alpha = presenting.startOpacity;
console.log(`---> ${presenting.view.sharedTransitionTag} animate to: `, printRect(presenting.startFrame));
console.log(`---> ${presenting.view.sharedTransitionTag} animate to: `, iosPrintRect(presenting.startFrame));
}
console.log(' ');
};
@@ -320,36 +313,3 @@ class ModalTransitionController extends NSObject implements UIViewControllerAnim
}
}
}
function snapshotView(view: UIView): 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, Screen.mainScreen.scale);
view.layer.renderInContext(UIGraphicsGetCurrentContext());
const image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}
// todo: figure out all the properties/layer properties that we want to transition automatically
// note: some need to be done at various stages of the animation and are not in here. (e.g. alpha)
function matchLayerProperties(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];
}
});
}

View File

@@ -0,0 +1,124 @@
import { Transition } from '.';
import { Screen } from '../../platform';
import lazy from '../../utils/lazy';
const screenWidth = lazy(() => Screen.mainScreen.widthPixels);
const screenHeight = lazy(() => Screen.mainScreen.heightPixels);
export class PageTransition extends Transition {
private _direction: string;
constructor(direction: string, duration: number, curve: any) {
super(duration, curve);
this._direction = direction;
}
public createAndroidAnimator(transitionType: string): android.animation.Animator {
const translationValues = Array.create('float', 2);
switch (this._direction) {
case 'left':
switch (transitionType) {
case Transition.AndroidTransitionType.enter:
translationValues[0] = screenWidth();
translationValues[1] = 0;
break;
case Transition.AndroidTransitionType.exit:
translationValues[0] = 0;
translationValues[1] = -screenWidth();
break;
case Transition.AndroidTransitionType.popEnter:
translationValues[0] = -screenWidth();
translationValues[1] = 0;
break;
case Transition.AndroidTransitionType.popExit:
translationValues[0] = 0;
translationValues[1] = screenWidth();
break;
}
break;
case 'right':
switch (transitionType) {
case Transition.AndroidTransitionType.enter:
translationValues[0] = -screenWidth();
translationValues[1] = 0;
break;
case Transition.AndroidTransitionType.exit:
translationValues[0] = 0;
translationValues[1] = screenWidth();
break;
case Transition.AndroidTransitionType.popEnter:
translationValues[0] = screenWidth();
translationValues[1] = 0;
break;
case Transition.AndroidTransitionType.popExit:
translationValues[0] = 0;
translationValues[1] = -screenWidth();
break;
}
break;
case 'top':
switch (transitionType) {
case Transition.AndroidTransitionType.enter:
translationValues[0] = screenHeight();
translationValues[1] = 0;
break;
case Transition.AndroidTransitionType.exit:
translationValues[0] = 0;
translationValues[1] = -screenHeight();
break;
case Transition.AndroidTransitionType.popEnter:
translationValues[0] = -screenHeight();
translationValues[1] = 0;
break;
case Transition.AndroidTransitionType.popExit:
translationValues[0] = 0;
translationValues[1] = screenHeight();
break;
}
break;
case 'bottom':
switch (transitionType) {
case Transition.AndroidTransitionType.enter:
translationValues[0] = -screenHeight();
translationValues[1] = 0;
break;
case Transition.AndroidTransitionType.exit:
translationValues[0] = 0;
translationValues[1] = screenHeight();
break;
case Transition.AndroidTransitionType.popEnter:
translationValues[0] = screenHeight();
translationValues[1] = 0;
break;
case Transition.AndroidTransitionType.popExit:
translationValues[0] = 0;
translationValues[1] = -screenHeight();
break;
}
break;
}
let prop;
if (this._direction === 'left' || this._direction === 'right') {
prop = 'translationX';
} else {
prop = 'translationY';
}
const animator = android.animation.ObjectAnimator.ofFloat(null, prop, translationValues);
const duration = this.getDuration();
if (duration !== undefined) {
animator.setDuration(duration);
}
animator.setInterpolator(this.getCurve());
const animatorSet = new android.animation.AnimatorSet();
animatorSet.play(animator);
return animatorSet;
}
public toString(): string {
return `${super.toString()} ${this._direction}`;
}
}

View File

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

View File

@@ -0,0 +1,316 @@
import { querySelectorAll } from '../core/view-base';
import type { View } from '../core/view';
import { Transition, iosMatchLayerProperties, iosSnapshotView, iosPrintRect } from '.';
import { Screen } from '../../platform';
import { SharedTransition, SharedTransitionAnimationType, DEFAULT_DURATION } from './shared-transition';
export class PageTransition extends Transition {
transitionController: PageTransitionController;
presented: UIViewController;
presenting: UIViewController;
sharedElements: {
presented?: Array<{ view: View; startFrame?: CGRect; endFrame?: CGRect; snapshot?: UIImageView; startOpacity?: number; endOpacity?: number }>;
presenting?: Array<{ view: View; startFrame: CGRect; endFrame?: CGRect; snapshot?: UIImageView; startOpacity?: number; endOpacity?: number }>;
};
iosPresentedController(presented: UIViewController, presenting: UIViewController, source: UIViewController): UIViewControllerAnimatedTransitioning {
console.log('-- iosPresentedController --');
this.transitionController = PageTransitionController.initWithOwner(new WeakRef(this));
this.presented = presented;
return this.transitionController;
}
iosDismissedController(dismissed: UIViewController): UIViewControllerAnimatedTransitioning {
console.log('-- iosDismissedController --');
this.transitionController = PageTransitionController.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 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 {
return DEFAULT_DURATION;
}
animateTransition(transitionContext: UIViewControllerContextTransitioning): void {
console.log('PageTransitionController animateTransition');
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 presentedView = presentedSharedElements.find((v) => v.sharedTransitionTag === presentingView.sharedTransitionTag);
const presentedSharedElement = presentedView.ios;
const sharedElementSnapshot = UIImageView.alloc().init(); // WithImage(snapshotView(presentedSharedElement));
// treat images differently...
if (presentedSharedElement instanceof UIImageView) {
// in case the image is loaded async, we need to update the snapshot when it changes
// todo: remove listener on transition end
presentedView.on('imageSourceChange', () => {
sharedElementSnapshot.image = iosSnapshotView(presentedSharedElement);
sharedElementSnapshot.tintColor = presentedSharedElement.tintColor;
});
sharedElementSnapshot.tintColor = presentedSharedElement.tintColor;
sharedElementSnapshot.contentMode = presentedSharedElement.contentMode;
}
iosMatchLayerProperties(sharedElementSnapshot, presentingSharedElement);
sharedElementSnapshot.clipsToBounds = true;
// console.log('---> snapshot: ', sharedElementSnapshot);
const startFrame = presentingSharedElement.convertRectToView(presentingSharedElement.bounds, transitionContext.containerView);
const endFrame = presentedSharedElement.convertRectToView(presentedSharedElement.bounds, transitionContext.containerView);
sharedElementSnapshot.frame = startFrame;
console.log('---> ', presentingView.sharedTransitionTag, ' frame:', iosPrintRect(sharedElementSnapshot.frame));
owner.sharedElements.presenting.push({
view: presentingView,
startFrame,
endFrame,
snapshot: sharedElementSnapshot,
startOpacity: presentingView.opacity,
endOpacity: presentedView.opacity,
});
owner.sharedElements.presented.push({
view: presentedView,
startFrame: endFrame,
endFrame: startFrame,
startOpacity: presentedView.opacity,
endOpacity: presentingView.opacity,
});
// set initial opacity to match the source view opacity
sharedElementSnapshot.alpha = presentingView.opacity;
// hide both while animating within the transition context
presentingView.opacity = 0;
presentedView.opacity = 0;
// add snapshot to animate
transitionContext.containerView.addSubview(sharedElementSnapshot);
}
const cleanupPresent = () => {
for (const presented of owner.sharedElements.presented) {
presented.view.opacity = presented.startOpacity;
}
// TODO: Discuss with Igor whether this is necessary
// potential this could help smooth some shared element transitions
UIView.animateWithDurationAnimationsCompletion(
0, // Igor: disabled for now, we'll talk about this and decide
() => {
for (const presenting of owner.sharedElements.presenting) {
presenting.snapshot.alpha = presenting.endOpacity;
}
},
() => {
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;
// apply view and layer properties to the snapshot view to match the source/presented view
iosMatchLayerProperties(presentingMatch.snapshot, presented.view.ios);
// create a snapshot of the presented view
presentingMatch.snapshot.image = iosSnapshotView(presented.view.ios);
// apply correct alpha
presentingMatch.snapshot.alpha = presentingMatch.endOpacity;
console.log(`---> ${presentingMatch.view.sharedTransitionTag} animate to: `, iosPrintRect(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 : DEFAULT_DURATION,
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 = p.endOpacity;
transitionContext.containerView.addSubview(p.snapshot);
}
const cleanupDismiss = () => {
for (const presenting of owner.sharedElements.presenting) {
presenting.view.opacity = presenting.startOpacity;
}
SharedTransition.finishState(owner.id);
transitionContext.completeTransition(true);
};
const updateFrameDismiss = () => {
console.log('3. Dismissing shared elements:');
for (const presenting of owner.sharedElements.presenting) {
iosMatchLayerProperties(presenting.snapshot, presenting.view.ios);
presenting.snapshot.frame = presenting.startFrame;
presenting.snapshot.alpha = presenting.startOpacity;
console.log(`---> ${presenting.view.sharedTransitionTag} animate to: `, iosPrintRect(presenting.startFrame));
}
console.log(' ');
};
UIView.animateWithDurationDelayUsingSpringWithDampingInitialSpringVelocityOptionsAnimationsCompletion(
typeof state.dismissViewEnd?.duration === 'number' ? state.dismissViewEnd?.duration / 1000 : DEFAULT_DURATION,
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;
}
}
}
}
}

View File

@@ -2,6 +2,7 @@ import type { Transition } from '.';
// import type { View } from '../../core/view';
import type { Page } from '../page';
export const DEFAULT_DURATION = 0.35;
// always increment when adding new transitions to be able to track their state
export enum SharedTransitionAnimationType {
present,