From 36dc88bfad99c09a08451cfd6f4de3ac73496d7f Mon Sep 17 00:00:00 2001 From: Eduardo Speroni Date: Fri, 7 Jan 2022 17:26:52 -0300 Subject: [PATCH] fix: better handling of states --- .../src/ui/frame/frame-tests.android.ts | 141 +++++++++++++++++- packages/core/ui/frame/index.android.ts | 104 +++++++------ 2 files changed, 200 insertions(+), 45 deletions(-) diff --git a/apps/automated/src/ui/frame/frame-tests.android.ts b/apps/automated/src/ui/frame/frame-tests.android.ts index 27a049a89..24bef8128 100644 --- a/apps/automated/src/ui/frame/frame-tests.android.ts +++ b/apps/automated/src/ui/frame/frame-tests.android.ts @@ -1,10 +1,10 @@ -import { Frame } from '@nativescript/core/ui/frame'; +import { Frame, GridLayout, Label, Page, Trace, View } from '@nativescript/core'; import * as TKUnit from '../../tk-unit'; import { unsetValue } from '@nativescript/core/ui/core/view'; import { PercentLength } from '@nativescript/core/ui/styling/style-properties'; +import { buildUIAndRunTest } from '../../ui-helper'; export * from './frame-tests-common'; - export function test_percent_width_and_height_set_to_page_support() { let topFrame = Frame.topmost(); let currentPage = topFrame.currentPage; @@ -65,3 +65,140 @@ export function test_percent_margin_set_to_page_support() { TKUnit.assertTrue(PercentLength.equals(currentPage.marginRight, 0)); TKUnit.assertTrue(PercentLength.equals(currentPage.marginBottom, 0)); } + +export function test_nested_frames() { + let topFrame = Frame.topmost(); + let currentPage = topFrame.currentPage; + console.log(Frame.topmost(), currentPage); + class PageAbstraction { + page: Page; + root: GridLayout; + setContent(content: View) { + this.root.insertChild(content, 0); + content.marginTop = 30; + } + } + + const pageFactory = (color: string) => { + const page = new Page(); + page.backgroundColor = color; + const gl = new GridLayout(); + const label = new Label(); + page.on('navigatedTo', () => { + let depth = 0; + let parent = label.parent; + let parentFrame: Frame = null; + while (parent) { + if (parent instanceof Frame) { + parentFrame = parentFrame || parent; + depth++; + } + parent = parent.parent; + } + label.text = `Depth: ${depth} - Page: ${parentFrame?.backStack.length}`; + }); + label.style.zIndex = 999; + gl.insertChild(label, 0); + page.content = gl; + const abs = new PageAbstraction(); + abs.root = gl; + abs.page = page; + + return abs; + }; + + const page1 = pageFactory('red'); + const page2 = pageFactory('blue'); + const page3 = pageFactory('green'); + const parentPage2 = pageFactory('yellow'); + + const frameFactory = () => { + const frame = new Frame(); + frame._popFromFrameStack(); + frame.navigate = function (...args) { + // console.log('navigateTo', args, args[0].create().backgroundColor); + Frame.prototype.navigate.call(frame, ...args); + this._popFromFrameStack(); + }; + frame.goBack = function (...args) { + // console.log('goBack', args); + Frame.prototype.goBack.call(frame, ...args); + this._popFromFrameStack(); + }; + frame.on('navigatingTo', () => ((frame as any).__midNav = true)); + frame.on('navigatedTo', () => ((frame as any).__midNav = false)); + return frame; + }; + + const innerFrame = frameFactory(); + const innerFrame2 = frameFactory(); + page1.setContent(innerFrame2); + + innerFrame.navigate({ + create: () => { + return page1.page; + }, + }); + + innerFrame2.navigate({ + create: () => { + return page2.page; + }, + }); + function validateState(frame: Frame) { + TKUnit.waitUntilReady(() => (frame as any).__midNav === false, 1); + TKUnit.assertTrue(frame._executingContext == null); + TKUnit.assertTrue(frame._currentEntry != null); + if (frame.isLoaded) { + TKUnit.assertTrue(frame._currentEntry.fragment != null); + } + } + // TKUnit.wait(1); + buildUIAndRunTest(innerFrame, ([parentFrame, parentPage]) => { + Trace.enable(); + Trace.setCategories(Trace.categories.concat(Trace.categories.NativeLifecycle, Trace.categories.Transition)); + validateState(innerFrame); + validateState(innerFrame2); + innerFrame2.navigate({ + create: () => { + return page3.page; + }, + }); + validateState(innerFrame); + validateState(innerFrame2); + innerFrame.navigate({ + create: () => parentPage2.page, + }); + validateState(innerFrame); + validateState(innerFrame2); + innerFrame.goBack(); + validateState(innerFrame); + validateState(innerFrame2); + innerFrame2.goBack(); + validateState(innerFrame); + validateState(innerFrame2); + innerFrame.navigate({ + create: () => parentPage2.page, + }); + validateState(innerFrame); + validateState(innerFrame2); + innerFrame.goBack(); + validateState(innerFrame); + validateState(innerFrame2); + + // innerFrame.navigate({ + // create: () => parentPage2.page, + // }); + // TKUnit.wait(1); + // innerFrame.goBack(); + // TKUnit.wait(1); + // TKUnit.assertTrue(innerFrame2._currentEntry.fragment != null); + // TKUnit.wait(5); + }); + + // TKUnit.waitUntilReady(() => { + // return innerFrame.isLayoutValid; + // }, 1); + + TKUnit.wait(10); +} diff --git a/packages/core/ui/frame/index.android.ts b/packages/core/ui/frame/index.android.ts index 17d82f61e..5ab2f6c3c 100644 --- a/packages/core/ui/frame/index.android.ts +++ b/packages/core/ui/frame/index.android.ts @@ -77,6 +77,26 @@ function getAttachListener(): android.view.View.OnAttachStateChangeListener { return attachStateChangeListener; } +function waitUntilReady(lifecycle: androidx.lifecycle.Lifecycle, resolve: (v: boolean) => unknown, targetState = androidx.lifecycle.Lifecycle.State.RESUMED) { + if (!lifecycle || lifecycle.getCurrentState().isAtLeast(targetState)) { + resolve(true); + return; + } + const observer = new androidx.lifecycle.LifecycleEventObserver({ + onStateChanged: (source: androidx.lifecycle.LifecycleOwner, event: androidx.lifecycle.Lifecycle.Event) => { + if (lifecycle.getCurrentState().isAtLeast(targetState)) { + lifecycle.removeObserver(observer); + resolve(true); + } + if (event === androidx.lifecycle.Lifecycle.Event.ON_DESTROY) { + lifecycle.removeObserver(observer); + resolve(false); + } + }, + }); + lifecycle.addObserver(observer); +} + export class Frame extends FrameBase { public _originalBackground: any; private _android: AndroidFrame; @@ -158,7 +178,7 @@ export class Frame extends FrameBase { this._attachedToWindow = false; } - protected async _processNextNavigationEntry(): Promise { + protected _processNextNavigationEntry(): any { // In case activity was destroyed because of back button pressed (e.g. app exit) // and application is restored from recent apps, current fragment isn't recreated. // In this case call _navigateCore in order to recreate the current fragment. @@ -177,27 +197,6 @@ export class Frame extends FrameBase { } let manager = this._getFragmentManager(); - const lifecycle: androidx.lifecycle.Lifecycle = this._getFragmentLifecycle(); - if (lifecycle && !lifecycle.getCurrentState().isAtLeast(androidx.lifecycle.Lifecycle.State.STARTED)) { - const success = await new Promise((resolve) => { - const observer = new androidx.lifecycle.LifecycleEventObserver({ - onStateChanged: (source: androidx.lifecycle.LifecycleOwner, event: androidx.lifecycle.Lifecycle.Event) => { - if (event === androidx.lifecycle.Lifecycle.Event.ON_START) { - lifecycle.removeObserver(observer); - resolve(true); - } - if (event === androidx.lifecycle.Lifecycle.Event.ON_DESTROY) { - lifecycle.removeObserver(observer); - resolve(false); - } - }, - }); - lifecycle.addObserver(observer); - }); - if (!success) { - manager = null; - } - } const entry = this._currentEntry; const isNewEntry = !this._cachedTransitionState || entry !== this._cachedTransitionState.entry; @@ -256,12 +255,24 @@ export class Frame extends FrameBase { this.disposeCurrentFragment(); } - onLoaded(): void { + onLoaded() { if (this._originalBackground) { this.backgroundColor = null; this.backgroundColor = this._originalBackground; this._originalBackground = null; } + // const entry = this._currentEntry || this._executingContext?.entry; + // if (entry && !entry.fragment && entry.fragmentTag) { + // let manager = this._getFragmentManager(); + // const lifecycle: androidx.lifecycle.Lifecycle = this._getFragmentLifecycle(); + // entry.fragment = this.createFragment(entry, entry.fragmentTag); + // waitUntilStarted(lifecycle, () => { + // const transaction = manager.beginTransaction(); + // _updateTransitions(entry); + // transaction.replace(this.containerViewId, entry.fragment, entry.fragmentTag); + // transaction.commitAllowingStateLoss(); + // }); + // } super.onLoaded(); } @@ -414,6 +425,8 @@ export class Frame extends FrameBase { } const manager: androidx.fragment.app.FragmentManager = this._getFragmentManager(); + const lifecycle = this._getFragmentLifecycle(); + const clearHistory = newEntry.entry.clearHistory; const currentEntry = this._currentEntry; @@ -430,30 +443,35 @@ export class Frame extends FrameBase { fragmentId++; const newFragmentTag = `fragment${fragmentId}[${navDepth}]`; const newFragment = this.createFragment(newEntry, newFragmentTag); - const transaction = manager.beginTransaction(); - let animated = currentEntry ? this._getIsAnimatedNavigation(newEntry.entry) : false; - // NOTE: Don't use transition for the initial navigation (same as on iOS) - // On API 21+ transition won't be triggered unless there was at least one - // layout pass so we will wait forever for transitionCompleted handler... - // https://github.com/NativeScript/NativeScript/issues/4895 - let navigationTransition: NavigationTransition; - if (this._currentEntry) { - navigationTransition = this._getNavigationTransition(newEntry.entry); - } else { - navigationTransition = null; - } + waitUntilReady(lifecycle, (started: boolean) => { + if (!started) { + return; + } + const transaction = manager.beginTransaction(); + let animated = currentEntry ? this._getIsAnimatedNavigation(newEntry.entry) : false; + // NOTE: Don't use transition for the initial navigation (same as on iOS) + // On API 21+ transition won't be triggered unless there was at least one + // layout pass so we will wait forever for transitionCompleted handler... + // https://github.com/NativeScript/NativeScript/issues/4895 + let navigationTransition: NavigationTransition; + if (this._currentEntry) { + navigationTransition = this._getNavigationTransition(newEntry.entry); + } else { + navigationTransition = null; + } - const isNestedDefaultTransition = !currentEntry; + const isNestedDefaultTransition = !currentEntry; - _setAndroidFragmentTransitions(animated, navigationTransition, currentEntry, newEntry, this._android.frameId, transaction, isNestedDefaultTransition); + _setAndroidFragmentTransitions(animated, navigationTransition, currentEntry, newEntry, this._android.frameId, transaction, isNestedDefaultTransition); - if (currentEntry && animated && !navigationTransition) { - //TODO: Check whether or not this is still necessary. For Modal views? - //transaction.setTransition(androidx.fragment.app.FragmentTransaction.TRANSIT_FRAGMENT_OPEN); - } + if (currentEntry && animated && !navigationTransition) { + //TODO: Check whether or not this is still necessary. For Modal views? + //transaction.setTransition(androidx.fragment.app.FragmentTransaction.TRANSIT_FRAGMENT_OPEN); + } - transaction.replace(this.containerViewId, newFragment, newFragmentTag); - transaction.commitAllowingStateLoss(); + transaction.replace(this.containerViewId, newFragment, newFragmentTag); + transaction.commitAllowingStateLoss(); + }); } public _goBackCore(backstackEntry: BackstackEntry) {