diff --git a/apps/toolbox/src/pages/transitions.ts b/apps/toolbox/src/pages/transitions.ts index 3f60a4da6..a1db5edc8 100644 --- a/apps/toolbox/src/pages/transitions.ts +++ b/apps/toolbox/src/pages/transitions.ts @@ -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) { diff --git a/packages/core/ui/core/view/index.ios.ts b/packages/core/ui/core/view/index.ios.ts index 475533485..2c160ac88 100644 --- a/packages/core/ui/core/view/index.ios.ts +++ b/packages/core/ui/core/view/index.ios.ts @@ -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; - ownerView: WeakRef; static ObjCProtocols = [UIViewControllerTransitioningDelegate]; - static initWithOwner(owner: WeakRef, ownerView: WeakRef) { + static initWithOwner(owner: WeakRef) { const delegate = 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 { diff --git a/packages/core/ui/core/view/view-common.ts b/packages/core/ui/core/view/view-common.ts index 6c915c87d..3b64a3da0 100644 --- a/packages/core/ui/core/view/view-common.ts +++ b/packages/core/ui/core/view/view-common.ts @@ -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(); } diff --git a/packages/core/ui/frame/fragment.transitions.d.ts b/packages/core/ui/frame/fragment.transitions.d.ts index 0c8d83fc7..b9f349494 100644 --- a/packages/core/ui/frame/fragment.transitions.d.ts +++ b/packages/core/ui/frame/fragment.transitions.d.ts @@ -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 diff --git a/packages/core/ui/frame/fragment.transitions.ios.ts b/packages/core/ui/frame/fragment.transitions.ios.ts deleted file mode 100644 index e52083942..000000000 --- a/packages/core/ui/frame/fragment.transitions.ios.ts +++ /dev/null @@ -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.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 = 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; -} diff --git a/packages/core/ui/frame/index.ios.ts b/packages/core/ui/frame/index.ios.ts index 69fc22f1a..216e2bcf7 100644 --- a/packages/core/ui/frame/index.ios.ts +++ b/packages/core/ui/frame/index.ios.ts @@ -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; - started = false; - transitionContext: UIViewControllerContextTransitioning; - backgroundAnimation: UIViewPropertyAnimator; - - static initWithOwner(owner: WeakRef) { - const ctrl = 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; diff --git a/packages/core/ui/index.ts b/packages/core/ui/index.ts index 719dc71d5..21f6de033 100644 --- a/packages/core/ui/index.ts +++ b/packages/core/ui/index.ts @@ -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'; diff --git a/packages/core/ui/transition/fade-transition.ios.ts b/packages/core/ui/transition/fade-transition.ios.ts index 2c8240c52..f18a19f0e 100644 --- a/packages/core/ui/transition/fade-transition.ios.ts +++ b/packages/core/ui/transition/fade-transition.ios.ts @@ -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; + + static initWithOwner(owner: WeakRef) { + const ctrl = 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); + } + ); + } } } diff --git a/packages/core/ui/transition/index.d.ts b/packages/core/ui/transition/index.d.ts index 707075367..85a01707d 100644 --- a/packages/core/ui/transition/index.d.ts +++ b/packages/core/ui/transition/index.d.ts @@ -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; } diff --git a/packages/core/ui/transition/index.ios.ts b/packages/core/ui/transition/index.ios.ts index 294f69469..93a554cdb 100644 --- a/packages/core/ui/transition/index.ios.ts +++ b/packages/core/ui/transition/index.ios.ts @@ -1,5 +1,4 @@ import type { Transition as TransitionType } from '.'; -import { Screen } from '../../platform'; let transitionId = 0; export class Transition implements TransitionType { diff --git a/packages/core/ui/transition/modal-transition.ios.ts b/packages/core/ui/transition/modal-transition.ios.ts index 58df067d8..0061f37c9 100644 --- a/packages/core/ui/transition/modal-transition.ios.ts +++ b/packages/core/ui/transition/modal-transition.ios.ts @@ -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); diff --git a/packages/core/ui/transition/page-transition.ios.ts b/packages/core/ui/transition/page-transition.ios.ts index 4c115156b..182419697 100644 --- a/packages/core/ui/transition/page-transition.ios.ts +++ b/packages/core/ui/transition/page-transition.ios.ts @@ -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; + started = false; + transitionContext: UIViewControllerContextTransitioning; + backgroundAnimation: UIViewPropertyAnimator; + + static initWithOwner(owner: WeakRef) { + const ctrl = 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; + + static initWithOwner(owner: WeakRef) { + const ctrl = 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; + } } } } diff --git a/packages/core/ui/transition/shared-transition.ts b/packages/core/ui/transition/shared-transition.ts index ace2f4390..4c65b58f2 100644 --- a/packages/core/ui/transition/shared-transition.ts +++ b/packages/core/ui/transition/shared-transition.ts @@ -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; diff --git a/packages/core/ui/transition/slide-transition.ios.ts b/packages/core/ui/transition/slide-transition.ios.ts index 578633b43..5b9f1d827 100644 --- a/packages/core/ui/transition/slide-transition.ios.ts +++ b/packages/core/ui/transition/slide-transition.ios.ts @@ -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; + + static initWithOwner(owner: WeakRef) { + const ctrl = 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); + } + ); + } } }