diff --git a/packages/core/ui/frame/index.android.ts b/packages/core/ui/frame/index.android.ts index d3dd9807f..fd75499d9 100644 --- a/packages/core/ui/frame/index.android.ts +++ b/packages/core/ui/frame/index.android.ts @@ -22,6 +22,7 @@ import { profile } from '../../profiling'; import { setSuspended } from '../../application/application-common'; import { ad } from '../../utils/native-helper'; import type { ExpandedEntry } from './fragment.transitions.android'; +import { SharedTransition, SharedTransitionAnimationType } from '../transition/shared-transition'; export * from './frame-common'; @@ -467,6 +468,12 @@ export class Frame extends FrameBase { navigationTransition?.instance?.androidFragmentTransactionCallback?.(transaction, currentEntry, newEntry); transaction.commitAllowingStateLoss(); + + if (navigationTransition?.instance) { + SharedTransition.updateState(navigationTransition?.instance?.id, { + activeType: SharedTransitionAnimationType.dismiss, + }); + } } public _goBackCore(backstackEntry: BackstackEntry & ExpandedEntry) { @@ -492,6 +499,10 @@ export class Frame extends FrameBase { backstackEntry.transition?.androidFragmentTransactionCallback?.(transaction, this._currentEntry, backstackEntry); transaction.commitAllowingStateLoss(); + + if (backstackEntry?.transition) { + SharedTransition.finishState(backstackEntry.transition.id); + } } public _removeEntry(removed: BackstackEntry): void { diff --git a/packages/core/ui/transition/index.android.ts b/packages/core/ui/transition/index.android.ts index 78cb9b822..bc1b31784 100644 --- a/packages/core/ui/transition/index.android.ts +++ b/packages/core/ui/transition/index.android.ts @@ -27,6 +27,10 @@ export class Transition implements TransitionType { return this._duration; } + public setDuration(value: number) { + this._duration = value; + } + public getCurve(): android.view.animation.Interpolator { return this._interpolator; } diff --git a/packages/core/ui/transition/index.d.ts b/packages/core/ui/transition/index.d.ts index ca0979e4d..804908540 100644 --- a/packages/core/ui/transition/index.d.ts +++ b/packages/core/ui/transition/index.d.ts @@ -24,6 +24,7 @@ export declare class Transition { 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; + setDuration(value: number): void; getCurve(): any; animateIOSTransition(transitionContext: any /*UIViewControllerContextTransitioning */, fromViewCtrl: any /* UIViewController */, toViewCtrl: any /* UIViewController */, operation: any /* UINavigationControllerOperation */): void; createAndroidAnimator(transitionType: string): any; diff --git a/packages/core/ui/transition/page-transition.android.ts b/packages/core/ui/transition/page-transition.android.ts index 3e18cdf64..0df1e55ac 100644 --- a/packages/core/ui/transition/page-transition.android.ts +++ b/packages/core/ui/transition/page-transition.android.ts @@ -1,14 +1,57 @@ +import type { View } from '../core/view'; import { ViewBase } from '../core/view-base'; import { BackstackEntry } from '../frame'; +import { isNumber } from '../../utils/types'; import { FadeTransition } from './fade-transition'; -import { SharedTransition } from './shared-transition'; +import { SharedTransition, SharedTransitionAnimationType } from './shared-transition'; +import { ImageSource } from '../../image-source'; +import { ContentView } from '../content-view'; +import { GridLayout } from '../layouts/grid-layout'; +import { ad } from '../../utils'; +// import { Image } from '../image'; @NativeClass -class CustomInterpolator extends android.view.animation.AnticipateOvershootInterpolator { +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) { - // HACK: we speed up the interpolation by 10% to fix the issue with the transition not being finished + // 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 hack and should be fixed properly once we + // 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; @@ -21,7 +64,7 @@ class CustomInterpolator extends android.view.animation.AnticipateOvershootInter } function setTransitionName(view: ViewBase) { - if (!view.sharedTransitionTag) { + if (!view?.sharedTransitionTag) { return; } try { @@ -44,26 +87,77 @@ export class PageTransition extends FadeTransition { const fromPage = currentEntry.resolvedPage; const toPage = newEntry.resolvedPage; const newFragment: androidx.fragment.app.Fragment = newEntry.fragment; + const state = SharedTransition.getState(this.id); + const pageEnd = state.pageEnd; - const { presented, presenting } = SharedTransition.getSharedElements(fromPage, toPage); + const { sharedElements, presented, presenting } = SharedTransition.getSharedElements(fromPage, toPage); + const sharedElementTags = sharedElements.map((v) => v.sharedTransitionTag); + if (SharedTransition.DEBUG) { + console.log(` Page: ${state.activeType === SharedTransitionAnimationType.present ? 'Present' : 'Dismiss'}`); + console.log(`1. Found sharedTransitionTags to animate:`, sharedElementTags); + } + + // 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); + // (fromPage).addChild(hostView); + // independentView.opacity = 0; + // } + // } toPage.once('loaded', () => { - presented.forEach(setTransitionName); + presented.filter((v) => sharedElementTags.includes(v.sharedTransitionTag)).forEach(setTransitionName); newFragment.startPostponedEnterTransition(); }); - presenting.forEach((v) => { + sharedElements.forEach((v) => { setTransitionName(v); fragmentTransaction.addSharedElement(v.nativeView, v.sharedTransitionTag); }); 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(this.getDuration()); + transitionSet.setDuration(customDuration || this.getDuration()); transitionSet.addTransition(new androidx.transition.ChangeBounds()); transitionSet.addTransition(new androidx.transition.ChangeTransform()); - transitionSet.setInterpolator(new CustomInterpolator()); + + 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(); @@ -71,3 +165,29 @@ export class PageTransition extends FadeTransition { newFragment.setSharedElementReturnTransition(transitionSet); } } + +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(ad.getApplicationContext()); + hiddenHost.callLoaded(); + + ad.getCurrentActivity().addContentView(hiddenHost.android, new android.view.ViewGroup.LayoutParams(0, 0)); + + return { + hiddenHost, + hostView, + }; +} diff --git a/packages/core/ui/transition/page-transition.ios.ts b/packages/core/ui/transition/page-transition.ios.ts index 0597227c0..14e07f950 100644 --- a/packages/core/ui/transition/page-transition.ios.ts +++ b/packages/core/ui/transition/page-transition.ios.ts @@ -1,10 +1,8 @@ import type { View } from '../core/view'; import { SharedElementSettings, SharedInteractiveState, 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, getRectFromProps, getPageStartDefaultsForType } from './shared-transition'; +import { SharedTransition, DEFAULT_DURATION } from './shared-transition'; import { SharedTransitionHelper } from './shared-transition-helper'; export class PageTransition extends Transition { diff --git a/packages/core/ui/transition/shared-transition.ts b/packages/core/ui/transition/shared-transition.ts index 70ccee6af..b0e85425a 100644 --- a/packages/core/ui/transition/shared-transition.ts +++ b/packages/core/ui/transition/shared-transition.ts @@ -89,6 +89,10 @@ type SharedTransitionPageProperties = SharedProperties & { * Note: When this is defined, it will override spring options and use only linear animation. */ duration?: number; + /** + * (iOS Only) Allow "independent" elements found only on one of the screens to take part in the animation. + * Note: This feature will be brought to Android in a future release. + */ sharedTransitionTags?: { [key: string]: SharedProperties }; /** * Spring animation settings. @@ -116,6 +120,10 @@ export class SharedTransition { instance: transition, activeType: SharedTransitionAnimationType.present, }); + const pageEnd = options.pageEnd; + if (isNumber(pageEnd?.duration)) { + transition.setDuration(pageEnd?.duration); + } return { instance: transition }; } static events() {