diff --git a/tests/app/ui/tab-view/tab-view-root-tests.ts b/tests/app/ui/tab-view/tab-view-root-tests.ts index 2d2ab6fbe..18d0fc99a 100644 --- a/tests/app/ui/tab-view/tab-view-root-tests.ts +++ b/tests/app/ui/tab-view/tab-view-root-tests.ts @@ -1,13 +1,12 @@ -import * as helper from "../helper"; import TKUnit = require("../../TKUnit"); -import { isIOS, isAndroid } from "tns-core-modules/platform"; +import { isAndroid } from "tns-core-modules/platform"; import { _resetRootView } from "tns-core-modules/application/"; import { Frame, NavigationEntry, topmost } from "tns-core-modules/ui/frame"; import { Page } from "tns-core-modules/ui/page"; import { TabView, TabViewItem } from "tns-core-modules/ui/tab-view"; function waitUntilNavigatedToMaxTimeout(pages: Page[], action: Function) { - const maxTimeout = 5; + const maxTimeout = 8; let completed = 0; function navigatedTo(args) { args.object.page.off("navigatedTo", navigatedTo); @@ -67,13 +66,19 @@ export function test_frame_topmost_matches_selectedIndex() { create: () => tabView }; - waitUntilNavigatedToMaxTimeout([items[0].page], () => _resetRootView(entry)); + if (isAndroid) { + waitUntilNavigatedToMaxTimeout([items[0].page, items[1].page], () => _resetRootView(entry)); + TKUnit.assertEqual(topmost().id, "Tab0 Frame0"); + + tabView.selectedIndex = 1; + TKUnit.assertEqual(topmost().id, "Tab1 Frame1"); + } else { + waitUntilNavigatedToMaxTimeout([items[0].page], () => _resetRootView(entry)); + TKUnit.assertEqual(topmost().id, "Tab0 Frame0"); - TKUnit.assertEqual(topmost().id, "Tab0 Frame0"); - - waitUntilNavigatedToMaxTimeout([items[1].page], () => tabView.selectedIndex = 1); - - TKUnit.assertEqual(topmost().id, "Tab1 Frame1"); + waitUntilNavigatedToMaxTimeout([items[1].page], () => tabView.selectedIndex = 1); + TKUnit.assertEqual(topmost().id, "Tab1 Frame1"); + } } export function test_offset_zero_should_raise_same_events() { diff --git a/tns-core-modules/ui/core/view/view.android.ts b/tns-core-modules/ui/core/view/view.android.ts index 948763d86..4b1bfaab4 100644 --- a/tns-core-modules/ui/core/view/view.android.ts +++ b/tns-core-modules/ui/core/view/view.android.ts @@ -234,6 +234,7 @@ export class View extends ViewCommon { private layoutChangeListenerIsSet: boolean; private layoutChangeListener: android.view.View.OnLayoutChangeListener; private _manager: android.support.v4.app.FragmentManager; + private _rootManager: android.support.v4.app.FragmentManager; nativeViewProtected: android.view.View; @@ -265,20 +266,52 @@ export class View extends ViewCommon { } } + public _getChildFragmentManager(): android.support.v4.app.FragmentManager { + return null; + } + + public _getRootFragmentManager(): android.support.v4.app.FragmentManager { + if (!this._rootManager && this._context) { + this._rootManager = (this._context).getSupportFragmentManager(); + } + + return this._rootManager; + } + public _getFragmentManager(): android.support.v4.app.FragmentManager { let manager = this._manager; if (!manager) { let view: View = this; + let frameOrTabViewItemFound = false; while (view) { + // when interacting with nested fragments instead of using getSupportFragmentManager + // we must always use getChildFragmentManager instead; + // we have three sources of fragments -- Frame fragments, TabViewItem fragments, and + // modal dialog fragments + + // modal -> frame / tabview (frame / tabview use modal CHILD fm) const dialogFragment = view._dialogFragment; if (dialogFragment) { manager = dialogFragment.getChildFragmentManager(); break; - } else { - // the case is needed because _dialogFragment is on View - // but parent may be ViewBase. - view = view.parent as View; } + + // - frame1 -> frame2 (frame2 uses frame1 CHILD fm) + // - tabview -> frame1 (frame1 uses tabview item CHILD fm) + // - frame1 -> tabview (tabview uses frame1 CHILD fm) + // - frame1 -> tabview -> frame2 (tabview uses frame1 CHILD fm; frame2 uses tabview item CHILD fm) + if (view._hasFragments) { + if (frameOrTabViewItemFound) { + manager = view._getChildFragmentManager(); + break; + } + + frameOrTabViewItemFound = true; + } + + // the case is needed because _dialogFragment is on View + // but parent may be ViewBase. + view = view.parent as View; } if (!manager && this._context) { @@ -294,6 +327,7 @@ export class View extends ViewCommon { @profile public onLoaded() { this._manager = null; + this._rootManager = null; super.onLoaded(); this.setOnTouchListener(); } @@ -307,6 +341,7 @@ export class View extends ViewCommon { } this._manager = null; + this._rootManager = null; super.onUnloaded(); } @@ -405,6 +440,10 @@ export class View extends ViewCommon { return false; } + get _hasFragments(): boolean { + return false; + } + public layoutNativeView(left: number, top: number, right: number, bottom: number): void { if (this.nativeViewProtected) { this.nativeViewProtected.layout(left, top, right, bottom); @@ -571,7 +610,7 @@ export class View extends ViewCommon { this._dialogFragment = df; this._raiseShowingModallyEvent(); - this._dialogFragment.show(parent._getFragmentManager(), this._domId.toString()); + this._dialogFragment.show(parent._getRootFragmentManager(), this._domId.toString()); } protected _hideNativeModalView(parent: View) { diff --git a/tns-core-modules/ui/frame/frame.android.ts b/tns-core-modules/ui/frame/frame.android.ts index e0c8ab556..fad8bf09e 100644 --- a/tns-core-modules/ui/frame/frame.android.ts +++ b/tns-core-modules/ui/frame/frame.android.ts @@ -121,6 +121,10 @@ export class Frame extends FrameBase { return this._android; } + get _hasFragments(): boolean { + return true; + } + _onAttachedToWindow(): void { super._onAttachedToWindow(); this._attachedToWindow = true; @@ -175,7 +179,16 @@ export class Frame extends FrameBase { } } - _onRootViewReset(): void { + public _getChildFragmentManager() { + const backstackEntry = this._executingEntry || this._currentEntry; + if (backstackEntry && backstackEntry.fragment && backstackEntry.fragment.isAdded()) { + return backstackEntry.fragment.getChildFragmentManager(); + } + + return null; + } + + public _onRootViewReset(): void { this.disposeCurrentFragment(); super._onRootViewReset(); } @@ -186,7 +199,14 @@ export class Frame extends FrameBase { } private disposeCurrentFragment(): void { - if (!this._currentEntry || !this._currentEntry.fragment) { + // when interacting with nested fragments it seems Android is smart enough + // to automatically remove child fragments when parent fragment is removed; + // however, we must add a fragment.isAdded() guard as our logic will try to + // explicitly remove the already removed child fragment causing an + // IllegalStateException: Fragment has not been attached yet. + if (!this._currentEntry || + !this._currentEntry.fragment || + !this._currentEntry.fragment.isAdded()) { return; } diff --git a/tns-core-modules/ui/frame/frame.d.ts b/tns-core-modules/ui/frame/frame.d.ts index b18af7387..38638d553 100644 --- a/tns-core-modules/ui/frame/frame.d.ts +++ b/tns-core-modules/ui/frame/frame.d.ts @@ -125,6 +125,10 @@ export class Frame extends View { * @private */ _currentEntry: BackstackEntry; + /** + * @private + */ + _executingEntry: BackstackEntry; /** * @private */ diff --git a/tns-core-modules/ui/tab-view/tab-view.android.ts b/tns-core-modules/ui/tab-view/tab-view.android.ts index c8893bc98..91bf8c989 100644 --- a/tns-core-modules/ui/tab-view/tab-view.android.ts +++ b/tns-core-modules/ui/tab-view/tab-view.android.ts @@ -260,6 +260,10 @@ export class TabViewItem extends TabViewItemBase { public index: number; private _defaultTransformationMethod: android.text.method.TransformationMethod; + get _hasFragments(): boolean { + return true; + } + public initNativeView(): void { super.initNativeView(); if (this.nativeViewProtected) { @@ -297,6 +301,24 @@ export class TabViewItem extends TabViewItemBase { } } + public _getChildFragmentManager(): android.support.v4.app.FragmentManager { + const tabView = this.parent as TabView; + let tabFragment = null; + const fragmentManager = tabView._getFragmentManager(); + for (let fragment of (>fragmentManager.getFragments().toArray())) { + if (fragment.index === this.index) { + tabFragment = fragment; + break; + } + } + + if (!tabFragment) { + throw new Error(`Could not get child fragment manager for tab item with index ${this.index}`); + } + + return tabFragment.getChildFragmentManager(); + } + [fontSizeProperty.getDefault](): { nativeSize: number } { return { nativeSize: this.nativeViewProtected.getTextSize() }; } @@ -361,6 +383,10 @@ export class TabView extends TabViewBase { tabs.push(new WeakRef(this)); } + get _hasFragments(): boolean { + return true; + } + public onItemsChanged(oldItems: TabViewItem[], newItems: TabViewItem[]): void { super.onItemsChanged(oldItems, newItems); @@ -512,6 +538,20 @@ export class TabView extends TabViewBase { return false; } + public _onRootViewReset(): void { + this.disposeCurrentFragments(); + super._onRootViewReset(); + } + + private disposeCurrentFragments(): void { + const fragmentManager = this._getFragmentManager(); + const transaction = fragmentManager.beginTransaction(); + for (let fragment of (>fragmentManager.getFragments().toArray())) { + transaction.remove(fragment); + } + transaction.commitNowAllowingStateLoss(); + } + private shouldUpdateAdapter(items: Array) { if (!this._pagerAdapter) { return false;