mirror of
https://github.com/NativeScript/NativeScript.git
synced 2025-11-05 13:26:48 +08:00
chore: cleanup
This commit is contained in:
@@ -1,2 +1,2 @@
|
||||
<Frame defaultPage="pages/transitions">
|
||||
<Frame defaultPage="main-page">
|
||||
</Frame>
|
||||
|
||||
@@ -80,5 +80,6 @@ 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 type { SharedTransitionConfig } from './transition/shared-transition';
|
||||
export { WebView } from './web-view';
|
||||
export type { LoadEventData, WebViewNavigationType } from './web-view';
|
||||
|
||||
21
packages/core/ui/transition/index.d.ts
vendored
21
packages/core/ui/transition/index.d.ts
vendored
@@ -1,7 +1,26 @@
|
||||
import type { BackstackEntry } from '../frame';
|
||||
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 SharedTransitionType = 'page' | 'modal';
|
||||
export interface SharedInteractiveState {
|
||||
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;
|
||||
|
||||
@@ -2,10 +2,10 @@ import type { View } from '../core/view';
|
||||
import { Screen } from '../../platform';
|
||||
import { iOSNativeHelper } from '../../utils/native-helper';
|
||||
import { isNumber } from '../../utils/types';
|
||||
import { Transition } from '.';
|
||||
import { SharedTransition, SharedTransitionAnimationType, DEFAULT_DURATION, DEFAULT_SPRING, getRectFromProps } from './shared-transition';
|
||||
import { Transition, SharedElementSettings, SharedInteractiveState } from '.';
|
||||
import { SharedTransition, SharedTransitionAnimationType, DEFAULT_DURATION, DEFAULT_SPRING, getRectFromProps, getPageStartDefaultsForType } from './shared-transition';
|
||||
import { SharedTransitionHelper } from './shared-transition-helper';
|
||||
import { PanGestureEventData, GestureStateTypes } from '../gestures';
|
||||
type SharedElementSettings = { view: View; startFrame: CGRect; endFrame?: CGRect; startOpacity?: number; endOpacity?: number; scale?: { x?: number; y?: number }; startTransform?: CGAffineTransform; snapshot?: UIImageView };
|
||||
|
||||
export class ModalTransition extends Transition {
|
||||
transitionController: ModalTransitionController;
|
||||
@@ -59,10 +59,6 @@ export class ModalTransition extends Transition {
|
||||
private _interactiveDismissGestureHandler(args: PanGestureEventData) {
|
||||
if (args?.ios?.view) {
|
||||
const state = SharedTransition.getState(this.id);
|
||||
// SharedTransition.updateState(this.id, {
|
||||
// interactiveBegan: true,
|
||||
// interactiveCancelled: false
|
||||
// });
|
||||
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);
|
||||
@@ -107,9 +103,7 @@ export class ModalTransition extends Transition {
|
||||
class PercentInteractiveController extends UIPercentDrivenInteractiveTransition implements UIViewControllerInteractiveTransitioning {
|
||||
static ObjCProtocols = [UIViewControllerInteractiveTransitioning];
|
||||
owner: WeakRef<ModalTransition>;
|
||||
started = false;
|
||||
transitionContext: UIViewControllerContextTransitioning;
|
||||
backgroundAnimation: UIViewPropertyAnimator;
|
||||
interactiveState: SharedInteractiveState;
|
||||
|
||||
static initWithOwner(owner: WeakRef<ModalTransition>) {
|
||||
const ctrl = <PercentInteractiveController>PercentInteractiveController.new();
|
||||
@@ -119,93 +113,41 @@ class PercentInteractiveController extends UIPercentDrivenInteractiveTransition
|
||||
|
||||
startInteractiveTransition(transitionContext: UIViewControllerContextTransitioning) {
|
||||
// console.log('startInteractiveTransition');
|
||||
this.transitionContext = transitionContext;
|
||||
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) {
|
||||
if (!this.started) {
|
||||
this.started = true;
|
||||
for (const p of owner.sharedElements.presented) {
|
||||
p.view.opacity = 0;
|
||||
}
|
||||
for (const p of owner.sharedElements.presenting) {
|
||||
p.snapshot.alpha = p.endOpacity;
|
||||
this.transitionContext.containerView.addSubview(p.snapshot);
|
||||
}
|
||||
const state = SharedTransition.getState(owner.id);
|
||||
const toProps = state.pageStart;
|
||||
const startY = isNumber(toProps?.y) ? toProps?.y : Screen.mainScreen.heightDIPs;
|
||||
this.backgroundAnimation = UIViewPropertyAnimator.alloc().initWithDurationDampingRatioAnimations(1, 1, () => {
|
||||
for (const p of owner.sharedElements.presenting) {
|
||||
p.snapshot.frame = p.startFrame;
|
||||
iOSNativeHelper.copyLayerProperties(p.snapshot, p.view.ios);
|
||||
|
||||
p.snapshot.alpha = 1;
|
||||
}
|
||||
owner.presented.view.alpha = 0;
|
||||
owner.presented.view.frame = CGRectMake(0, startY, owner.presented.view.bounds.size.width, owner.presented.view.bounds.size.height);
|
||||
});
|
||||
}
|
||||
|
||||
this.backgroundAnimation.fractionComplete = percentComplete;
|
||||
const state = SharedTransition.getState(owner.id);
|
||||
SharedTransitionHelper.interactiveUpdate(state, this.interactiveState, 'modal', percentComplete);
|
||||
}
|
||||
}
|
||||
|
||||
cancelInteractiveTransition() {
|
||||
// console.log('cancelInteractiveTransition');
|
||||
const owner = this.owner?.deref();
|
||||
if (owner && this.started) {
|
||||
if (owner) {
|
||||
const state = SharedTransition.getState(owner.id);
|
||||
if (!state) {
|
||||
return;
|
||||
}
|
||||
if (this.backgroundAnimation) {
|
||||
this.backgroundAnimation.reversed = true;
|
||||
const duration = isNumber(state.pageStart?.duration) ? state.pageStart?.duration / 1000 : DEFAULT_DURATION;
|
||||
this.backgroundAnimation.continueAnimationWithTimingParametersDurationFactor(null, duration);
|
||||
setTimeout(() => {
|
||||
for (const p of owner.sharedElements.presented) {
|
||||
p.view.opacity = 1;
|
||||
}
|
||||
for (const p of owner.sharedElements.presenting) {
|
||||
p.snapshot.removeFromSuperview();
|
||||
}
|
||||
owner.presented.view.alpha = 1;
|
||||
this.backgroundAnimation = null;
|
||||
this.started = false;
|
||||
this.transitionContext.cancelInteractiveTransition();
|
||||
this.transitionContext.completeTransition(false);
|
||||
}, duration * 1000);
|
||||
}
|
||||
SharedTransitionHelper.interactiveCancel(state, this.interactiveState, 'modal');
|
||||
}
|
||||
}
|
||||
|
||||
finishInteractiveTransition() {
|
||||
// console.log('finishInteractiveTransition');
|
||||
const owner = this.owner?.deref();
|
||||
if (owner && this.started) {
|
||||
if (this.backgroundAnimation) {
|
||||
this.backgroundAnimation.reversed = false;
|
||||
const state = SharedTransition.getState(owner.id);
|
||||
if (!state) {
|
||||
SharedTransition.finishState(owner.id);
|
||||
this.transitionContext.completeTransition(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const duration = isNumber(state.pageReturn?.duration) ? state.pageReturn?.duration / 1000 : DEFAULT_DURATION;
|
||||
this.backgroundAnimation.continueAnimationWithTimingParametersDurationFactor(null, duration);
|
||||
setTimeout(() => {
|
||||
for (const presenting of owner.sharedElements.presenting) {
|
||||
presenting.view.opacity = presenting.startOpacity;
|
||||
}
|
||||
SharedTransition.finishState(owner.id);
|
||||
this.transitionContext.finishInteractiveTransition();
|
||||
this.transitionContext.completeTransition(true);
|
||||
}, duration * 1000);
|
||||
}
|
||||
if (owner) {
|
||||
const state = SharedTransition.getState(owner.id);
|
||||
SharedTransitionHelper.interactiveFinish(state, this.interactiveState, 'modal');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -234,396 +176,7 @@ class ModalTransitionController extends NSObject implements UIViewControllerAnim
|
||||
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();
|
||||
|
||||
const { sharedElements, presented, presenting } = SharedTransition.getSharedElements(state.page, state.toPage);
|
||||
|
||||
if (SharedTransition.DEBUG) {
|
||||
console.log(' ModalTransition: 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;
|
||||
owner.presented.view.alpha = isNumber(pageStart?.opacity) ? pageStart?.opacity : 0;
|
||||
|
||||
const startFrame = getRectFromProps(pageStart, {
|
||||
x: 0,
|
||||
y: Screen.mainScreen.heightDIPs,
|
||||
width: Screen.mainScreen.widthDIPs,
|
||||
height: Screen.mainScreen.heightDIPs,
|
||||
});
|
||||
|
||||
const pageEnd = state.pageEnd;
|
||||
const pageEndIndependentTags = Object.keys(pageEnd?.sharedTransitionTags || {});
|
||||
console.log('pageEndIndependentTags:', pageEndIndependentTags);
|
||||
|
||||
for (const presentingView of sharedElements) {
|
||||
if (!owner.sharedElements) {
|
||||
owner.sharedElements = {
|
||||
presented: [],
|
||||
presenting: [],
|
||||
independent: [],
|
||||
};
|
||||
}
|
||||
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));
|
||||
}
|
||||
|
||||
owner.sharedElements.presenting.push({
|
||||
view: presentingView,
|
||||
startFrame,
|
||||
endFrame,
|
||||
snapshot,
|
||||
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
|
||||
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[tag];
|
||||
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 = 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));
|
||||
owner.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
|
||||
owner.presented.view.frame = CGRectMake(startFrame.x, startFrame.y, startFrame.width, startFrame.height);
|
||||
|
||||
const cleanupPresent = () => {
|
||||
for (const presented of owner.sharedElements.presented) {
|
||||
presented.view.opacity = presented.startOpacity;
|
||||
}
|
||||
for (const presenting of owner.sharedElements.presenting) {
|
||||
presenting.snapshot.removeFromSuperview();
|
||||
}
|
||||
for (const independent of owner.sharedElements.independent) {
|
||||
independent.snapshot.removeFromSuperview();
|
||||
if (independent.isPresented) {
|
||||
independent.view.opacity = independent.startOpacity;
|
||||
}
|
||||
}
|
||||
SharedTransition.updateState(owner.id, {
|
||||
activeType: SharedTransitionAnimationType.dismiss,
|
||||
});
|
||||
transitionContext.completeTransition(true);
|
||||
};
|
||||
|
||||
const animateProperties = () => {
|
||||
if (SharedTransition.DEBUG) {
|
||||
console.log('3. Animating shared elements:');
|
||||
}
|
||||
owner.presented.view.alpha = isNumber(pageEnd?.opacity) ? pageEnd?.opacity : 1;
|
||||
|
||||
const endFrame = getRectFromProps(pageEnd);
|
||||
owner.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 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
|
||||
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 owner.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 {
|
||||
const spring = pageEnd?.spring;
|
||||
iOSNativeHelper.animateWithSpring({
|
||||
tension: isNumber(spring?.tension) ? spring?.tension : DEFAULT_SPRING.tension,
|
||||
friction: isNumber(spring?.friction) ? spring?.friction : DEFAULT_SPRING.friction,
|
||||
mass: isNumber(spring?.mass) ? spring?.mass : DEFAULT_SPRING.mass,
|
||||
velocity: isNumber(spring?.velocity) ? spring?.velocity : DEFAULT_SPRING.velocity,
|
||||
delay: isNumber(spring?.delay) ? spring?.delay : DEFAULT_SPRING.delay,
|
||||
animations: () => {
|
||||
animateProperties();
|
||||
},
|
||||
completion: () => {
|
||||
cleanupPresent();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case SharedTransitionAnimationType.dismiss: {
|
||||
// console.log('-- Transition dismiss --');
|
||||
|
||||
// console.log('transitionContext.containerView.subviews.count:', transitionContext.containerView.subviews.count);
|
||||
|
||||
if (SharedTransition.DEBUG) {
|
||||
console.log(' ModalTransition: Dismiss');
|
||||
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);
|
||||
}
|
||||
for (const independent of owner.sharedElements.independent) {
|
||||
independent.snapshot.alpha = independent.endOpacity;
|
||||
transitionContext.containerView.addSubview(independent.snapshot);
|
||||
}
|
||||
|
||||
const pageReturn = state.pageReturn;
|
||||
|
||||
const cleanupDismiss = () => {
|
||||
for (const presenting of owner.sharedElements.presenting) {
|
||||
presenting.view.opacity = presenting.startOpacity;
|
||||
}
|
||||
for (const independent of owner.sharedElements.independent) {
|
||||
independent.view.opacity = independent.startOpacity;
|
||||
}
|
||||
SharedTransition.finishState(owner.id);
|
||||
owner.sharedElements = null;
|
||||
transitionContext.completeTransition(true);
|
||||
};
|
||||
|
||||
const animateProperties = () => {
|
||||
if (SharedTransition.DEBUG) {
|
||||
console.log('3. Dismissing shared elements:');
|
||||
}
|
||||
|
||||
owner.presented.view.alpha = isNumber(pageReturn?.opacity) ? pageReturn?.opacity : 0;
|
||||
|
||||
const endFrame = getRectFromProps(pageReturn, {
|
||||
x: 0,
|
||||
y: Screen.mainScreen.heightDIPs,
|
||||
width: Screen.mainScreen.widthDIPs,
|
||||
height: Screen.mainScreen.heightDIPs,
|
||||
});
|
||||
owner.presented.view.frame = CGRectMake(endFrame.x, endFrame.y, endFrame.width, endFrame.height);
|
||||
|
||||
for (const presenting of owner.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 owner.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 {
|
||||
const spring = pageReturn?.spring;
|
||||
iOSNativeHelper.animateWithSpring({
|
||||
tension: isNumber(spring?.tension) ? spring?.tension : DEFAULT_SPRING.tension,
|
||||
friction: isNumber(spring?.friction) ? spring?.friction : DEFAULT_SPRING.friction,
|
||||
animations: () => {
|
||||
animateProperties();
|
||||
},
|
||||
completion: () => {
|
||||
cleanupDismiss();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
SharedTransitionHelper.animate(state, transitionContext, 'modal');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import type { View } from '../core/view';
|
||||
import { Transition } from '.';
|
||||
import { SharedElementSettings, SharedInteractiveState, Transition } from '.';
|
||||
import { Screen } from '../../platform';
|
||||
import { iOSNativeHelper } from '../../utils/native-helper';
|
||||
import { isNumber } from '../../utils/types';
|
||||
import { PanGestureEventData, GestureStateTypes } from '../gestures';
|
||||
import { SharedTransition, SharedTransitionAnimationType, DEFAULT_DURATION, DEFAULT_SPRING } from './shared-transition';
|
||||
import { SharedTransition, SharedTransitionAnimationType, DEFAULT_DURATION, DEFAULT_SPRING, getRectFromProps, getPageStartDefaultsForType } from './shared-transition';
|
||||
import { SharedTransitionHelper } from './shared-transition-helper';
|
||||
|
||||
export class PageTransition extends Transition {
|
||||
transitionController: PageTransitionController;
|
||||
@@ -14,8 +15,10 @@ export class PageTransition extends Transition {
|
||||
navigationController: UINavigationController;
|
||||
operation: number;
|
||||
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 }>;
|
||||
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;
|
||||
@@ -52,7 +55,9 @@ export class PageTransition extends Transition {
|
||||
|
||||
this._interactiveGestureTeardown = () => {
|
||||
// console.log(`-- TEARDOWN setupInteractiveGesture --`);
|
||||
view.off('pan', this._interactiveDismissGesture);
|
||||
if (view) {
|
||||
view.off('pan', this._interactiveDismissGesture);
|
||||
}
|
||||
this._interactiveDismissGesture = null;
|
||||
};
|
||||
return this._interactiveGestureTeardown;
|
||||
@@ -62,6 +67,14 @@ export class PageTransition extends Transition {
|
||||
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) {
|
||||
@@ -111,9 +124,7 @@ export class PageTransition extends Transition {
|
||||
class PercentInteractiveController extends UIPercentDrivenInteractiveTransition implements UIViewControllerInteractiveTransitioning {
|
||||
static ObjCProtocols = [UIViewControllerInteractiveTransitioning];
|
||||
owner: WeakRef<PageTransition>;
|
||||
started = false;
|
||||
transitionContext: UIViewControllerContextTransitioning;
|
||||
backgroundAnimation: UIViewPropertyAnimator;
|
||||
interactiveState: SharedInteractiveState;
|
||||
|
||||
static initWithOwner(owner: WeakRef<PageTransition>) {
|
||||
const ctrl = <PercentInteractiveController>PercentInteractiveController.new();
|
||||
@@ -121,16 +132,16 @@ class PercentInteractiveController extends UIPercentDrivenInteractiveTransition
|
||||
return ctrl;
|
||||
}
|
||||
|
||||
added = false;
|
||||
startInteractiveTransition(transitionContext: UIViewControllerContextTransitioning) {
|
||||
// console.log('startInteractiveTransition');
|
||||
this.transitionContext = transitionContext;
|
||||
|
||||
if (!this.started) {
|
||||
this.started = true;
|
||||
const owner: any = this.owner?.deref();
|
||||
if (!this.interactiveState) {
|
||||
this.interactiveState = {
|
||||
transitionContext,
|
||||
};
|
||||
const owner = this.owner?.deref();
|
||||
if (owner) {
|
||||
this.transitionContext.containerView.insertSubviewBelowSubview(owner.presenting.view, owner.presented.view);
|
||||
const state = SharedTransition.getState(owner.id);
|
||||
SharedTransitionHelper.interactiveStart(state, this.interactiveState, 'page');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -138,94 +149,26 @@ class PercentInteractiveController extends UIPercentDrivenInteractiveTransition
|
||||
updateInteractiveTransition(percentComplete: number) {
|
||||
const owner = this.owner?.deref();
|
||||
if (owner) {
|
||||
if (!this.added) {
|
||||
this.added = true;
|
||||
for (const p of owner.sharedElements.presented) {
|
||||
p.view.opacity = 0;
|
||||
}
|
||||
for (const p of owner.sharedElements.presenting) {
|
||||
p.snapshot.alpha = p.endOpacity;
|
||||
this.transitionContext.containerView.addSubview(p.snapshot);
|
||||
}
|
||||
const state = SharedTransition.getState(owner.id);
|
||||
const props = state.pageReturn;
|
||||
this.backgroundAnimation = UIViewPropertyAnimator.alloc().initWithDurationDampingRatioAnimations(1, 1, () => {
|
||||
for (const p of owner.sharedElements.presenting) {
|
||||
p.snapshot.frame = p.startFrame;
|
||||
iOSNativeHelper.copyLayerProperties(p.snapshot, p.view.ios);
|
||||
|
||||
p.snapshot.alpha = 1;
|
||||
}
|
||||
owner.presented.view.alpha = isNumber(props?.opacity) ? props?.opacity : 0;
|
||||
const endX = isNumber(props?.x) ? props?.x : Screen.mainScreen.widthDIPs;
|
||||
const endY = isNumber(props?.y) ? props?.y : 0;
|
||||
const endWidth = isNumber(props?.width) ? props?.width : Screen.mainScreen.widthDIPs;
|
||||
const endHeight = isNumber(props?.height) ? props?.height : Screen.mainScreen.heightDIPs;
|
||||
owner.presented.view.frame = CGRectMake(endX, endY, endWidth, endHeight);
|
||||
});
|
||||
}
|
||||
|
||||
this.backgroundAnimation.fractionComplete = percentComplete;
|
||||
const state = SharedTransition.getState(owner.id);
|
||||
SharedTransitionHelper.interactiveUpdate(state, this.interactiveState, 'page', percentComplete);
|
||||
}
|
||||
}
|
||||
|
||||
cancelInteractiveTransition() {
|
||||
// console.log('cancelInteractiveTransition');
|
||||
const owner = this.owner?.deref();
|
||||
if (owner && this.added) {
|
||||
if (owner) {
|
||||
const state = SharedTransition.getState(owner.id);
|
||||
if (!state) {
|
||||
return;
|
||||
}
|
||||
if (this.backgroundAnimation) {
|
||||
this.backgroundAnimation.reversed = true;
|
||||
const duration = isNumber(state.pageStart?.duration) ? state.pageStart?.duration / 1000 : 0.35;
|
||||
this.backgroundAnimation.continueAnimationWithTimingParametersDurationFactor(null, duration);
|
||||
setTimeout(() => {
|
||||
for (const p of owner.sharedElements.presented) {
|
||||
p.view.opacity = 1;
|
||||
}
|
||||
for (const p of owner.sharedElements.presenting) {
|
||||
p.snapshot.removeFromSuperview();
|
||||
}
|
||||
owner.presented.view.alpha = 1;
|
||||
this.backgroundAnimation = null;
|
||||
this.added = false;
|
||||
this.transitionContext.cancelInteractiveTransition();
|
||||
this.transitionContext.completeTransition(false);
|
||||
}, duration * 1000);
|
||||
}
|
||||
SharedTransitionHelper.interactiveCancel(state, this.interactiveState, 'page');
|
||||
}
|
||||
}
|
||||
|
||||
finishInteractiveTransition() {
|
||||
// console.log('finishInteractiveTransition');
|
||||
const owner = this.owner?.deref();
|
||||
if (owner && this.added) {
|
||||
if (this.backgroundAnimation) {
|
||||
this.backgroundAnimation.reversed = false;
|
||||
const state = SharedTransition.getState(owner.id);
|
||||
if (!state) {
|
||||
SharedTransition.finishState(owner.id);
|
||||
this.transitionContext.completeTransition(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const duration = isNumber(state.pageReturn?.duration) ? state.pageReturn?.duration / 1000 : 0.35;
|
||||
this.backgroundAnimation.continueAnimationWithTimingParametersDurationFactor(null, duration);
|
||||
setTimeout(() => {
|
||||
for (const presenting of owner.sharedElements.presenting) {
|
||||
presenting.view.opacity = presenting.startOpacity;
|
||||
presenting.snapshot.removeFromSuperview();
|
||||
}
|
||||
|
||||
SharedTransition.finishState(owner.id);
|
||||
this.backgroundAnimation = null;
|
||||
this.added = false;
|
||||
this.transitionContext.finishInteractiveTransition();
|
||||
this.transitionContext.completeTransition(true);
|
||||
}, duration * 1000);
|
||||
}
|
||||
if (owner) {
|
||||
const state = SharedTransition.getState(owner.id);
|
||||
SharedTransitionHelper.interactiveFinish(state, this.interactiveState, 'page');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -253,287 +196,11 @@ class PageTransitionController extends NSObject implements UIViewControllerAnima
|
||||
const owner = this.owner.deref();
|
||||
if (owner) {
|
||||
// console.log('--- PageTransitionController animateTransition');
|
||||
// console.log('owner.presented:', owner.presented);
|
||||
// console.log('owner.presenting:', owner.presenting);
|
||||
|
||||
// console.log('owner.id:', owner.id);
|
||||
const state = SharedTransition.getState(owner.id);
|
||||
if (!state) {
|
||||
return;
|
||||
}
|
||||
// console.log('state.activeType:', state.activeType);
|
||||
// console.log('operation:', owner.operation);
|
||||
// switch (state.activeType) {
|
||||
switch (owner.operation) {
|
||||
case UINavigationControllerOperation.Push: {
|
||||
// console.log('pushing!!');
|
||||
// this.presented = toViewCtrl;
|
||||
// this.presenting = fromViewCtrl;
|
||||
|
||||
transitionContext.containerView.insertSubviewAboveSubview(owner.presented.view, owner.presenting.view);
|
||||
owner.presented.view.layoutIfNeeded();
|
||||
|
||||
const { sharedElements, presented } = SharedTransition.getSharedElements(state.page, state.toPage);
|
||||
|
||||
if (SharedTransition.DEBUG) {
|
||||
console.log(' PageTransition: Push');
|
||||
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:`);
|
||||
}
|
||||
|
||||
for (const presentingView of sharedElements) {
|
||||
if (!owner.sharedElements) {
|
||||
owner.sharedElements = {
|
||||
presented: [],
|
||||
presenting: [],
|
||||
};
|
||||
}
|
||||
const presentingSharedElement = presentingView.ios;
|
||||
|
||||
// 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 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 = iOSNativeHelper.snapshotView(presentedSharedElement, Screen.mainScreen.scale);
|
||||
sharedElementSnapshot.tintColor = presentedSharedElement.tintColor;
|
||||
});
|
||||
|
||||
sharedElementSnapshot.tintColor = presentedSharedElement.tintColor;
|
||||
sharedElementSnapshot.contentMode = presentedSharedElement.contentMode;
|
||||
}
|
||||
|
||||
iOSNativeHelper.copyLayerProperties(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;
|
||||
if (SharedTransition.DEBUG) {
|
||||
console.log('---> ', presentingView.sharedTransitionTag, ' frame:', iOSNativeHelper.printCGRect(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;
|
||||
}
|
||||
for (const presenting of owner.sharedElements.presenting) {
|
||||
presenting.snapshot.removeFromSuperview();
|
||||
}
|
||||
SharedTransition.updateState(owner.id, {
|
||||
activeType: SharedTransitionAnimationType.dismiss,
|
||||
});
|
||||
owner.presenting.view.removeFromSuperview();
|
||||
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();
|
||||
if (SharedTransition.DEBUG) {
|
||||
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
|
||||
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));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// starting page properties
|
||||
const toProps = state.pageStart;
|
||||
owner.presented.view.alpha = isNumber(toProps?.opacity) ? toProps?.opacity : 0;
|
||||
|
||||
const startX = isNumber(toProps?.x) ? toProps?.x : Screen.mainScreen.widthDIPs;
|
||||
const startY = isNumber(toProps?.y) ? toProps?.y : 0;
|
||||
const startWidth = isNumber(toProps?.width) ? toProps?.width : Screen.mainScreen.widthDIPs;
|
||||
const startHeight = isNumber(toProps?.height) ? toProps?.height : Screen.mainScreen.heightDIPs;
|
||||
owner.presented.view.frame = CGRectMake(startX, startY, startWidth, startHeight);
|
||||
|
||||
const animateProperties = () => {
|
||||
const props = state.pageEnd;
|
||||
// animate page properties to the following:
|
||||
owner.presented.view.alpha = isNumber(props?.opacity) ? props?.opacity : 1;
|
||||
|
||||
const endX = isNumber(props?.x) ? props?.x : 0;
|
||||
const endY = isNumber(props?.y) ? props?.y : 0;
|
||||
const endWidth = isNumber(props?.width) ? props?.width : Screen.mainScreen.widthDIPs;
|
||||
const endHeight = isNumber(props?.height) ? props?.height : Screen.mainScreen.heightDIPs;
|
||||
owner.presented.view.frame = CGRectMake(endX, endY, endWidth, endHeight);
|
||||
};
|
||||
if (isNumber(toProps?.duration)) {
|
||||
// override spring and use only linear animation
|
||||
UIView.animateWithDurationAnimationsCompletion(
|
||||
toProps?.duration / 1000,
|
||||
() => {
|
||||
animateProperties();
|
||||
updateFramePresent();
|
||||
},
|
||||
() => {
|
||||
cleanupPresent();
|
||||
}
|
||||
);
|
||||
} else {
|
||||
const spring = toProps?.spring;
|
||||
iOSNativeHelper.animateWithSpring({
|
||||
tension: isNumber(spring?.tension) ? spring?.tension : DEFAULT_SPRING.tension,
|
||||
friction: isNumber(spring?.friction) ? spring?.friction : DEFAULT_SPRING.friction,
|
||||
animations: () => {
|
||||
animateProperties();
|
||||
|
||||
updateFramePresent();
|
||||
},
|
||||
completion: () => {
|
||||
cleanupPresent();
|
||||
},
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case UINavigationControllerOperation.Pop: {
|
||||
// console.log('popping!');
|
||||
transitionContext.containerView.insertSubviewBelowSubview(owner.presenting.view, owner.presented.view);
|
||||
|
||||
// console.log('transitionContext.containerView.subviews.count:', transitionContext.containerView.subviews.count);
|
||||
|
||||
if (SharedTransition.DEBUG) {
|
||||
console.log(' PageTransition: Pop');
|
||||
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;
|
||||
presenting.snapshot.removeFromSuperview();
|
||||
}
|
||||
SharedTransition.finishState(owner.id);
|
||||
transitionContext.completeTransition(true);
|
||||
};
|
||||
const updateFrameDismiss = () => {
|
||||
if (SharedTransition.DEBUG) {
|
||||
console.log('3. Dismissing shared elements:');
|
||||
}
|
||||
for (const presenting of owner.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));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const props = state.pageReturn;
|
||||
|
||||
const animateProperties = () => {
|
||||
owner.presented.view.alpha = isNumber(props?.opacity) ? props?.opacity : 0;
|
||||
|
||||
const endX = isNumber(props?.x) ? props?.x : Screen.mainScreen.widthDIPs;
|
||||
const endY = isNumber(props?.y) ? props?.y : 0;
|
||||
const endWidth = isNumber(props?.width) ? props?.width : Screen.mainScreen.widthDIPs;
|
||||
const endHeight = isNumber(props?.height) ? props?.height : Screen.mainScreen.heightDIPs;
|
||||
owner.presented.view.frame = CGRectMake(endX, endY, endWidth, endHeight);
|
||||
};
|
||||
|
||||
if (isNumber(props?.duration)) {
|
||||
// override spring and use only linear animation
|
||||
UIView.animateWithDurationAnimationsCompletion(
|
||||
props?.duration / 1000,
|
||||
() => {
|
||||
animateProperties();
|
||||
updateFrameDismiss();
|
||||
},
|
||||
() => {
|
||||
cleanupDismiss();
|
||||
}
|
||||
);
|
||||
} else {
|
||||
const spring = props?.spring;
|
||||
iOSNativeHelper.animateWithSpring({
|
||||
tension: isNumber(spring?.tension) ? spring?.tension : DEFAULT_SPRING.tension,
|
||||
friction: isNumber(spring?.friction) ? spring?.friction : DEFAULT_SPRING.friction,
|
||||
animations: () => {
|
||||
animateProperties();
|
||||
|
||||
updateFrameDismiss();
|
||||
},
|
||||
completion: () => {
|
||||
cleanupDismiss();
|
||||
},
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
SharedTransitionHelper.animate(state, transitionContext, 'page');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { SharedTransitionState } from './shared-transition';
|
||||
|
||||
export class SharedTransitionHelper {
|
||||
static animate(state: SharedTransitionState, transitionContext: any, type: 'page' | 'modal') {
|
||||
// may be able to consolidate android handling here in future
|
||||
}
|
||||
}
|
||||
10
packages/core/ui/transition/shared-transition-helper.d.ts
vendored
Normal file
10
packages/core/ui/transition/shared-transition-helper.d.ts
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
import type { SharedTransitionType, SharedInteractiveState } from '.';
|
||||
import type { SharedTransitionState } from './shared-transition';
|
||||
|
||||
export declare class SharedTransitionHelper {
|
||||
static animate(state: SharedTransitionState, transitionContext: any /* iOS: UIViewControllerContextTransitioning */, type: SharedTransitionType): void;
|
||||
static interactiveStart(state: SharedTransitionState, interactiveState: SharedInteractiveState, type: SharedTransitionType): void;
|
||||
static interactiveUpdate(state: SharedTransitionState, interactiveState: SharedInteractiveState, type: SharedTransitionType, percent: number): void;
|
||||
static interactiveCancel(state: SharedTransitionState, interactiveState: SharedInteractiveState, type: SharedTransitionType): void;
|
||||
static interactiveFinish(state: SharedTransitionState, interactiveState: SharedInteractiveState, type: SharedTransitionType): void;
|
||||
}
|
||||
502
packages/core/ui/transition/shared-transition-helper.ios.ts
Normal file
502
packages/core/ui/transition/shared-transition-helper.ios.ts
Normal file
@@ -0,0 +1,502 @@
|
||||
import type { SharedInteractiveState, SharedTransitionType } from '.';
|
||||
import { getPageStartDefaultsForType, getRectFromProps, getSpringFromProps, SharedTransition, SharedTransitionAnimationType, SharedTransitionState } from './shared-transition';
|
||||
import { isNumber } from '../../utils/types';
|
||||
import { Screen } from '../../platform';
|
||||
import { iOSNativeHelper } from '../../utils/native-helper';
|
||||
interface PlatformSharedInteractiveState extends SharedInteractiveState {
|
||||
transitionContext?: UIViewControllerContextTransitioning;
|
||||
propertyAnimator?: UIViewPropertyAnimator;
|
||||
}
|
||||
export class SharedTransitionHelper {
|
||||
static animate(state: SharedTransitionState, transitionContext: UIViewControllerContextTransitioning, type: SharedTransitionType) {
|
||||
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({
|
||||
eventName: SharedTransition.startedEvent,
|
||||
});
|
||||
|
||||
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 (SharedTransition.DEBUG) {
|
||||
console.log(' ModalTransition: 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) {
|
||||
if (!transition.sharedElements) {
|
||||
transition.sharedElements = {
|
||||
presented: [],
|
||||
presenting: [],
|
||||
independent: [],
|
||||
};
|
||||
}
|
||||
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[tag];
|
||||
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({
|
||||
eventName: SharedTransition.finishedEvent,
|
||||
});
|
||||
};
|
||||
|
||||
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({
|
||||
eventName: SharedTransition.startedEvent,
|
||||
});
|
||||
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(' ModalTransition: 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({
|
||||
eventName: SharedTransition.finishedEvent,
|
||||
});
|
||||
};
|
||||
|
||||
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: SharedInteractiveState, type: SharedTransitionType) {
|
||||
SharedTransition.events().notify({
|
||||
eventName: SharedTransition.startedEvent,
|
||||
});
|
||||
switch (type) {
|
||||
case 'page':
|
||||
interactiveState.transitionContext.containerView.insertSubviewBelowSubview(state.instance.presenting.view, state.instance.presented.view);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static interactiveUpdate(state: SharedTransitionState, interactiveState: SharedInteractiveState, type: SharedTransitionType, 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;
|
||||
}
|
||||
|
||||
static interactiveCancel(state: SharedTransitionState, interactiveState: SharedInteractiveState, type: SharedTransitionType) {
|
||||
if (state.instance && interactiveState.added && interactiveState.propertyAnimator) {
|
||||
interactiveState.propertyAnimator.reversed = true;
|
||||
const duration = isNumber(state.pageStart?.duration) ? state.pageStart?.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({
|
||||
eventName: SharedTransition.cancelledEvent,
|
||||
});
|
||||
}, duration * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
static interactiveFinish(state: SharedTransitionState, interactiveState: SharedInteractiveState, type: SharedTransitionType) {
|
||||
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({
|
||||
eventName: SharedTransition.finishedEvent,
|
||||
});
|
||||
}, duration * 1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Transition } from '.';
|
||||
import { Observable } from '../../data/observable';
|
||||
import { Screen } from '../../platform';
|
||||
import { isNumber } from '../../utils/types';
|
||||
import { querySelectorAll, ViewBase } from '../core/view-base';
|
||||
@@ -34,10 +35,6 @@ export interface SharedTransitionInteractiveOptions {
|
||||
percentFormula?: (eventData: PanGestureEventData) => number;
|
||||
}
|
||||
export interface SharedTransitionConfig {
|
||||
/**
|
||||
* Page which will start the transition.
|
||||
*/
|
||||
page?: ViewBase;
|
||||
/**
|
||||
* Preconfigured transition or your own custom configured one.
|
||||
*/
|
||||
@@ -66,6 +63,10 @@ export interface SharedTransitionConfig {
|
||||
pageReturn?: SharedTransitionPageProperties;
|
||||
}
|
||||
export interface SharedTransitionState extends SharedTransitionConfig {
|
||||
/**
|
||||
* Page which will start the transition.
|
||||
*/
|
||||
page?: ViewBase;
|
||||
activeType?: SharedTransitionAnimationType;
|
||||
toPage?: ViewBase;
|
||||
// used internally for determining interactive gesture state of the transition
|
||||
@@ -74,6 +75,14 @@ export interface SharedTransitionState extends SharedTransitionConfig {
|
||||
}
|
||||
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 & {
|
||||
/**
|
||||
* Linear duration in milliseconds
|
||||
@@ -85,15 +94,9 @@ type SharedTransitionPageProperties = SharedProperties & {
|
||||
* Spring animation settings.
|
||||
* Defaults to 140 tension with 10 friction.
|
||||
*/
|
||||
spring?: {
|
||||
tension?: number;
|
||||
friction?: number;
|
||||
mass?: number;
|
||||
delay?: number;
|
||||
velocity?: number;
|
||||
animateOptions?: any /* ios only: UIViewAnimationOptions */;
|
||||
};
|
||||
spring?: SharedSpringProperties;
|
||||
};
|
||||
let sharedTransitionEvents: Observable;
|
||||
/**
|
||||
* Shared Element Transitions (preview)
|
||||
* Allows you to auto animate between shared elements on two different screesn to create smooth navigational experiences.
|
||||
@@ -114,6 +117,15 @@ export class SharedTransition {
|
||||
});
|
||||
return { instance: transition };
|
||||
}
|
||||
static events() {
|
||||
if (!sharedTransitionEvents) {
|
||||
sharedTransitionEvents = new Observable();
|
||||
}
|
||||
return sharedTransitionEvents;
|
||||
}
|
||||
static startedEvent = 'SharedTransitionStartedEvent';
|
||||
static finishedEvent = 'SharedTransitionFinishedEvent';
|
||||
static cancelledEvent = 'SharedTransitionCancelledEvent';
|
||||
|
||||
/**
|
||||
* Enable to see various console logging output of Shared Element Transition behavior.
|
||||
@@ -145,13 +157,13 @@ export class SharedTransition {
|
||||
* @private
|
||||
*/
|
||||
static getState(id: number) {
|
||||
return SharedTransition.currentStack.find((t) => t.instance.id === id);
|
||||
return SharedTransition.currentStack?.find((t) => t.instance.id === id);
|
||||
}
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
static finishState(id: number) {
|
||||
const index = SharedTransition.currentStack.findIndex((t) => t.instance.id === id);
|
||||
const index = SharedTransition.currentStack?.findIndex((t) => t.instance.id === id);
|
||||
if (index > -1) {
|
||||
SharedTransition.currentStack.splice(index, 1);
|
||||
}
|
||||
@@ -176,10 +188,10 @@ export class SharedTransition {
|
||||
|
||||
// 2. Presenting view: gather all sharedTransitionTag views
|
||||
const presentingSharedElements = <Array<View>>querySelectorAll(fromPage, 'sharedTransitionTag');
|
||||
console.log(
|
||||
'presenting sharedTransitionTags:',
|
||||
presentingSharedElements.map((v) => v.sharedTransitionTag)
|
||||
);
|
||||
// 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);
|
||||
@@ -206,3 +218,22 @@ export function getRectFromProps(props: SharedTransitionPageProperties, defaults
|
||||
height: isNumber(props?.height) ? props?.height : defaults.height,
|
||||
};
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
export function getPageStartDefaultsForType(type: 'page' | 'modal') {
|
||||
return {
|
||||
x: type === 'page' ? Screen.mainScreen.widthDIPs : 0,
|
||||
y: type === 'page' ? 0 : Screen.mainScreen.heightDIPs,
|
||||
width: Screen.mainScreen.widthDIPs,
|
||||
height: Screen.mainScreen.heightDIPs,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user