mirror of
https://github.com/NativeScript/NativeScript.git
synced 2025-08-14 10:01:08 +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,
|
|
};
|
|
}
|