mirror of
				https://github.com/NativeScript/NativeScript.git
				synced 2025-11-04 12:58:38 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			302 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			302 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
import type { View } from '../core/view';
 | 
						|
import { ViewBase } from '../core/view-base';
 | 
						|
import { BackstackEntry } from '../frame';
 | 
						|
import { isNumber } from '../../utils/types';
 | 
						|
import { Transition } from '.';
 | 
						|
import { getRectFromProps, SharedTransition, SharedTransitionAnimationType, SharedTransitionEventData } from './shared-transition';
 | 
						|
import { ImageSource } from '../../image-source';
 | 
						|
import { ContentView } from '../content-view';
 | 
						|
import { GridLayout } from '../layouts/grid-layout';
 | 
						|
import { Screen } from '../../platform';
 | 
						|
import { ExpandedEntry } from '../frame/fragment.transitions.android';
 | 
						|
import { android as AndroidUtils } from '../../utils/native-helper';
 | 
						|
// import { Image } from '../image';
 | 
						|
 | 
						|
@NativeClass
 | 
						|
class SnapshotViewGroup extends android.view.ViewGroup {
 | 
						|
	constructor(context: android.content.Context) {
 | 
						|
		super(context);
 | 
						|
		return global.__native(this);
 | 
						|
	}
 | 
						|
	public onMeasure(): void {
 | 
						|
		this.setMeasuredDimension(0, 0);
 | 
						|
	}
 | 
						|
	public onLayout(): void {
 | 
						|
		//
 | 
						|
	}
 | 
						|
}
 | 
						|
class ContentViewSnapshot extends ContentView {
 | 
						|
	createNativeView() {
 | 
						|
		return new SnapshotViewGroup(this._context);
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
@NativeClass
 | 
						|
class CustomSpringInterpolator extends android.view.animation.AnticipateOvershootInterpolator {
 | 
						|
	getInterpolation(input: number) {
 | 
						|
		// Note: we speed up the interpolation by 10% to fix the issue with the transition not being finished
 | 
						|
		// and the views shifting from their intended final position...
 | 
						|
		// this is really just a workaround and should be fixed properly once we
 | 
						|
		// can figure out the root cause of the issue.
 | 
						|
		const res = super.getInterpolation(input) * 1.1;
 | 
						|
 | 
						|
		if (res > 1) {
 | 
						|
			return float(1);
 | 
						|
		}
 | 
						|
 | 
						|
		return float(res);
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
@NativeClass
 | 
						|
class CustomLinearInterpolator extends android.view.animation.LinearInterpolator {
 | 
						|
	getInterpolation(input: number) {
 | 
						|
		// Note: we speed up the interpolation by 10% to fix the issue with the transition not being finished
 | 
						|
		// and the views shifting from their intended final position...
 | 
						|
		// this is really just a workaround and should be fixed properly once we
 | 
						|
		// can figure out the root cause of the issue.
 | 
						|
		const res = super.getInterpolation(input) * 1.1;
 | 
						|
 | 
						|
		if (res > 1) {
 | 
						|
			return float(1);
 | 
						|
		}
 | 
						|
 | 
						|
		return float(res);
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
function setTransitionName(view: ViewBase) {
 | 
						|
	if (!view?.sharedTransitionTag) {
 | 
						|
		return;
 | 
						|
	}
 | 
						|
	try {
 | 
						|
		androidx.core.view.ViewCompat.setTransitionName(view.nativeView, view.sharedTransitionTag);
 | 
						|
	} catch (err) {
 | 
						|
		// ignore
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
export class PageTransition extends Transition {
 | 
						|
	constructor(
 | 
						|
		duration?: number,
 | 
						|
		curve?: any,
 | 
						|
		private pageLoadedTimeout: number = 0,
 | 
						|
	) {
 | 
						|
		// disable custom curves until we can fix the issue with the animation not completing
 | 
						|
		if (curve) {
 | 
						|
			console.warn('PageTransition does not support custom curves at the moment. The passed in curve will be ignored.');
 | 
						|
		}
 | 
						|
		if (typeof duration !== 'number') {
 | 
						|
			duration = 500;
 | 
						|
		}
 | 
						|
		super(duration);
 | 
						|
	}
 | 
						|
 | 
						|
	createAndroidAnimator(transitionType: string) {
 | 
						|
		const state = SharedTransition.getState(this.id);
 | 
						|
		const pageStart = state.pageStart;
 | 
						|
		const startFrame = getRectFromProps(pageStart, {
 | 
						|
			x: 0,
 | 
						|
			y: 0,
 | 
						|
			width: Screen.mainScreen.widthPixels,
 | 
						|
			height: Screen.mainScreen.heightPixels,
 | 
						|
		});
 | 
						|
		const pageEnd = state.pageEnd;
 | 
						|
		const endFrame = getRectFromProps(pageEnd);
 | 
						|
		const pageReturn = state.pageReturn;
 | 
						|
		const returnFrame = getRectFromProps(pageReturn);
 | 
						|
 | 
						|
		let customDuration = -1;
 | 
						|
		if (state.activeType === SharedTransitionAnimationType.present && isNumber(pageEnd?.duration)) {
 | 
						|
			customDuration = pageEnd.duration;
 | 
						|
		} else if (isNumber(state.pageReturn?.duration)) {
 | 
						|
			customDuration = state.pageReturn.duration;
 | 
						|
		}
 | 
						|
 | 
						|
		const animationSet = new android.animation.AnimatorSet();
 | 
						|
		animationSet.setDuration(customDuration > -1 ? customDuration : this.getDuration());
 | 
						|
 | 
						|
		const alphaValues = Array.create('float', 2);
 | 
						|
		const translationXValues = Array.create('float', 2);
 | 
						|
		const translationYValues = Array.create('float', 2);
 | 
						|
		switch (transitionType) {
 | 
						|
			case Transition.AndroidTransitionType.enter:
 | 
						|
				// incoming page (to)
 | 
						|
				alphaValues[0] = isNumber(pageStart?.opacity) ? pageStart?.opacity : 0;
 | 
						|
				alphaValues[1] = isNumber(pageEnd?.opacity) ? pageEnd?.opacity : 1;
 | 
						|
				translationYValues[0] = startFrame.y;
 | 
						|
				translationYValues[1] = endFrame.y;
 | 
						|
				translationXValues[0] = startFrame.x;
 | 
						|
				translationXValues[1] = endFrame.x;
 | 
						|
				break;
 | 
						|
			case Transition.AndroidTransitionType.exit:
 | 
						|
				// current page (from)
 | 
						|
				alphaValues[0] = 1;
 | 
						|
				alphaValues[1] = 0;
 | 
						|
				translationYValues[0] = 0;
 | 
						|
				translationYValues[1] = 0;
 | 
						|
				break;
 | 
						|
			case Transition.AndroidTransitionType.popEnter:
 | 
						|
				// current page (returning to)
 | 
						|
				alphaValues[0] = 0;
 | 
						|
				alphaValues[1] = 1;
 | 
						|
				break;
 | 
						|
			case Transition.AndroidTransitionType.popExit:
 | 
						|
				// removing page (to)
 | 
						|
				alphaValues[0] = isNumber(pageEnd?.opacity) ? pageEnd?.opacity : 1;
 | 
						|
				alphaValues[1] = isNumber(pageStart?.opacity) ? pageStart?.opacity : 0;
 | 
						|
				translationYValues[0] = endFrame.y;
 | 
						|
				translationYValues[1] = startFrame.y;
 | 
						|
				translationXValues[0] = endFrame.x;
 | 
						|
				translationXValues[1] = startFrame.x;
 | 
						|
				break;
 | 
						|
		}
 | 
						|
		const properties = {
 | 
						|
			alpha: alphaValues,
 | 
						|
			translationX: translationXValues,
 | 
						|
			translationY: translationYValues,
 | 
						|
		};
 | 
						|
 | 
						|
		const animations = new java.util.HashSet<any>();
 | 
						|
		for (const prop in properties) {
 | 
						|
			// console.log(prop, ' ', properties[prop][1]);
 | 
						|
			const animator = android.animation.ObjectAnimator.ofFloat(null, prop, properties[prop]);
 | 
						|
			if (customDuration) {
 | 
						|
				// duration always overrides default spring
 | 
						|
				animator.setInterpolator(new CustomLinearInterpolator());
 | 
						|
			} else {
 | 
						|
				animator.setInterpolator(new CustomSpringInterpolator());
 | 
						|
			}
 | 
						|
			animations.add(animator);
 | 
						|
		}
 | 
						|
 | 
						|
		animationSet.playTogether(animations);
 | 
						|
 | 
						|
		return animationSet;
 | 
						|
	}
 | 
						|
 | 
						|
	androidFragmentTransactionCallback(fragmentTransaction: androidx.fragment.app.FragmentTransaction, currentEntry: ExpandedEntry, newEntry: BackstackEntry) {
 | 
						|
		const fromPage = currentEntry.resolvedPage;
 | 
						|
		const toPage = newEntry.resolvedPage;
 | 
						|
		const newFragment: androidx.fragment.app.Fragment = newEntry.fragment;
 | 
						|
		const state = SharedTransition.getState(this.id);
 | 
						|
		if (!state) {
 | 
						|
			// when navigating transition is set on the currentEntry but never cleaned up
 | 
						|
			// which means that on a next navigation forward (without transition) and back
 | 
						|
			// we will go here with an empty state!
 | 
						|
			currentEntry.transition = null;
 | 
						|
			return;
 | 
						|
		}
 | 
						|
 | 
						|
		const pageEnd = state.pageEnd;
 | 
						|
 | 
						|
		//we can't look for presented right now as the toPage might not be loaded
 | 
						|
		// and thus some views like ListView/Pager... might not have loaded their "children"
 | 
						|
		// presented will be handled in loaded event of toPage
 | 
						|
		const { presenting } = SharedTransition.getSharedElements(fromPage, toPage);
 | 
						|
 | 
						|
		// Note: we can enhance android more over time with element targeting across different screens
 | 
						|
		// const pageStart = state.pageStart;
 | 
						|
		// const pageEndIndependentTags = Object.keys(pageEnd?.sharedTransitionTags || {});
 | 
						|
		// console.log('pageEndIndependentTags:', pageEndIndependentTags);
 | 
						|
		// for (const tag of pageEndIndependentTags) {
 | 
						|
		// 	// only consider start when there's a matching end
 | 
						|
		// 	const pageStartIndependentProps = pageStart?.sharedTransitionTags[tag];
 | 
						|
		// 	if (pageStartIndependentProps) {
 | 
						|
		// 		console.log('pageStartIndependentProps:', tag, pageStartIndependentProps);
 | 
						|
		// 	}
 | 
						|
		// 	const pageEndIndependentProps = pageEnd?.sharedTransitionTags[tag];
 | 
						|
		// 	let independentView = presenting.find((v) => v.sharedTransitionTag === tag);
 | 
						|
		// 	let isPresented = false;
 | 
						|
		// 	if (!independentView) {
 | 
						|
		// 		independentView = presented.find((v) => v.sharedTransitionTag === tag);
 | 
						|
		// 		if (!independentView) {
 | 
						|
		// 			break;
 | 
						|
		// 		}
 | 
						|
		// 		isPresented = true;
 | 
						|
		// 	}
 | 
						|
		// 	if (independentView) {
 | 
						|
		// 		console.log('independentView:', independentView);
 | 
						|
		// 		const imageSource = renderToImageSource(independentView);
 | 
						|
		// 		const image = new Image();
 | 
						|
		// 		image.src = imageSource;
 | 
						|
		// 		const { hostView } = loadViewInBackground(image);
 | 
						|
		// 		(<any>fromPage).addChild(hostView);
 | 
						|
		// 		independentView.opacity = 0;
 | 
						|
		// 	}
 | 
						|
		// }
 | 
						|
		const onPageLoaded = () => {
 | 
						|
			// add a timeout so that Views like ListView / CollectionView can have their children instantiated
 | 
						|
			setTimeout(() => {
 | 
						|
				const { presented } = SharedTransition.getSharedElements(fromPage, toPage);
 | 
						|
				// const sharedElementTags = sharedElements.map((v) => v.sharedTransitionTag);
 | 
						|
				presented.forEach(setTransitionName);
 | 
						|
				newFragment.startPostponedEnterTransition();
 | 
						|
			}, this.pageLoadedTimeout);
 | 
						|
		};
 | 
						|
 | 
						|
		fragmentTransaction.setReorderingAllowed(true);
 | 
						|
 | 
						|
		let customDuration = -1;
 | 
						|
		if (state.activeType === SharedTransitionAnimationType.present && isNumber(pageEnd?.duration)) {
 | 
						|
			customDuration = pageEnd.duration;
 | 
						|
		} else if (isNumber(state.pageReturn?.duration)) {
 | 
						|
			customDuration = state.pageReturn.duration;
 | 
						|
		}
 | 
						|
 | 
						|
		const transitionSet = new androidx.transition.TransitionSet();
 | 
						|
		transitionSet.setDuration(customDuration > -1 ? customDuration : this.getDuration());
 | 
						|
		transitionSet.addTransition(new androidx.transition.ChangeBounds());
 | 
						|
		transitionSet.addTransition(new androidx.transition.ChangeTransform());
 | 
						|
		transitionSet.setOrdering(androidx.transition.TransitionSet.ORDERING_TOGETHER);
 | 
						|
 | 
						|
		if (customDuration) {
 | 
						|
			// duration always overrides default spring
 | 
						|
			transitionSet.setInterpolator(new CustomLinearInterpolator());
 | 
						|
		} else {
 | 
						|
			transitionSet.setInterpolator(new CustomSpringInterpolator());
 | 
						|
		}
 | 
						|
 | 
						|
		// postpone enter until we call "loaded" on the new page
 | 
						|
		newFragment.postponeEnterTransition();
 | 
						|
		newFragment.setSharedElementEnterTransition(transitionSet);
 | 
						|
		newFragment.setSharedElementReturnTransition(transitionSet);
 | 
						|
 | 
						|
		presenting.forEach((v) => {
 | 
						|
			setTransitionName(v);
 | 
						|
			fragmentTransaction.addSharedElement(v.nativeView, v.sharedTransitionTag);
 | 
						|
		});
 | 
						|
		if (toPage.isLoaded) {
 | 
						|
			onPageLoaded();
 | 
						|
		} else {
 | 
						|
			toPage.once('loaded', onPageLoaded);
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
function renderToImageSource(hostView: View): ImageSource {
 | 
						|
	const bitmap = android.graphics.Bitmap.createBitmap(hostView.android.getWidth(), hostView.android.getHeight(), android.graphics.Bitmap.Config.ARGB_8888);
 | 
						|
	const canvas = new android.graphics.Canvas(bitmap);
 | 
						|
	// ensure we start with a blank transparent canvas
 | 
						|
	canvas.drawARGB(0, 0, 0, 0);
 | 
						|
	hostView.android.draw(canvas);
 | 
						|
	return new ImageSource(bitmap);
 | 
						|
}
 | 
						|
 | 
						|
function loadViewInBackground(view: View) {
 | 
						|
	const hiddenHost = new ContentViewSnapshot();
 | 
						|
	const hostView = new GridLayout(); // use a host view to ensure margins are respected
 | 
						|
	hiddenHost.content = hostView;
 | 
						|
	hiddenHost.visibility = 'collapse';
 | 
						|
	hostView.addChild(view);
 | 
						|
	hiddenHost._setupAsRootView(AndroidUtils.getApplicationContext());
 | 
						|
	hiddenHost.callLoaded();
 | 
						|
 | 
						|
	AndroidUtils.getCurrentActivity().addContentView(hiddenHost.android, new android.view.ViewGroup.LayoutParams(0, 0));
 | 
						|
 | 
						|
	return {
 | 
						|
		hiddenHost,
 | 
						|
		hostView,
 | 
						|
	};
 | 
						|
}
 |