chore: cleanup

This commit is contained in:
Nathan Walker
2023-03-19 18:55:16 -07:00
parent 23726b3943
commit de0a6dc7ae
14 changed files with 804 additions and 733 deletions

View File

@@ -1,4 +1,4 @@
import { Observable, EventData, Page, ShowModalOptions, SharedTransition, ModalTransition, PageTransition } from '@nativescript/core';
import { Observable, EventData, Page, ShowModalOptions, SharedTransition, ModalTransition, PageTransition, FadeTransition, SlideTransition } from '@nativescript/core';
let page: Page;
export function navigatingTo(args: EventData) {

View File

@@ -33,11 +33,9 @@ const majorVersion = iOSNativeHelper.MajorVersion;
export class View extends ViewCommon implements ViewDefinition {
nativeViewProtected: UIView;
viewController: UIViewController;
transitionInteractiveCtrl: UIPercentDrivenInteractiveTransition;
private _popoverPresentationDelegate: IOSHelper.UIPopoverPresentationControllerDelegateImp;
private _adaptivePresentationDelegate: IOSHelper.UIAdaptivePresentationControllerDelegateImp;
private _transitioningDelegate: UIViewControllerTransitioningDelegateImpl;
private _interactiveDismissGesture: (args: PanGestureEventData) => void;
/**
* Track modal open animated options to use same option upon close
@@ -471,18 +469,15 @@ export class View extends ViewCommon implements ViewDefinition {
if (options.transition) {
controller.modalPresentationStyle = UIModalPresentationStyle.Custom;
if (options.transition.instance) {
this._transitioningDelegate = UIViewControllerTransitioningDelegateImpl.initWithOwner(new WeakRef(options.transition.instance), new WeakRef(this));
this._transitioningDelegate = UIViewControllerTransitioningDelegateImpl.initWithOwner(new WeakRef(options.transition.instance));
controller.transitioningDelegate = this._transitioningDelegate;
this.transitionId = options.transition.instance.id;
const transitionState = SharedTransition.getState(options.transition.instance.id);
if (transitionState?.interactive?.dismiss) {
this._updateInteractiveTransition({
options: transitionState?.interactive?.dismiss,
});
// interactive transitions via gestures
// TODO - these could be typed as: boolean | (view: View) => void
// to allow users to define their own custom gesture dismissals
this._interactiveDismissGesture = this._interactiveDismissGestureHandler.bind(this);
this.on('pan', this._interactiveDismissGesture);
options.transition.instance.setupInteractiveGesture(this._closeModalCallback.bind(this), this);
}
}
} else if (options.fullscreen) {
@@ -565,47 +560,6 @@ export class View extends ViewCommon implements ViewDefinition {
controller = null;
}
private _interactiveDismissGestureHandler(args: PanGestureEventData) {
if (args?.ios?.view) {
this._updateInteractiveTransition({
began: true,
cancelled: false,
});
const percent = this.interactiveTransition?.options.percentFormula ? this.interactiveTransition?.options.percentFormula(args) : args.deltaY / (args.ios.view.bounds.size.height / 2);
if (SharedTransition.DEBUG) {
console.log('Interactive dismissal percentage:', percent);
}
switch (args.state) {
case GestureStateTypes.began:
if (this._closeModalCallback) {
this._closeModalCallback();
}
break;
case GestureStateTypes.changed:
if (percent < 1) {
if (this.transitionInteractiveCtrl) {
this.transitionInteractiveCtrl.updateInteractiveTransition(percent);
}
}
break;
case GestureStateTypes.cancelled:
case GestureStateTypes.ended:
if (this.transitionInteractiveCtrl) {
const finishThreshold = isNumber(this.interactiveTransition?.options?.finishThreshold) ? this.interactiveTransition?.options?.finishThreshold : 0.5;
if (percent > finishThreshold) {
this.transitionInteractiveCtrl.finishInteractiveTransition();
} else {
this._updateInteractiveTransition({
cancelled: true,
});
this.transitionInteractiveCtrl.cancelInteractiveTransition();
}
}
break;
}
}
}
protected _hideNativeModalView(parent: View, whenClosedCallback: () => void) {
if (!parent || !parent.viewController) {
Trace.error('Trying to hide modal view but no parent with viewController specified.');
@@ -627,10 +581,10 @@ export class View extends ViewCommon implements ViewDefinition {
}
parentController.dismissViewControllerAnimatedCompletion(animated, () => {
if (!this.interactiveTransition?.cancelled) {
const transitionState = SharedTransition.getState(this.transitionId);
if (!transitionState?.interactiveCancelled) {
this._transitioningDelegate = null;
this.transitionInteractiveCtrl = null;
this.off('pan', this._interactiveDismissGesture);
// this.off('pan', this._interactiveDismissGesture);
if (this._modalAnimatedOptions) {
this._modalAnimatedOptions.pop();
}
@@ -990,13 +944,11 @@ View.prototype._nativeBackgroundState = 'unset';
@NativeClass
class UIViewControllerTransitioningDelegateImpl extends NSObject implements UIViewControllerTransitioningDelegate {
owner: WeakRef<ModalTransition>;
ownerView: WeakRef<View>;
static ObjCProtocols = [UIViewControllerTransitioningDelegate];
static initWithOwner(owner: WeakRef<ModalTransition>, ownerView: WeakRef<View>) {
static initWithOwner(owner: WeakRef<ModalTransition>) {
const delegate = <UIViewControllerTransitioningDelegateImpl>UIViewControllerTransitioningDelegateImpl.new();
delegate.owner = owner;
delegate.ownerView = ownerView;
return delegate;
}
@@ -1019,12 +971,9 @@ class UIViewControllerTransitioningDelegateImpl extends NSObject implements UIVi
interactionControllerForDismissal?(animator: UIViewControllerAnimatedTransitioning): UIViewControllerInteractiveTransitioning {
const owner = this.owner?.deref();
if (owner?.iosInteractionDismiss) {
const ownerView = this.ownerView?.deref();
if (ownerView) {
if (ownerView.interactiveTransition?.began) {
ownerView.transitionInteractiveCtrl = owner.iosInteractionDismiss(animator);
return ownerView.transitionInteractiveCtrl;
}
const transitionState = SharedTransition.getState(owner.id);
if (transitionState?.interactiveBegan) {
return owner.iosInteractionDismiss(animator);
}
}
return null;
@@ -1037,14 +986,6 @@ class UIViewControllerTransitioningDelegateImpl extends NSObject implements UIVi
}
return null;
}
presentationControllerForPresentedViewControllerPresentingViewControllerSourceViewController?(presented: UIViewController, presenting: UIViewController, source: UIViewController): UIPresentationController {
const owner = this.owner?.deref();
if (owner?.iosPresentedViewController) {
return owner.iosPresentedViewController(presented, presenting, source);
}
return null;
}
}
export class ContainerView extends View {

View File

@@ -96,7 +96,9 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition {
public _modalParent: ViewCommon;
private _modalContext: any;
private _modal: ViewCommon;
interactiveTransition: InteractiveTransitionState;
// any active transition instance id for tracking state
transitionId: number;
private _measuredWidth: number;
private _measuredHeight: number;
@@ -250,13 +252,6 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition {
return false;
}
_updateInteractiveTransition(state: InteractiveTransitionState) {
this.interactiveTransition = {
...(this.interactiveTransition || {}),
...state,
};
}
_setupAsRootView(context: any): void {
super._setupAsRootView(context);
if (!this._styleScope) {
@@ -423,16 +418,17 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition {
};
const whenClosedCallback = () => {
if (this.interactiveTransition?.began) {
this._updateInteractiveTransition({
began: false,
const transitionState = SharedTransition.getState(this.transitionId);
if (transitionState?.interactiveBegan) {
SharedTransition.updateState(this.transitionId, {
interactiveBegan: false,
});
if (!this.interactiveTransition?.cancelled) {
if (!transitionState?.interactiveCancelled) {
cleanupModalViews();
}
}
if (!this.interactiveTransition?.cancelled) {
if (!transitionState?.interactiveCancelled) {
if (typeof options.closeCallback === 'function') {
options.closeCallback.apply(undefined, originalArgs);
}
@@ -441,7 +437,8 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition {
}
};
if (!this.interactiveTransition?.began) {
const transitionState = SharedTransition.getState(this.transitionId);
if (!transitionState?.interactiveBegan) {
cleanupModalViews();
}

View File

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

View File

@@ -1,82 +0,0 @@
import { NavigationTransition } from '.';
import { Transition } from '../transition';
import { SlideTransition } from '../transition/slide-transition';
import { FadeTransition } from '../transition/fade-transition';
import { Trace } from '../../trace';
@NativeClass
class AnimatedTransitioning extends NSObject implements UIViewControllerAnimatedTransitioning {
public static ObjCProtocols = [UIViewControllerAnimatedTransitioning];
private _transition: Transition;
private _operation: UINavigationControllerOperation;
private _fromVC: UIViewController;
private _toVC: UIViewController;
private _transitionType: string;
public static init(transition: Transition, operation: UINavigationControllerOperation, fromVC: UIViewController, toVC: UIViewController): AnimatedTransitioning {
const impl = <AnimatedTransitioning>AnimatedTransitioning.new();
impl._transition = transition;
impl._operation = operation;
impl._fromVC = fromVC;
impl._toVC = toVC;
return impl;
}
public animateTransition(transitionContext: UIViewControllerContextTransitioning): void {
switch (this._operation) {
case UINavigationControllerOperation.Push:
this._transitionType = 'push';
break;
case UINavigationControllerOperation.Pop:
this._transitionType = 'pop';
break;
case UINavigationControllerOperation.None:
this._transitionType = 'none';
break;
}
if (Trace.isEnabled()) {
Trace.write(`START ${this._transition} ${this._transitionType}`, Trace.categories.Transition);
}
this._transition.animateIOSTransition(transitionContext, this._fromVC, this._toVC, this._operation);
}
public transitionDuration(transitionContext: UIViewControllerContextTransitioning): number {
return this._transition.getDuration();
}
public animationEnded(transitionCompleted: boolean): void {
if (transitionCompleted) {
if (Trace.isEnabled()) {
Trace.write(`END ${this._transition} ${this._transitionType}`, Trace.categories.Transition);
}
} else {
if (Trace.isEnabled()) {
Trace.write(`CANCEL ${this._transition} ${this._transitionType}`, Trace.categories.Transition);
}
}
}
}
export function _createIOSAnimatedTransitioning(navigationTransition: NavigationTransition, nativeCurve: UIViewAnimationCurve, operation: UINavigationControllerOperation, fromVC: UIViewController, toVC: UIViewController): UIViewControllerAnimatedTransitioning {
const instance = <Transition>navigationTransition.instance;
let transition: Transition;
if (instance) {
// Instance transition should take precedence even if the given name match existing transition.
transition = instance;
} else if (navigationTransition.name) {
const name = navigationTransition.name.toLowerCase();
if (name.indexOf('slide') === 0) {
const direction = name.substring('slide'.length) || 'left'; //Extract the direction from the string
transition = new SlideTransition(direction, navigationTransition.duration, nativeCurve);
} else if (name === 'fade') {
transition = new FadeTransition(navigationTransition.duration, nativeCurve);
}
}
return transition ? AnimatedTransitioning.init(transition, operation, fromVC, toVC) : undefined;
}

View File

@@ -3,16 +3,14 @@ import { iOSFrame as iOSFrameDefinition, BackstackEntry, NavigationTransition }
import { FrameBase, NavigationType } from './frame-common';
import { Page } from '../page';
import { View } from '../core/view';
import { _createIOSAnimatedTransitioning } from './fragment.transitions';
import { IOSHelper } from '../core/view/view-helper';
import { Screen } from '../../platform';
import { profile } from '../../profiling';
import { iOSNativeHelper, layout } from '../../utils';
import { isNumber } from '../../utils/types';
import { Trace } from '../../trace';
import type { PageTransition } from '../transition/page-transition';
import { SlideTransition } from '../transition/slide-transition';
import { FadeTransition } from '../transition/fade-transition';
import { SharedTransition } from '../transition/shared-transition';
import { GestureStateTypes, PanGestureEventData } from '../gestures';
export * from './frame-common';
@@ -29,9 +27,7 @@ let navDepth = -1;
export class Frame extends FrameBase {
viewController: UINavigationControllerImpl;
_animatedDelegate: UINavigationControllerDelegate;
transitionInteractiveCtrl: UIPercentDrivenInteractiveTransition;
public _ios: iOSFrame;
private _interactiveDismissGesture: (args: PanGestureEventData) => void;
constructor() {
super();
@@ -110,15 +106,16 @@ export class Frame extends FrameBase {
}
this._ios.controller.delegate = this._animatedDelegate;
viewController[DELEGATE] = this._animatedDelegate;
const transitionState = SharedTransition.getState(navigationTransition.instance.id);
if (transitionState?.interactive?.dismiss) {
this._updateInteractiveTransition({
options: transitionState?.interactive?.dismiss,
});
// interactive transitions via gestures
// TODO - allow users to define their own custom gesture dismissals
this._interactiveDismissGesture = this._interactiveDismissGestureHandler.bind(this);
this.on('pan', this._interactiveDismissGesture);
if (navigationTransition.instance) {
this.transitionId = navigationTransition.instance.id;
const transitionState = SharedTransition.getState(this.transitionId);
if (transitionState?.interactive?.dismiss) {
// interactive transitions via gestures
// TODO - allow users to define their own custom gesture dismissals
navigationTransition.instance.setupInteractiveGesture(() => {
this._ios.controller.popViewControllerAnimated(true);
}, this);
}
}
} else {
viewController[DELEGATE] = null;
@@ -203,48 +200,6 @@ export class Frame extends FrameBase {
}
}
private _interactiveDismissGestureHandler(args: PanGestureEventData) {
if (args?.ios?.view) {
this._updateInteractiveTransition({
began: true,
cancelled: false,
});
const percent = this.interactiveTransition?.options.percentFormula ? this.interactiveTransition?.options.percentFormula(args) : args.deltaX / (args.ios.view.bounds.size.width / 2);
if (SharedTransition.DEBUG) {
console.log('Interactive dismissal percentage:', percent);
}
switch (args.state) {
case GestureStateTypes.began:
this._ios.controller.popViewControllerAnimated(true);
break;
case GestureStateTypes.changed:
if (percent < 1) {
if (this.transitionInteractiveCtrl) {
this.transitionInteractiveCtrl.updateInteractiveTransition(percent);
}
}
break;
case GestureStateTypes.cancelled:
case GestureStateTypes.ended:
if (this.transitionInteractiveCtrl) {
const finishThreshold = isNumber(this.interactiveTransition?.options?.finishThreshold) ? this.interactiveTransition?.options?.finishThreshold : 0.5;
if (percent > finishThreshold) {
this.off('pan', this._interactiveDismissGesture);
this.transitionInteractiveCtrl.finishInteractiveTransition();
// TODO: may wanna null this out at some later point
// this.transitionInteractiveCtrl = null;
} else {
this._updateInteractiveTransition({
cancelled: true,
});
this.transitionInteractiveCtrl.cancelInteractiveTransition();
}
}
break;
}
}
}
private pushViewControllerAnimated(viewController: UIViewController, animated: boolean, isModal: boolean) {
const transitionCoordinator = this._ios.controller.transitionCoordinator;
if (!isModal && transitionCoordinator) {
@@ -473,21 +428,35 @@ class UINavigationControllerAnimatedDelegate extends NSObject implements UINavig
if (Trace.isEnabled()) {
Trace.write(`UINavigationControllerImpl.navigationControllerAnimationControllerForOperationFromViewControllerToViewController(${operation}, ${fromVC}, ${toVC}), transition: ${JSON.stringify(navigationTransition)}`, Trace.categories.NativeLifecycle);
}
this.transition = navigationTransition.instance;
const curve = _getNativeCurve(navigationTransition);
const animationController = _createIOSAnimatedTransitioning(navigationTransition, curve, operation, fromVC, toVC);
if (!this.transition) {
if (navigationTransition.name) {
const curve = _getNativeCurve(navigationTransition);
const name = navigationTransition.name.toLowerCase();
if (name.indexOf('slide') === 0) {
const direction = name.substring('slide'.length) || 'left'; //Extract the direction from the string
this.transition = new SlideTransition(direction, navigationTransition.duration, curve);
} else if (name === 'fade') {
this.transition = new FadeTransition(navigationTransition.duration, curve);
}
}
}
return animationController;
if (this.transition?.iosNavigatedController) {
return this.transition.iosNavigatedController(navigationController, operation, fromVC, toVC);
}
return null;
}
navigationControllerInteractionControllerForAnimationController(navigationController: UINavigationController, animationController: UIViewControllerAnimatedTransitioning): UIViewControllerInteractiveTransitioning {
const owner = this.owner?.deref();
if (owner) {
if (owner.interactiveTransition?.began) {
owner.transitionInteractiveCtrl = PercentInteractiveController.initWithOwner(new WeakRef(this.transition));
return owner.transitionInteractiveCtrl;
const state = SharedTransition.getState(owner.transitionId);
if (state?.instance?.iosInteractionDismiss) {
if (state?.interactiveBegan) {
return state?.instance?.iosInteractionDismiss(null);
}
}
}
@@ -495,134 +464,6 @@ class UINavigationControllerAnimatedDelegate extends NSObject implements UINavig
}
}
@NativeClass()
class PercentInteractiveController extends UIPercentDrivenInteractiveTransition implements UIViewControllerInteractiveTransitioning {
static ObjCProtocols = [UIViewControllerInteractiveTransitioning];
owner: WeakRef<PageTransition>;
started = false;
transitionContext: UIViewControllerContextTransitioning;
backgroundAnimation: UIViewPropertyAnimator;
static initWithOwner(owner: WeakRef<PageTransition>) {
const ctrl = <PercentInteractiveController>PercentInteractiveController.new();
ctrl.owner = owner;
return ctrl;
}
startInteractiveTransition(transitionContext: UIViewControllerContextTransitioning) {
console.log('startInteractiveTransition');
this.transitionContext = transitionContext;
// Using insertSubviewBelowSubview solves view stack visually but leaves a controller overlapping
// TODO: need to find out solution here
// console.log('this.transitionContext.containerView.subviews.count:', this.transitionContext.containerView.subviews.count)
const owner: any = this.owner?.deref();
if (owner) {
// console.log('owner.presenting:', owner.presenting)
// console.log('owner.presented:', owner.presented)
if (owner.presenting) {
this.transitionContext.containerView.insertSubviewBelowSubview(owner.presenting.view, owner.presented.view);
}
}
}
updateInteractiveTransition(percentComplete: number) {
const owner: any = 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 props = state.fromPageEnd;
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;
}
}
cancelInteractiveTransition() {
// console.log('cancelInteractiveTransition');
const owner: any = this.owner?.deref();
if (owner && this.started) {
const state = SharedTransition.getState(owner.id);
if (!state) {
return;
}
if (this.backgroundAnimation) {
this.backgroundAnimation.reversed = true;
const duration = isNumber(state.toPageStart?.duration) ? state.toPageStart?.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.started = false;
this.transitionContext.completeTransition(false);
}, duration * 1000);
}
}
}
finishInteractiveTransition() {
// console.log('finishInteractiveTransition');
const owner: any = 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.fromPageEnd?.duration) ? state.fromPageEnd?.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.started = false;
// console.log('this.transitionContext.containerView.subviews.count:', this.transitionContext.containerView.subviews.count)
// this.transitionContext.containerView.subviews.objectAtIndex(this.transitionContext.containerView.subviews.count-1).removeFromSuperview();
this.transitionContext.completeTransition(true);
console.log('completeTransition!');
// console.log('this.transitionContext.containerView.subviews.count:', this.transitionContext.containerView.subviews.count)
}, duration * 1001);
}
}
}
}
@NativeClass
class UINavigationControllerImpl extends UINavigationController {
private _owner: WeakRef<Frame>;

View File

@@ -77,6 +77,8 @@ export { TimePicker } from './time-picker';
export { Transition } from './transition';
export { ModalTransition } from './transition/modal-transition';
export { PageTransition } from './transition/page-transition';
export { FadeTransition } from './transition/fade-transition';
export { SlideTransition } from './transition/slide-transition';
export { SharedTransition, SharedTransitionAnimationType } from './transition/shared-transition';
export { WebView } from './web-view';
export type { LoadEventData, WebViewNavigationType } from './web-view';

View File

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

View File

@@ -8,6 +8,9 @@ export declare class Transition {
getCurve(): any;
animateIOSTransition(transitionContext: any /*UIViewControllerContextTransitioning */, fromViewCtrl: any /* UIViewController */, toViewCtrl: any /* UIViewController */, operation: any /* UINavigationControllerOperation */): void;
createAndroidAnimator(transitionType: string): any;
setupInteractiveGesture?(startCallback: () => void, view: View): void;
iosDismissedController?(dismissed: any /* UIViewController */): any /* UIViewControllerAnimatedTransitioning */;
iosPresentedController?(presented: any /* UIViewController */, presenting: any /* UIViewController */, source: any /* UIViewController */): any /* UIViewControllerAnimatedTransitioning */;
@@ -16,7 +19,7 @@ export declare class Transition {
iosInteractionPresented?(animator: any /* UIViewControllerAnimatedTransitioning */): any /* UIViewControllerInteractiveTransitioning */;
iosPresentedViewController?(presented: any /* UIViewController */, presenting: any /* UIViewController */, source: any /* UIViewController */): any /* UIPresentationController */;
iosNavigatedController?(navigationController: any /* UINavigationController */, operation: number, fromVC: any /* UIViewController */, toVC: any /* UIViewController */): any /* UIViewControllerAnimatedTransitioning */;
androidFragmentTransactionCallback?(fragmentTransaction: any /* androidx.fragment.app.FragmentTransaction */, currentEntry: BackstackEntry, newEntry: BackstackEntry): void;
}

View File

@@ -1,5 +1,4 @@
import type { Transition as TransitionType } from '.';
import { Screen } from '../../platform';
let transitionId = 0;
export class Transition implements TransitionType {

View File

@@ -4,16 +4,20 @@ import { iOSNativeHelper } from '../../utils/native-helper';
import { isNumber } from '../../utils/types';
import { Transition } from '.';
import { SharedTransition, SharedTransitionAnimationType, DEFAULT_DURATION, DEFAULT_SPRING } from './shared-transition';
import { PanGestureEventData, GestureStateTypes } from '../gestures';
export class ModalTransition extends Transition {
transitionController: ModalTransitionController;
interactiveController: UIPercentDrivenInteractiveTransition;
interactiveGestureRecognizer: UIScreenEdgePanGestureRecognizer;
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 }>;
};
private _interactiveStartCallback: () => void;
private _interactiveDismissGesture: (args: any /*PanGestureEventData*/) => void;
iosPresentedController(presented: UIViewController, presenting: UIViewController, source: UIViewController): UIViewControllerAnimatedTransitioning {
this.transitionController = ModalTransitionController.initWithOwner(new WeakRef(this));
@@ -39,9 +43,60 @@ export class ModalTransition extends Transition {
return null;
}
iosPresentedViewController(presented: UIViewController, presenting: UIViewController, source: UIViewController): UIPresentationController {
// console.log('-- iosPresentedViewController --');
return null;
setupInteractiveGesture(startCallback: () => void, view: View) {
this._interactiveStartCallback = startCallback;
this._interactiveDismissGesture = this._interactiveDismissGestureHandler.bind(this);
view.on('pan', this._interactiveDismissGesture);
// this.interactiveGestureRecognizer = UIScreenEdgePanGestureRecognizer.alloc().initWithTargetAction()
// let edgeSwipeGestureRecognizer = UIScreenEdgePanGestureRecognizer(target: self, action: #selector(handleSwipe(_:)))
// edgeSwipeGestureRecognizer.edges = .left
// view.addGestureRecognizer(edgeSwipeGestureRecognizer)
}
private _interactiveDismissGestureHandler(args: PanGestureEventData) {
if (args?.ios?.view) {
const state = SharedTransition.getState(this.id);
// 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);
}
switch (args.state) {
case GestureStateTypes.began:
SharedTransition.updateState(this.id, {
interactiveBegan: true,
interactiveCancelled: false,
});
if (this._interactiveStartCallback) {
this._interactiveStartCallback();
}
break;
case GestureStateTypes.changed:
if (percent < 1) {
if (this.interactiveController) {
this.interactiveController.updateInteractiveTransition(percent);
}
}
break;
case GestureStateTypes.cancelled:
case GestureStateTypes.ended:
if (this.interactiveController) {
const finishThreshold = isNumber(state.interactive?.dismiss?.finishThreshold) ? state.interactive.dismiss.finishThreshold : 0.5;
if (percent > finishThreshold) {
this.interactiveController.finishInteractiveTransition();
} else {
SharedTransition.updateState(this.id, {
interactiveCancelled: true,
});
this.interactiveController.cancelInteractiveTransition();
}
}
break;
}
}
}
}
@@ -128,6 +183,7 @@ class PercentInteractiveController extends UIPercentDrivenInteractiveTransition
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);
@@ -165,7 +221,7 @@ class ModalTransitionController extends NSObject implements UIViewControllerAnim
}
animateTransition(transitionContext: UIViewControllerContextTransitioning): void {
// console.log('ModalTransitionController animateTransition:', animationType);
// console.log('ModalTransitionController animateTransition');
const owner = this.owner.deref();
if (owner) {
// console.log('owner.id:', owner.id);

View File

@@ -3,294 +3,532 @@ import { 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';
export class PageTransition extends Transition {
transitionController: PageTransitionController;
interactiveController: UIPercentDrivenInteractiveTransition;
presented: UIViewController;
presenting: UIViewController;
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 }>;
};
private _interactiveStartCallback: () => void;
private _interactiveDismissGesture: (args: any /*PanGestureEventData*/) => void;
private _interactiveGestureTeardown: () => void;
animateIOSTransition(transitionContext: UIViewControllerContextTransitioning, fromViewCtrl: UIViewController, toViewCtrl: UIViewController, operation: UINavigationControllerOperation): void {
// console.log('--- PageTransitionController animateTransition');
// console.log('toViewCtrl:', toViewCtrl);
// console.log('fromViewCtrl:', fromViewCtrl);
// console.log('owner.id:', owner.id);
const state = SharedTransition.getState(this.id);
if (!state) {
return;
iosNavigatedController(navigationController: UINavigationController, operation: number, fromVC: UIViewController, toVC: UIViewController): UIViewControllerAnimatedTransitioning {
if (!this.transitionController) {
this.presented = toVC;
this.presenting = fromVC;
}
// console.log('state.activeType:', state.activeType);
// console.log('operation:', operation);
// switch (state.activeType) {
switch (operation) {
case UINavigationControllerOperation.Push: {
this.presented = toViewCtrl;
this.presenting = fromViewCtrl;
this.transitionController = PageTransitionController.initWithOwner(new WeakRef(this));
console.log('iosNavigatedController presenting:', this.presenting);
transitionContext.containerView.insertSubviewAboveSubview(this.presented.view, fromViewCtrl.view);
this.presented.view.layoutIfNeeded();
this.operation = operation;
return this.transitionController;
}
const { sharedElements, presented } = SharedTransition.getSharedElements(state.page, state.toPage);
iosInteractionDismiss(animator: UIViewControllerAnimatedTransitioning): UIViewControllerInteractiveTransitioning {
// console.log('-- iosInteractionDismiss --');
this.interactiveController = PercentInteractiveController.initWithOwner(new WeakRef(this));
return this.interactiveController;
}
if (SharedTransition.DEBUG) {
console.log(' PageTransition: Push');
console.log(
`1. Found sharedTransitionTags to animate:`,
sharedElements.map((v) => v.sharedTransitionTag)
);
setupInteractiveGesture(startCallback: () => void, view: View): () => void {
console.log(' -- setupInteractiveGesture --');
this._interactiveStartCallback = startCallback;
if (!this._interactiveDismissGesture) {
console.log('setup but tearing down first!');
view.off('pan', this._interactiveDismissGesture);
this._interactiveDismissGesture = this._interactiveDismissGestureHandler.bind(this);
}
view.on('pan', this._interactiveDismissGesture);
console.log(`2. Take snapshots of shared elements and position them based on presenting view:`);
}
this._interactiveGestureTeardown = () => {
console.log(`-- TEARDOWN setupInteractiveGesture --`);
view.off('pan', this._interactiveDismissGesture);
this._interactiveDismissGesture = null;
};
return this._interactiveGestureTeardown;
}
for (const presentingView of sharedElements) {
if (!this.sharedElements) {
this.sharedElements = {
presented: [],
presenting: [],
};
}
const presentingSharedElement = presentingView.ios;
private _interactiveDismissGestureHandler(args: PanGestureEventData) {
if (args?.ios?.view) {
// console.log('this.id:', this.id);
const state = SharedTransition.getState(this.id);
// 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));
}
this.sharedElements.presenting.push({
view: presentingView,
startFrame,
endFrame,
snapshot: sharedElementSnapshot,
startOpacity: presentingView.opacity,
endOpacity: presentedView.opacity,
});
this.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 this.sharedElements.presented) {
presented.view.opacity = presented.startOpacity;
}
for (const presenting of this.sharedElements.presenting) {
presenting.snapshot.removeFromSuperview();
}
SharedTransition.updateState(this.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();
if (SharedTransition.DEBUG) {
console.log('3. Animating shared elements:');
}
for (const presented of this.sharedElements.presented) {
const presentingMatch = this.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.toPageStart;
this.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;
this.presented.view.frame = CGRectMake(startX, startY, startWidth, startHeight);
const animateProperties = () => {
const props = state.toPageEnd;
// animate page properties to the following:
this.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;
this.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?.tension : DEFAULT_SPRING.friction,
animations: () => {
animateProperties();
updateFramePresent();
},
completion: () => {
cleanupPresent();
},
});
}
break;
const percent = state.interactive?.dismiss?.percentFormula ? state.interactive.dismiss.percentFormula(args) : args.deltaX / (args.ios.view.bounds.size.width / 2);
if (SharedTransition.DEBUG) {
console.log('Interactive dismissal percentage:', percent);
}
case UINavigationControllerOperation.Pop: {
transitionContext.containerView.insertSubviewBelowSubview(toViewCtrl.view, fromViewCtrl.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:`,
this.sharedElements.presented.map((p) => p.view.sharedTransitionTag)
);
console.log(`2. Add back previously stored sharedElements to dismiss:`);
}
for (const p of this.sharedElements.presented) {
p.view.opacity = 0;
}
for (const p of this.sharedElements.presenting) {
p.snapshot.alpha = p.endOpacity;
transitionContext.containerView.addSubview(p.snapshot);
}
const cleanupDismiss = () => {
for (const presenting of this.sharedElements.presenting) {
presenting.view.opacity = presenting.startOpacity;
presenting.snapshot.removeFromSuperview();
}
SharedTransition.finishState(this.id);
transitionContext.completeTransition(true);
};
const updateFrameDismiss = () => {
if (SharedTransition.DEBUG) {
console.log('3. Dismissing shared elements:');
}
for (const presenting of this.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.fromPageEnd;
const animateProperties = () => {
this.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;
this.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?.tension : DEFAULT_SPRING.friction,
animations: () => {
animateProperties();
updateFrameDismiss();
},
completion: () => {
cleanupDismiss();
},
switch (args.state) {
case GestureStateTypes.began:
SharedTransition.updateState(this.id, {
interactiveBegan: true,
interactiveCancelled: false,
});
}
break;
if (this._interactiveStartCallback) {
this._interactiveStartCallback();
}
break;
case GestureStateTypes.changed:
if (percent < 1) {
if (this.interactiveController) {
this.interactiveController.updateInteractiveTransition(percent);
}
}
break;
case GestureStateTypes.cancelled:
case GestureStateTypes.ended:
if (this.interactiveController) {
const finishThreshold = isNumber(state.interactive?.dismiss?.finishThreshold) ? state.interactive.dismiss.finishThreshold : 0.5;
if (percent > finishThreshold) {
if (this._interactiveGestureTeardown) {
this._interactiveGestureTeardown();
this._interactiveGestureTeardown = null;
}
this.interactiveController.finishInteractiveTransition();
} else {
SharedTransition.updateState(this.id, {
interactiveCancelled: true,
});
this.interactiveController.cancelInteractiveTransition();
}
}
break;
}
}
}
}
@NativeClass()
class PercentInteractiveController extends UIPercentDrivenInteractiveTransition implements UIViewControllerInteractiveTransitioning {
static ObjCProtocols = [UIViewControllerInteractiveTransitioning];
owner: WeakRef<PageTransition>;
started = false;
transitionContext: UIViewControllerContextTransitioning;
backgroundAnimation: UIViewPropertyAnimator;
static initWithOwner(owner: WeakRef<PageTransition>) {
const ctrl = <PercentInteractiveController>PercentInteractiveController.new();
ctrl.owner = owner;
return ctrl;
}
startInteractiveTransition(transitionContext: UIViewControllerContextTransitioning) {
console.log('startInteractiveTransition');
this.transitionContext = transitionContext;
const owner: any = this.owner?.deref();
if (owner) {
this.transitionContext.containerView.insertSubviewBelowSubview(owner.presenting.view, owner.presented.view);
}
}
updateInteractiveTransition(percentComplete: number) {
const owner: any = 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 props = state.fromPageEnd;
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;
}
}
cancelInteractiveTransition() {
// console.log('cancelInteractiveTransition');
const owner: any = this.owner?.deref();
if (owner && this.started) {
const state = SharedTransition.getState(owner.id);
if (!state) {
return;
}
if (this.backgroundAnimation) {
this.backgroundAnimation.reversed = true;
const duration = isNumber(state.toPageStart?.duration) ? state.toPageStart?.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.started = false;
this.transitionContext.completeTransition(false);
}, duration * 1000);
}
}
}
finishInteractiveTransition() {
// console.log('finishInteractiveTransition');
const owner: any = 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.fromPageEnd?.duration) ? state.fromPageEnd?.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);
// SharedTransition.updateState(owner.id, {
// interactiveCancelled: false,
// });
this.backgroundAnimation = null;
this.started = false;
// this.transitionContext.containerView.insertSubviewAboveSubview(owner.presenting.view, owner.presented.view);
this.transitionContext.completeTransition(true);
}, duration * 1000);
}
}
}
}
@NativeClass()
class PageTransitionController extends NSObject implements UIViewControllerAnimatedTransitioning {
static ObjCProtocols = [UIViewControllerAnimatedTransitioning];
owner: WeakRef<PageTransition>;
static initWithOwner(owner: WeakRef<PageTransition>) {
const ctrl = <PageTransitionController>PageTransitionController.new();
ctrl.owner = owner;
return ctrl;
}
transitionDuration(transitionContext: UIViewControllerContextTransitioning): number {
const owner = this.owner.deref();
if (owner) {
return owner.getDuration();
}
return DEFAULT_DURATION;
}
animateTransition(transitionContext: UIViewControllerContextTransitioning): void {
const owner = this.owner.deref();
if (owner) {
console.log('--- PageTransitionController animateTransition');
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.toPageStart;
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.toPageEnd;
// 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?.tension : 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.fromPageEnd;
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?.tension : DEFAULT_SPRING.friction,
animations: () => {
animateProperties();
updateFrameDismiss();
},
completion: () => {
cleanupDismiss();
},
});
}
break;
}
}
}
}

View File

@@ -24,7 +24,7 @@ export interface SharedTransitionInteractiveOptions {
* By default, we handle this via a formula like this for an interactive page back transition:
* - return eventData.deltaX / (eventData.ios.view.bounds.size.width / 2);
* @param eventData PanGestureEventData
* @returns Should return a percentage value
* @returns The percentage value to be used as the finish/cancel threshold
*/
percentFormula?: (eventData: PanGestureEventData) => number;
}
@@ -63,6 +63,9 @@ export interface SharedTransitionConfig {
export interface SharedTransitionState extends SharedTransitionConfig {
activeType?: SharedTransitionAnimationType;
toPage?: ViewBase;
// used internally for determining interactive gesture state of the transition
interactiveBegan?: boolean;
interactiveCancelled?: boolean;
}
type SharedTransitionPageProperties = {
x?: number;

View File

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