mirror of
https://github.com/NativeScript/NativeScript.git
synced 2025-11-05 13:26:48 +08:00
feat: ios page push/pop shared element transitions
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
import { Observable, EventData, Page, ShowModalOptions, SharedTransition, ModalTransition, TransitionType, Screen } from '@nativescript/core';
|
||||
// import { DetailTransition } from './transitions/detail';
|
||||
import { Observable, EventData, Page, ShowModalOptions, SharedTransition, ModalTransition, PageTransition, Screen } from '@nativescript/core';
|
||||
let page: Page;
|
||||
|
||||
export function navigatingTo(args: EventData) {
|
||||
@@ -12,13 +11,22 @@ export function navigatingTo(args: EventData) {
|
||||
// }
|
||||
export class TransitionsModel extends Observable {
|
||||
open() {
|
||||
// const detailTransition = new DetailTransition(400, global.isIOS ? UIViewAnimationCurve.EaseInOut : null);
|
||||
// detailTransition.page = page;
|
||||
page.frame.navigate({
|
||||
moduleName: `pages/transitions/transitions-detail`,
|
||||
// transition: {
|
||||
// instance: detailTransition,
|
||||
// },
|
||||
transition: {
|
||||
instance: SharedTransition.configure({
|
||||
page,
|
||||
instance: new PageTransition(),
|
||||
incomingViewStart: {
|
||||
y: 200,
|
||||
duration: 1000,
|
||||
},
|
||||
dismissViewEnd: {
|
||||
y: 100,
|
||||
duration: 500,
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<Image row="1" sharedTransitionTag="image-modal" src="https://cdn.pixabay.com/photo/2012/08/27/14/19/mountains-55067__340.png" height="230" verticalAlignment="top" marginTop="10" />
|
||||
|
||||
<GridLayout row="2" rows="auto,auto,auto">
|
||||
<Label text="Opened Modal" verticalAlignment="top" marginTop="20" class="text-center" fontSize="28" color="black" />
|
||||
<Label text="Opened Navigated Page" verticalAlignment="top" marginTop="20" class="text-center" fontSize="28" color="black" />
|
||||
|
||||
<ContentView row="2" sharedTransitionTag="open-modal-box1" marginTop="20" width="200" height="200" borderRadius="100" backgroundColor="purple" />
|
||||
<ContentView row="1" sharedTransitionTag="open-modal-box2" marginTop="20" width="75" height="75" borderRadius="37" backgroundColor="orange" />
|
||||
|
||||
2
packages/core/global-types.d.ts
vendored
2
packages/core/global-types.d.ts
vendored
@@ -213,7 +213,7 @@ interface RequireContext {
|
||||
|
||||
interface WeakRef<T extends object> {
|
||||
/**
|
||||
* @deprecated Use deref instead with 8.4+
|
||||
* @deprecated Use deref instead with 8.5+
|
||||
*/
|
||||
get(): T;
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import { profile } from '../../../profiling';
|
||||
import { accessibilityEnabledProperty, accessibilityHiddenProperty, accessibilityHintProperty, accessibilityIdentifierProperty, accessibilityLabelProperty, accessibilityLanguageProperty, accessibilityLiveRegionProperty, accessibilityMediaSessionProperty, accessibilityRoleProperty, accessibilityStateProperty, accessibilityValueProperty, accessibilityIgnoresInvertColorsProperty } from '../../../accessibility/accessibility-properties';
|
||||
import { IOSPostAccessibilityNotificationType, isAccessibilityServiceEnabled, updateAccessibilityProperties, AccessibilityEventOptions, AccessibilityRole, AccessibilityState } from '../../../accessibility';
|
||||
import { CoreTypes } from '../../../core-types';
|
||||
import { CustomTransitionModal } from '../../transition';
|
||||
import type { ModalTransition } from '../../transition/modal-transition';
|
||||
|
||||
export * from './view-common';
|
||||
// helpers (these are okay re-exported here)
|
||||
@@ -922,10 +922,10 @@ View.prototype._nativeBackgroundState = 'unset';
|
||||
|
||||
@NativeClass
|
||||
class UIViewControllerTransitioningDelegateImpl extends NSObject implements UIViewControllerTransitioningDelegate {
|
||||
owner: WeakRef<CustomTransitionModal>;
|
||||
owner: WeakRef<ModalTransition>;
|
||||
static ObjCProtocols = [UIViewControllerTransitioningDelegate];
|
||||
|
||||
static initWithOwner(owner: WeakRef<CustomTransitionModal>) {
|
||||
static initWithOwner(owner: WeakRef<ModalTransition>) {
|
||||
const delegate = <UIViewControllerTransitioningDelegateImpl>UIViewControllerTransitioningDelegateImpl.new();
|
||||
delegate.owner = owner;
|
||||
return delegate;
|
||||
|
||||
@@ -402,11 +402,23 @@ class UINavigationControllerAnimatedDelegate extends NSObject implements UINavig
|
||||
Trace.write(`UINavigationControllerImpl.navigationControllerAnimationControllerForOperationFromViewControllerToViewController(${operation}, ${fromVC}, ${toVC}), transition: ${JSON.stringify(navigationTransition)}`, Trace.categories.NativeLifecycle);
|
||||
}
|
||||
|
||||
// Shared element transitions
|
||||
if (operation === UINavigationControllerOperation.Push && navigationTransition.instance?.iosPresentedController) {
|
||||
return navigationTransition.instance?.iosPresentedController(toVC, fromVC, navigationController);
|
||||
} else if (operation === UINavigationControllerOperation.Pop && navigationTransition.instance?.iosDismissedController) {
|
||||
return navigationTransition.instance?.iosDismissedController(toVC);
|
||||
}
|
||||
|
||||
const curve = _getNativeCurve(navigationTransition);
|
||||
const animationController = _createIOSAnimatedTransitioning(navigationTransition, curve, operation, fromVC, toVC);
|
||||
|
||||
return animationController;
|
||||
}
|
||||
|
||||
// TODO: wire this up for interactive gesture control of transition
|
||||
// navigationControllerInteractionControllerForAnimationController(navigationController: UINavigationController, animationController: UIViewControllerAnimatedTransitioning): UIViewControllerInteractiveTransitioning {
|
||||
|
||||
// }
|
||||
}
|
||||
|
||||
@NativeClass
|
||||
|
||||
@@ -76,7 +76,7 @@ export { TextView } from './text-view';
|
||||
export { TimePicker } from './time-picker';
|
||||
export { Transition } from './transition';
|
||||
export { ModalTransition } from './transition/modal-transition';
|
||||
export { PageTransition } from './transition/page-transition';
|
||||
export { SharedTransition, SharedTransitionAnimationType } from './transition/shared-transition';
|
||||
export type { TransitionType } from './transition';
|
||||
export { WebView } from './web-view';
|
||||
export type { LoadEventData, WebViewNavigationType } from './web-view';
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
import lazy from '../../utils/lazy';
|
||||
import type { Transition as TransitionType } from '.';
|
||||
|
||||
export type { TransitionType } from '.';
|
||||
|
||||
const _defaultInterpolator = lazy(() => new android.view.animation.AccelerateDecelerateInterpolator());
|
||||
|
||||
let transitionId = 0;
|
||||
@@ -45,3 +43,11 @@ export class Transition implements TransitionType {
|
||||
return `Transition@${this.id}`;
|
||||
}
|
||||
}
|
||||
|
||||
export function iosSnapshotView(view: any) {
|
||||
return null;
|
||||
}
|
||||
|
||||
export function iosMatchLayerProperties(view: any, toView: any) {}
|
||||
|
||||
export function iosPrintRect(r: any) {}
|
||||
|
||||
25
packages/core/ui/transition/index.d.ts
vendored
25
packages/core/ui/transition/index.d.ts
vendored
@@ -1,6 +1,11 @@
|
||||
export interface TransitionType {
|
||||
export declare class Transition {
|
||||
id: number;
|
||||
|
||||
static AndroidTransitionType?: { enter?: string; exit?: string; popEnter?: string; popExit?: string };
|
||||
constructor(duration?: number, nativeCurve?: any /* UIViewAnimationCurve | string | CubicBezierAnimationCurve | android.view.animation.Interpolator | android.view.animation.LinearInterpolator */);
|
||||
getDuration(): number;
|
||||
getCurve(): any;
|
||||
animateIOSTransition(transitionContext: any /*UIViewControllerContextTransitioning */, fromViewCtrl: any /* UIViewController */, toViewCtrl: any /* UIViewController */, operation: any /* UINavigationControllerOperation */): void;
|
||||
createAndroidAnimator(transitionType: string): any;
|
||||
iosDismissedController?(dismissed: any /* UIViewController */): any /* UIViewControllerAnimatedTransitioning */;
|
||||
|
||||
iosPresentedController?(presented: any /* UIViewController */, presenting: any /* UIViewController */, source: any /* UIViewController */): any /* UIViewControllerAnimatedTransitioning */;
|
||||
@@ -12,13 +17,9 @@
|
||||
iosPresentedViewController?(presented: any /* UIViewController */, presenting: any /* UIViewController */, source: any /* UIViewController */): any /* UIPresentationController */;
|
||||
}
|
||||
|
||||
export declare class Transition implements TransitionType {
|
||||
id: number;
|
||||
static AndroidTransitionType?: { enter?: string; exit?: string; popEnter?: string; popExit?: string };
|
||||
constructor(duration?: number, nativeCurve?: any /* UIViewAnimationCurve | string | CubicBezierAnimationCurve | android.view.animation.Interpolator | android.view.animation.LinearInterpolator */);
|
||||
getDuration(): number;
|
||||
getCurve(): any;
|
||||
animateIOSTransition(transitionContext: any /*UIViewControllerContextTransitioning */, fromViewCtrl: any /* UIViewController */, toViewCtrl: any /* UIViewController */, operation: any /* UINavigationControllerOperation */): void;
|
||||
createAndroidAnimator(transitionType: string): any;
|
||||
toString(): string;
|
||||
}
|
||||
// TODO: move to native-helper.{ios|android}
|
||||
export declare function iosSnapshotView(view: any /* UIView */): any; /* UIImage */
|
||||
|
||||
export declare function iosMatchLayerProperties(view: any /* UIView */, toView: any /* UIView */);
|
||||
|
||||
export declare function iosPrintRect(r: any /* CGRect */): void;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { TransitionType } from '.';
|
||||
|
||||
export type { TransitionType } from '.';
|
||||
import type { Transition as TransitionType } from '.';
|
||||
import { Screen } from '../../platform';
|
||||
|
||||
let transitionId = 0;
|
||||
export class Transition implements TransitionType {
|
||||
@@ -36,3 +35,40 @@ export class Transition implements TransitionType {
|
||||
return `Transition@${this.id}`;
|
||||
}
|
||||
}
|
||||
|
||||
export function iosSnapshotView(view: UIView): UIImage {
|
||||
if (view instanceof UIImageView) {
|
||||
return view.image;
|
||||
}
|
||||
// console.log('snapshotView view.frame:', printRect(view.frame));
|
||||
UIGraphicsBeginImageContextWithOptions(CGSizeMake(view.frame.size.width, view.frame.size.height), false, Screen.mainScreen.scale);
|
||||
view.layer.renderInContext(UIGraphicsGetCurrentContext());
|
||||
const image = UIGraphicsGetImageFromCurrentImageContext();
|
||||
UIGraphicsEndImageContext();
|
||||
return image;
|
||||
}
|
||||
|
||||
// todo: figure out all the properties/layer properties that we want to transition automatically
|
||||
// note: some need to be done at various stages of the animation and are not in here. (e.g. alpha)
|
||||
export function iosMatchLayerProperties(view: UIView, toView: UIView) {
|
||||
const viewPropertiesToMatch: Array<keyof UIView> = ['backgroundColor'];
|
||||
const layerPropertiesToMatch: Array<keyof CALayer> = ['cornerRadius', 'borderWidth', 'borderColor'];
|
||||
|
||||
viewPropertiesToMatch.forEach((property) => {
|
||||
if (view[property] !== toView[property]) {
|
||||
console.log('| -- matching view property:', property);
|
||||
view[property as any] = toView[property];
|
||||
}
|
||||
});
|
||||
|
||||
layerPropertiesToMatch.forEach((property) => {
|
||||
if (view.layer[property] !== toView.layer[property]) {
|
||||
console.log('| -- matching layer property:', property);
|
||||
view.layer[property as any] = toView.layer[property];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function iosPrintRect(r: CGRect) {
|
||||
return `CGRect(${r.origin.x} ${r.origin.y} ${r.size.width} ${r.size.height})`;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
import { Transition } from '.';
|
||||
|
||||
export declare class ModalTransition extends Transition {}
|
||||
|
||||
@@ -1,17 +1,10 @@
|
||||
import { querySelectorAll } from '../core/view-base';
|
||||
import type { View } from '../core/view';
|
||||
import { Screen } from '../../platform';
|
||||
import { Transition } from '.';
|
||||
import type { TransitionType } from '.';
|
||||
import { SharedTransition, SharedTransitionAnimationType } from './shared-transition';
|
||||
import { Transition, iosMatchLayerProperties, iosPrintRect, iosSnapshotView } from '.';
|
||||
import { SharedTransition, SharedTransitionAnimationType, DEFAULT_DURATION } from './shared-transition';
|
||||
|
||||
const printRect = (r: CGRect) => `CGRect(${r.origin.x} ${r.origin.y} ${r.size.width} ${r.size.height})`;
|
||||
const printPoint = (r: CGPoint) => `CGPoint(${r.x} ${r.y})`;
|
||||
const printSize = (r: CGSize) => `CGPoint(${r.width} ${r.height})`;
|
||||
|
||||
const DEFAULT_DURATION = 0.35;
|
||||
|
||||
export class ModalTransition extends Transition implements TransitionType {
|
||||
export class ModalTransition extends Transition {
|
||||
transitionController: ModalTransitionController;
|
||||
presented: UIViewController;
|
||||
presenting: UIViewController;
|
||||
@@ -65,7 +58,7 @@ class ModalTransitionController extends NSObject implements UIViewControllerAnim
|
||||
}
|
||||
|
||||
animateTransition(transitionContext: UIViewControllerContextTransitioning): void {
|
||||
// console.log('FullScreenAnimationController animateTransition:', animationType);
|
||||
// console.log('ModalTransitionController animateTransition:', animationType);
|
||||
const owner = this.owner.deref();
|
||||
if (owner) {
|
||||
// console.log('owner.id:', owner.id);
|
||||
@@ -125,7 +118,7 @@ class ModalTransitionController extends NSObject implements UIViewControllerAnim
|
||||
// in case the image is loaded async, we need to update the snapshot when it changes
|
||||
// todo: remove listener on transition end
|
||||
presentedView.on('imageSourceChange', () => {
|
||||
sharedElementSnapshot.image = snapshotView(presentedSharedElement);
|
||||
sharedElementSnapshot.image = iosSnapshotView(presentedSharedElement);
|
||||
sharedElementSnapshot.tintColor = presentedSharedElement.tintColor;
|
||||
});
|
||||
|
||||
@@ -133,14 +126,14 @@ class ModalTransitionController extends NSObject implements UIViewControllerAnim
|
||||
sharedElementSnapshot.contentMode = presentedSharedElement.contentMode;
|
||||
}
|
||||
|
||||
matchLayerProperties(sharedElementSnapshot, presentingSharedElement);
|
||||
iosMatchLayerProperties(sharedElementSnapshot, presentingSharedElement);
|
||||
sharedElementSnapshot.clipsToBounds = true;
|
||||
// console.log('---> snapshot: ', sharedElementSnapshot);
|
||||
|
||||
const startFrame = presentingSharedElement.convertRectToView(presentingSharedElement.bounds, transitionContext.containerView);
|
||||
const endFrame = presentedSharedElement.convertRectToView(presentedSharedElement.bounds, transitionContext.containerView);
|
||||
sharedElementSnapshot.frame = startFrame;
|
||||
console.log('---> ', presentingView.sharedTransitionTag, ' frame:', printRect(sharedElementSnapshot.frame));
|
||||
console.log('---> ', presentingView.sharedTransitionTag, ' frame:', iosPrintRect(sharedElementSnapshot.frame));
|
||||
|
||||
owner.sharedElements.presenting.push({
|
||||
view: presentingView,
|
||||
@@ -209,13 +202,13 @@ class ModalTransitionController extends NSObject implements UIViewControllerAnim
|
||||
presentingMatch.snapshot.frame = correctedEndFrame;
|
||||
|
||||
// apply view and layer properties to the snapshot view to match the source/presented view
|
||||
matchLayerProperties(presentingMatch.snapshot, presented.view.ios);
|
||||
iosMatchLayerProperties(presentingMatch.snapshot, presented.view.ios);
|
||||
// create a snapshot of the presented view
|
||||
presentingMatch.snapshot.image = snapshotView(presented.view.ios);
|
||||
presentingMatch.snapshot.image = iosSnapshotView(presented.view.ios);
|
||||
// apply correct alpha
|
||||
presentingMatch.snapshot.alpha = presentingMatch.endOpacity;
|
||||
|
||||
console.log(`---> ${presentingMatch.view.sharedTransitionTag} animate to: `, printRect(correctedEndFrame));
|
||||
console.log(`---> ${presentingMatch.view.sharedTransitionTag} animate to: `, iosPrintRect(correctedEndFrame));
|
||||
}
|
||||
console.log(' ');
|
||||
};
|
||||
@@ -284,11 +277,11 @@ class ModalTransitionController extends NSObject implements UIViewControllerAnim
|
||||
const updateFrameDismiss = () => {
|
||||
console.log('3. Dismissing shared elements:');
|
||||
for (const presenting of owner.sharedElements.presenting) {
|
||||
matchLayerProperties(presenting.snapshot, presenting.view.ios);
|
||||
iosMatchLayerProperties(presenting.snapshot, presenting.view.ios);
|
||||
presenting.snapshot.frame = presenting.startFrame;
|
||||
presenting.snapshot.alpha = presenting.startOpacity;
|
||||
|
||||
console.log(`---> ${presenting.view.sharedTransitionTag} animate to: `, printRect(presenting.startFrame));
|
||||
console.log(`---> ${presenting.view.sharedTransitionTag} animate to: `, iosPrintRect(presenting.startFrame));
|
||||
}
|
||||
console.log(' ');
|
||||
};
|
||||
@@ -320,36 +313,3 @@ class ModalTransitionController extends NSObject implements UIViewControllerAnim
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function snapshotView(view: UIView): UIImage {
|
||||
if (view instanceof UIImageView) {
|
||||
return view.image;
|
||||
}
|
||||
// console.log('snapshotView view.frame:', printRect(view.frame));
|
||||
UIGraphicsBeginImageContextWithOptions(CGSizeMake(view.frame.size.width, view.frame.size.height), false, Screen.mainScreen.scale);
|
||||
view.layer.renderInContext(UIGraphicsGetCurrentContext());
|
||||
const image = UIGraphicsGetImageFromCurrentImageContext();
|
||||
UIGraphicsEndImageContext();
|
||||
return image;
|
||||
}
|
||||
|
||||
// todo: figure out all the properties/layer properties that we want to transition automatically
|
||||
// note: some need to be done at various stages of the animation and are not in here. (e.g. alpha)
|
||||
function matchLayerProperties(view: UIView, toView: UIView) {
|
||||
const viewPropertiesToMatch: Array<keyof UIView> = ['backgroundColor'];
|
||||
const layerPropertiesToMatch: Array<keyof CALayer> = ['cornerRadius', 'borderWidth', 'borderColor'];
|
||||
|
||||
viewPropertiesToMatch.forEach((property) => {
|
||||
if (view[property] !== toView[property]) {
|
||||
console.log('| -- matching view property:', property);
|
||||
view[property as any] = toView[property];
|
||||
}
|
||||
});
|
||||
|
||||
layerPropertiesToMatch.forEach((property) => {
|
||||
if (view.layer[property] !== toView.layer[property]) {
|
||||
console.log('| -- matching layer property:', property);
|
||||
view.layer[property as any] = toView.layer[property];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
124
packages/core/ui/transition/page-transition.android.ts
Normal file
124
packages/core/ui/transition/page-transition.android.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import { Transition } from '.';
|
||||
import { Screen } from '../../platform';
|
||||
import lazy from '../../utils/lazy';
|
||||
|
||||
const screenWidth = lazy(() => Screen.mainScreen.widthPixels);
|
||||
const screenHeight = lazy(() => Screen.mainScreen.heightPixels);
|
||||
|
||||
export class PageTransition extends Transition {
|
||||
private _direction: string;
|
||||
|
||||
constructor(direction: string, duration: number, curve: any) {
|
||||
super(duration, curve);
|
||||
this._direction = direction;
|
||||
}
|
||||
|
||||
public createAndroidAnimator(transitionType: string): android.animation.Animator {
|
||||
const translationValues = Array.create('float', 2);
|
||||
switch (this._direction) {
|
||||
case 'left':
|
||||
switch (transitionType) {
|
||||
case Transition.AndroidTransitionType.enter:
|
||||
translationValues[0] = screenWidth();
|
||||
translationValues[1] = 0;
|
||||
break;
|
||||
case Transition.AndroidTransitionType.exit:
|
||||
translationValues[0] = 0;
|
||||
translationValues[1] = -screenWidth();
|
||||
break;
|
||||
case Transition.AndroidTransitionType.popEnter:
|
||||
translationValues[0] = -screenWidth();
|
||||
translationValues[1] = 0;
|
||||
break;
|
||||
case Transition.AndroidTransitionType.popExit:
|
||||
translationValues[0] = 0;
|
||||
translationValues[1] = screenWidth();
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 'right':
|
||||
switch (transitionType) {
|
||||
case Transition.AndroidTransitionType.enter:
|
||||
translationValues[0] = -screenWidth();
|
||||
translationValues[1] = 0;
|
||||
break;
|
||||
case Transition.AndroidTransitionType.exit:
|
||||
translationValues[0] = 0;
|
||||
translationValues[1] = screenWidth();
|
||||
break;
|
||||
case Transition.AndroidTransitionType.popEnter:
|
||||
translationValues[0] = screenWidth();
|
||||
translationValues[1] = 0;
|
||||
break;
|
||||
case Transition.AndroidTransitionType.popExit:
|
||||
translationValues[0] = 0;
|
||||
translationValues[1] = -screenWidth();
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 'top':
|
||||
switch (transitionType) {
|
||||
case Transition.AndroidTransitionType.enter:
|
||||
translationValues[0] = screenHeight();
|
||||
translationValues[1] = 0;
|
||||
break;
|
||||
case Transition.AndroidTransitionType.exit:
|
||||
translationValues[0] = 0;
|
||||
translationValues[1] = -screenHeight();
|
||||
break;
|
||||
case Transition.AndroidTransitionType.popEnter:
|
||||
translationValues[0] = -screenHeight();
|
||||
translationValues[1] = 0;
|
||||
break;
|
||||
case Transition.AndroidTransitionType.popExit:
|
||||
translationValues[0] = 0;
|
||||
translationValues[1] = screenHeight();
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 'bottom':
|
||||
switch (transitionType) {
|
||||
case Transition.AndroidTransitionType.enter:
|
||||
translationValues[0] = -screenHeight();
|
||||
translationValues[1] = 0;
|
||||
break;
|
||||
case Transition.AndroidTransitionType.exit:
|
||||
translationValues[0] = 0;
|
||||
translationValues[1] = screenHeight();
|
||||
break;
|
||||
case Transition.AndroidTransitionType.popEnter:
|
||||
translationValues[0] = screenHeight();
|
||||
translationValues[1] = 0;
|
||||
break;
|
||||
case Transition.AndroidTransitionType.popExit:
|
||||
translationValues[0] = 0;
|
||||
translationValues[1] = -screenHeight();
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
let prop;
|
||||
|
||||
if (this._direction === 'left' || this._direction === 'right') {
|
||||
prop = 'translationX';
|
||||
} else {
|
||||
prop = 'translationY';
|
||||
}
|
||||
|
||||
const animator = android.animation.ObjectAnimator.ofFloat(null, prop, translationValues);
|
||||
const duration = this.getDuration();
|
||||
if (duration !== undefined) {
|
||||
animator.setDuration(duration);
|
||||
}
|
||||
animator.setInterpolator(this.getCurve());
|
||||
|
||||
const animatorSet = new android.animation.AnimatorSet();
|
||||
animatorSet.play(animator);
|
||||
return animatorSet;
|
||||
}
|
||||
|
||||
public toString(): string {
|
||||
return `${super.toString()} ${this._direction}`;
|
||||
}
|
||||
}
|
||||
2
packages/core/ui/transition/page-transition.d.ts
vendored
Normal file
2
packages/core/ui/transition/page-transition.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
import { Transition } from '.';
|
||||
export declare class PageTransition extends Transition {}
|
||||
316
packages/core/ui/transition/page-transition.ios.ts
Normal file
316
packages/core/ui/transition/page-transition.ios.ts
Normal file
@@ -0,0 +1,316 @@
|
||||
import { querySelectorAll } from '../core/view-base';
|
||||
import type { View } from '../core/view';
|
||||
import { Transition, iosMatchLayerProperties, iosSnapshotView, iosPrintRect } from '.';
|
||||
import { Screen } from '../../platform';
|
||||
import { SharedTransition, SharedTransitionAnimationType, DEFAULT_DURATION } from './shared-transition';
|
||||
|
||||
export class PageTransition extends Transition {
|
||||
transitionController: PageTransitionController;
|
||||
presented: UIViewController;
|
||||
presenting: UIViewController;
|
||||
sharedElements: {
|
||||
presented?: Array<{ view: View; startFrame?: CGRect; endFrame?: CGRect; snapshot?: UIImageView; startOpacity?: number; endOpacity?: number }>;
|
||||
presenting?: Array<{ view: View; startFrame: CGRect; endFrame?: CGRect; snapshot?: UIImageView; startOpacity?: number; endOpacity?: number }>;
|
||||
};
|
||||
|
||||
iosPresentedController(presented: UIViewController, presenting: UIViewController, source: UIViewController): UIViewControllerAnimatedTransitioning {
|
||||
console.log('-- iosPresentedController --');
|
||||
this.transitionController = PageTransitionController.initWithOwner(new WeakRef(this));
|
||||
this.presented = presented;
|
||||
return this.transitionController;
|
||||
}
|
||||
|
||||
iosDismissedController(dismissed: UIViewController): UIViewControllerAnimatedTransitioning {
|
||||
console.log('-- iosDismissedController --');
|
||||
this.transitionController = PageTransitionController.initWithOwner(new WeakRef(this));
|
||||
this.presented = dismissed;
|
||||
return this.transitionController;
|
||||
}
|
||||
|
||||
iosInteractionDismiss(animator: UIViewControllerAnimatedTransitioning): UIViewControllerInteractiveTransitioning {
|
||||
console.log('-- iosInteractionDismiss --');
|
||||
return null;
|
||||
}
|
||||
|
||||
iosInteractionPresented(animator: UIViewControllerAnimatedTransitioning): UIViewControllerInteractiveTransitioning {
|
||||
console.log('-- iosInteractionPresented --');
|
||||
return null;
|
||||
}
|
||||
|
||||
iosPresentedViewController(presented: UIViewController, presenting: UIViewController, source: UIViewController): UIPresentationController {
|
||||
console.log('-- iosPresentedViewController --');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@NativeClass()
|
||||
class PageTransitionController extends NSObject implements UIViewControllerAnimatedTransitioning {
|
||||
static ObjCProtocols = [UIViewControllerAnimatedTransitioning];
|
||||
owner: WeakRef<PageTransition>;
|
||||
|
||||
static initWithOwner(owner: WeakRef<PageTransition>) {
|
||||
const ctrl = <PageTransitionController>PageTransitionController.new();
|
||||
ctrl.owner = owner;
|
||||
return ctrl;
|
||||
}
|
||||
|
||||
transitionDuration(transitionContext: UIViewControllerContextTransitioning): number {
|
||||
return DEFAULT_DURATION;
|
||||
}
|
||||
|
||||
animateTransition(transitionContext: UIViewControllerContextTransitioning): void {
|
||||
console.log('PageTransitionController animateTransition');
|
||||
const owner = this.owner.deref();
|
||||
if (owner) {
|
||||
// console.log('owner.id:', owner.id);
|
||||
const state = SharedTransition.getState(owner.id);
|
||||
if (!state) {
|
||||
return;
|
||||
}
|
||||
console.log('state.activeType:', state.activeType);
|
||||
switch (state.activeType) {
|
||||
case SharedTransitionAnimationType.present: {
|
||||
console.log('-- Transition present --');
|
||||
|
||||
transitionContext.containerView.addSubview(owner.presented.view);
|
||||
owner.presented.view.layoutIfNeeded();
|
||||
|
||||
// 1. Presented view: gather all sharedTransitionTag views
|
||||
const presentedSharedElements = <Array<View>>querySelectorAll(state.toPage, 'sharedTransitionTag');
|
||||
// console.log('presented sharedTransitionTag total:', presentedSharedElements.length);
|
||||
|
||||
// 2. Presenting view: gather all sharedTransitionTag views
|
||||
const presentingSharedElements = <Array<View>>querySelectorAll(state.page, 'sharedTransitionTag');
|
||||
// console.log('presenting sharedTransitionTag total:', presentingSharedElements.length);
|
||||
|
||||
// 3. only handle sharedTransitionTag on presenting which match presented
|
||||
const presentedTags = presentedSharedElements.map((v) => v.sharedTransitionTag);
|
||||
const presentingViewsToHandle = presentingSharedElements.filter((v) => presentedTags.includes(v.sharedTransitionTag));
|
||||
|
||||
console.log(' ');
|
||||
console.log(
|
||||
`1. Found sharedTransitionTags to animate:`,
|
||||
presentingViewsToHandle.map((v) => v.sharedTransitionTag)
|
||||
);
|
||||
|
||||
console.log(`2. Take snapshots of shared elements and position them based on presenting view:`);
|
||||
|
||||
for (const presentingView of presentingViewsToHandle) {
|
||||
if (!owner.sharedElements) {
|
||||
owner.sharedElements = {
|
||||
presented: [],
|
||||
presenting: [],
|
||||
};
|
||||
}
|
||||
const presentingSharedElement = presentingView.ios;
|
||||
// console.log('fromTarget instanceof UIImageView:', fromTarget instanceof UIImageView)
|
||||
|
||||
// TODO: discuss whether we should check if UIImage/UIImageView type to always snapshot images or if other view types could be duped/added vs. snapshotted
|
||||
// Note: snapshot may be most efficient/simple
|
||||
// console.log('---> ', presentingView.sharedTransitionTag, ': ', presentingSharedElement)
|
||||
|
||||
const presentedView = presentedSharedElements.find((v) => v.sharedTransitionTag === presentingView.sharedTransitionTag);
|
||||
const presentedSharedElement = presentedView.ios;
|
||||
|
||||
const sharedElementSnapshot = UIImageView.alloc().init(); // WithImage(snapshotView(presentedSharedElement));
|
||||
|
||||
// treat images differently...
|
||||
if (presentedSharedElement instanceof UIImageView) {
|
||||
// in case the image is loaded async, we need to update the snapshot when it changes
|
||||
// todo: remove listener on transition end
|
||||
presentedView.on('imageSourceChange', () => {
|
||||
sharedElementSnapshot.image = iosSnapshotView(presentedSharedElement);
|
||||
sharedElementSnapshot.tintColor = presentedSharedElement.tintColor;
|
||||
});
|
||||
|
||||
sharedElementSnapshot.tintColor = presentedSharedElement.tintColor;
|
||||
sharedElementSnapshot.contentMode = presentedSharedElement.contentMode;
|
||||
}
|
||||
|
||||
iosMatchLayerProperties(sharedElementSnapshot, presentingSharedElement);
|
||||
sharedElementSnapshot.clipsToBounds = true;
|
||||
// console.log('---> snapshot: ', sharedElementSnapshot);
|
||||
|
||||
const startFrame = presentingSharedElement.convertRectToView(presentingSharedElement.bounds, transitionContext.containerView);
|
||||
const endFrame = presentedSharedElement.convertRectToView(presentedSharedElement.bounds, transitionContext.containerView);
|
||||
sharedElementSnapshot.frame = startFrame;
|
||||
console.log('---> ', presentingView.sharedTransitionTag, ' frame:', iosPrintRect(sharedElementSnapshot.frame));
|
||||
|
||||
owner.sharedElements.presenting.push({
|
||||
view: presentingView,
|
||||
startFrame,
|
||||
endFrame,
|
||||
snapshot: sharedElementSnapshot,
|
||||
startOpacity: presentingView.opacity,
|
||||
endOpacity: presentedView.opacity,
|
||||
});
|
||||
owner.sharedElements.presented.push({
|
||||
view: presentedView,
|
||||
startFrame: endFrame,
|
||||
endFrame: startFrame,
|
||||
startOpacity: presentedView.opacity,
|
||||
endOpacity: presentingView.opacity,
|
||||
});
|
||||
|
||||
// set initial opacity to match the source view opacity
|
||||
sharedElementSnapshot.alpha = presentingView.opacity;
|
||||
// hide both while animating within the transition context
|
||||
presentingView.opacity = 0;
|
||||
presentedView.opacity = 0;
|
||||
// add snapshot to animate
|
||||
transitionContext.containerView.addSubview(sharedElementSnapshot);
|
||||
}
|
||||
|
||||
const cleanupPresent = () => {
|
||||
for (const presented of owner.sharedElements.presented) {
|
||||
presented.view.opacity = presented.startOpacity;
|
||||
}
|
||||
|
||||
// TODO: Discuss with Igor whether this is necessary
|
||||
// potential this could help smooth some shared element transitions
|
||||
UIView.animateWithDurationAnimationsCompletion(
|
||||
0, // Igor: disabled for now, we'll talk about this and decide
|
||||
() => {
|
||||
for (const presenting of owner.sharedElements.presenting) {
|
||||
presenting.snapshot.alpha = presenting.endOpacity;
|
||||
}
|
||||
},
|
||||
() => {
|
||||
for (const presenting of owner.sharedElements.presenting) {
|
||||
presenting.snapshot.removeFromSuperview();
|
||||
}
|
||||
SharedTransition.updateState({
|
||||
id: owner.id,
|
||||
activeType: SharedTransitionAnimationType.dismiss,
|
||||
});
|
||||
transitionContext.completeTransition(true);
|
||||
}
|
||||
);
|
||||
};
|
||||
const updateFramePresent = () => {
|
||||
// https://stackoverflow.com/a/27997678/1418981
|
||||
// In order to have proper layout. Seems mostly needed when presenting.
|
||||
// For instance during presentation, destination view doesn't account navigation bar height.
|
||||
// Not sure if best to leave all the time?
|
||||
// owner.presented.view.setNeedsLayout();
|
||||
// owner.presented.view.layoutIfNeeded();
|
||||
console.log('3. Animating shared elements:');
|
||||
for (const presented of owner.sharedElements.presented) {
|
||||
const presentingMatch = owner.sharedElements.presenting.find((v) => v.view.sharedTransitionTag === presented.view.sharedTransitionTag);
|
||||
// Workaround wrong origin due ongoing layout process.
|
||||
const updatedEndFrame = presented.view.ios.convertRectToView(presented.view.ios.bounds, transitionContext.containerView);
|
||||
const correctedEndFrame = CGRectMake(updatedEndFrame.origin.x, updatedEndFrame.origin.y, presentingMatch.endFrame.size.width, presentingMatch.endFrame.size.height);
|
||||
presentingMatch.snapshot.frame = correctedEndFrame;
|
||||
|
||||
// apply view and layer properties to the snapshot view to match the source/presented view
|
||||
iosMatchLayerProperties(presentingMatch.snapshot, presented.view.ios);
|
||||
// create a snapshot of the presented view
|
||||
presentingMatch.snapshot.image = iosSnapshotView(presented.view.ios);
|
||||
// apply correct alpha
|
||||
presentingMatch.snapshot.alpha = presentingMatch.endOpacity;
|
||||
|
||||
console.log(`---> ${presentingMatch.view.sharedTransitionTag} animate to: `, iosPrintRect(correctedEndFrame));
|
||||
}
|
||||
console.log(' ');
|
||||
};
|
||||
|
||||
// starting page properties
|
||||
owner.presented.view.alpha = typeof state.incomingViewStart?.opacity === 'number' ? state.incomingViewStart?.opacity : 0;
|
||||
|
||||
const startX = typeof state.incomingViewStart?.x === 'number' ? state.incomingViewStart?.x : 0;
|
||||
const startY = typeof state.incomingViewStart?.y === 'number' ? state.incomingViewStart?.y : Screen.mainScreen.heightDIPs;
|
||||
const startWidth = typeof state.incomingViewStart?.width === 'number' ? state.incomingViewStart?.width : Screen.mainScreen.widthDIPs;
|
||||
const startHeight = typeof state.incomingViewStart?.height === 'number' ? state.incomingViewStart?.height : Screen.mainScreen.heightDIPs;
|
||||
owner.presented.view.frame = CGRectMake(startX, startY, startWidth, startHeight);
|
||||
|
||||
UIView.animateWithDurationDelayUsingSpringWithDampingInitialSpringVelocityOptionsAnimationsCompletion(
|
||||
typeof state.incomingViewStart?.duration === 'number' ? state.incomingViewStart?.duration / 1000 : DEFAULT_DURATION,
|
||||
0,
|
||||
0.5,
|
||||
3,
|
||||
UIViewAnimationOptions.CurveEaseInOut,
|
||||
() => {
|
||||
// animate page properties to the following:
|
||||
owner.presented.view.alpha = typeof state.incomingViewEnd?.opacity === 'number' ? state.incomingViewEnd?.opacity : 1;
|
||||
|
||||
const endX = typeof state.incomingViewEnd?.x === 'number' ? state.incomingViewEnd?.x : 0;
|
||||
const endY = typeof state.incomingViewEnd?.y === 'number' ? state.incomingViewEnd?.y : 0;
|
||||
const endWidth = typeof state.incomingViewEnd?.width === 'number' ? state.incomingViewEnd?.width : Screen.mainScreen.widthDIPs;
|
||||
const endHeight = typeof state.incomingViewEnd?.height === 'number' ? state.incomingViewEnd?.height : Screen.mainScreen.heightDIPs;
|
||||
owner.presented.view.frame = CGRectMake(endX, endY, endWidth, endHeight);
|
||||
|
||||
updateFramePresent();
|
||||
},
|
||||
() => {
|
||||
cleanupPresent();
|
||||
}
|
||||
);
|
||||
break;
|
||||
}
|
||||
case SharedTransitionAnimationType.dismiss: {
|
||||
console.log('-- Transition dismiss --');
|
||||
|
||||
// console.log('transitionContext.containerView.subviews.count:', transitionContext.containerView.subviews.count);
|
||||
|
||||
console.log(' ');
|
||||
console.log(
|
||||
`1. Dismiss sharedTransitionTags to animate:`,
|
||||
owner.sharedElements.presented.map((p) => p.view.sharedTransitionTag)
|
||||
);
|
||||
|
||||
console.log(`2. Add back previously stored sharedElements to dismiss:`);
|
||||
|
||||
for (const p of owner.sharedElements.presented) {
|
||||
p.view.opacity = 0;
|
||||
}
|
||||
for (const p of owner.sharedElements.presenting) {
|
||||
p.snapshot.alpha = p.endOpacity;
|
||||
transitionContext.containerView.addSubview(p.snapshot);
|
||||
}
|
||||
|
||||
const cleanupDismiss = () => {
|
||||
for (const presenting of owner.sharedElements.presenting) {
|
||||
presenting.view.opacity = presenting.startOpacity;
|
||||
}
|
||||
SharedTransition.finishState(owner.id);
|
||||
transitionContext.completeTransition(true);
|
||||
};
|
||||
const updateFrameDismiss = () => {
|
||||
console.log('3. Dismissing shared elements:');
|
||||
for (const presenting of owner.sharedElements.presenting) {
|
||||
iosMatchLayerProperties(presenting.snapshot, presenting.view.ios);
|
||||
presenting.snapshot.frame = presenting.startFrame;
|
||||
presenting.snapshot.alpha = presenting.startOpacity;
|
||||
|
||||
console.log(`---> ${presenting.view.sharedTransitionTag} animate to: `, iosPrintRect(presenting.startFrame));
|
||||
}
|
||||
console.log(' ');
|
||||
};
|
||||
|
||||
UIView.animateWithDurationDelayUsingSpringWithDampingInitialSpringVelocityOptionsAnimationsCompletion(
|
||||
typeof state.dismissViewEnd?.duration === 'number' ? state.dismissViewEnd?.duration / 1000 : DEFAULT_DURATION,
|
||||
0,
|
||||
0.5,
|
||||
3,
|
||||
UIViewAnimationOptions.CurveEaseInOut,
|
||||
() => {
|
||||
owner.presented.view.alpha = typeof state.dismissViewEnd?.opacity === 'number' ? state.dismissViewEnd?.opacity : 0;
|
||||
|
||||
const endX = typeof state.dismissViewEnd?.x === 'number' ? state.dismissViewEnd?.x : 0;
|
||||
const endY = typeof state.dismissViewEnd?.y === 'number' ? state.dismissViewEnd?.y : Screen.mainScreen.heightDIPs;
|
||||
const endWidth = typeof state.dismissViewEnd?.width === 'number' ? state.dismissViewEnd?.width : Screen.mainScreen.widthDIPs;
|
||||
const endHeight = typeof state.dismissViewEnd?.height === 'number' ? state.dismissViewEnd?.height : Screen.mainScreen.heightDIPs;
|
||||
owner.presented.view.frame = CGRectMake(endX, endY, endWidth, endHeight);
|
||||
|
||||
updateFrameDismiss();
|
||||
},
|
||||
() => {
|
||||
cleanupDismiss();
|
||||
}
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import type { Transition } from '.';
|
||||
// import type { View } from '../../core/view';
|
||||
import type { Page } from '../page';
|
||||
|
||||
export const DEFAULT_DURATION = 0.35;
|
||||
// always increment when adding new transitions to be able to track their state
|
||||
export enum SharedTransitionAnimationType {
|
||||
present,
|
||||
|
||||
Reference in New Issue
Block a user