fix: nested fragments interact thru child fragment manager (#6293)

This commit is contained in:
Manol Donev
2018-10-11 17:44:30 +03:00
committed by GitHub
parent d91bfd8e11
commit 307172003d
5 changed files with 124 additions and 16 deletions

View File

@@ -1,13 +1,12 @@
import * as helper from "../helper";
import TKUnit = require("../../TKUnit"); 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 { _resetRootView } from "tns-core-modules/application/";
import { Frame, NavigationEntry, topmost } from "tns-core-modules/ui/frame"; import { Frame, NavigationEntry, topmost } from "tns-core-modules/ui/frame";
import { Page } from "tns-core-modules/ui/page"; import { Page } from "tns-core-modules/ui/page";
import { TabView, TabViewItem } from "tns-core-modules/ui/tab-view"; import { TabView, TabViewItem } from "tns-core-modules/ui/tab-view";
function waitUntilNavigatedToMaxTimeout(pages: Page[], action: Function) { function waitUntilNavigatedToMaxTimeout(pages: Page[], action: Function) {
const maxTimeout = 5; const maxTimeout = 8;
let completed = 0; let completed = 0;
function navigatedTo(args) { function navigatedTo(args) {
args.object.page.off("navigatedTo", navigatedTo); args.object.page.off("navigatedTo", navigatedTo);
@@ -67,13 +66,19 @@ export function test_frame_topmost_matches_selectedIndex() {
create: () => tabView 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");
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");
waitUntilNavigatedToMaxTimeout([items[1].page], () => tabView.selectedIndex = 1); waitUntilNavigatedToMaxTimeout([items[1].page], () => tabView.selectedIndex = 1);
TKUnit.assertEqual(topmost().id, "Tab1 Frame1");
TKUnit.assertEqual(topmost().id, "Tab1 Frame1"); }
} }
export function test_offset_zero_should_raise_same_events() { export function test_offset_zero_should_raise_same_events() {

View File

@@ -234,6 +234,7 @@ export class View extends ViewCommon {
private layoutChangeListenerIsSet: boolean; private layoutChangeListenerIsSet: boolean;
private layoutChangeListener: android.view.View.OnLayoutChangeListener; private layoutChangeListener: android.view.View.OnLayoutChangeListener;
private _manager: android.support.v4.app.FragmentManager; private _manager: android.support.v4.app.FragmentManager;
private _rootManager: android.support.v4.app.FragmentManager;
nativeViewProtected: android.view.View; 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 = (<android.support.v4.app.FragmentActivity>this._context).getSupportFragmentManager();
}
return this._rootManager;
}
public _getFragmentManager(): android.support.v4.app.FragmentManager { public _getFragmentManager(): android.support.v4.app.FragmentManager {
let manager = this._manager; let manager = this._manager;
if (!manager) { if (!manager) {
let view: View = this; let view: View = this;
let frameOrTabViewItemFound = false;
while (view) { 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; const dialogFragment = view._dialogFragment;
if (dialogFragment) { if (dialogFragment) {
manager = dialogFragment.getChildFragmentManager(); manager = dialogFragment.getChildFragmentManager();
break; 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) { if (!manager && this._context) {
@@ -294,6 +327,7 @@ export class View extends ViewCommon {
@profile @profile
public onLoaded() { public onLoaded() {
this._manager = null; this._manager = null;
this._rootManager = null;
super.onLoaded(); super.onLoaded();
this.setOnTouchListener(); this.setOnTouchListener();
} }
@@ -307,6 +341,7 @@ export class View extends ViewCommon {
} }
this._manager = null; this._manager = null;
this._rootManager = null;
super.onUnloaded(); super.onUnloaded();
} }
@@ -405,6 +440,10 @@ export class View extends ViewCommon {
return false; return false;
} }
get _hasFragments(): boolean {
return false;
}
public layoutNativeView(left: number, top: number, right: number, bottom: number): void { public layoutNativeView(left: number, top: number, right: number, bottom: number): void {
if (this.nativeViewProtected) { if (this.nativeViewProtected) {
this.nativeViewProtected.layout(left, top, right, bottom); this.nativeViewProtected.layout(left, top, right, bottom);
@@ -571,7 +610,7 @@ export class View extends ViewCommon {
this._dialogFragment = df; this._dialogFragment = df;
this._raiseShowingModallyEvent(); this._raiseShowingModallyEvent();
this._dialogFragment.show(parent._getFragmentManager(), this._domId.toString()); this._dialogFragment.show(parent._getRootFragmentManager(), this._domId.toString());
} }
protected _hideNativeModalView(parent: View) { protected _hideNativeModalView(parent: View) {

View File

@@ -121,6 +121,10 @@ export class Frame extends FrameBase {
return this._android; return this._android;
} }
get _hasFragments(): boolean {
return true;
}
_onAttachedToWindow(): void { _onAttachedToWindow(): void {
super._onAttachedToWindow(); super._onAttachedToWindow();
this._attachedToWindow = true; 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(); this.disposeCurrentFragment();
super._onRootViewReset(); super._onRootViewReset();
} }
@@ -186,7 +199,14 @@ export class Frame extends FrameBase {
} }
private disposeCurrentFragment(): void { 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; return;
} }

View File

@@ -125,6 +125,10 @@ export class Frame extends View {
* @private * @private
*/ */
_currentEntry: BackstackEntry; _currentEntry: BackstackEntry;
/**
* @private
*/
_executingEntry: BackstackEntry;
/** /**
* @private * @private
*/ */

View File

@@ -260,6 +260,10 @@ export class TabViewItem extends TabViewItemBase {
public index: number; public index: number;
private _defaultTransformationMethod: android.text.method.TransformationMethod; private _defaultTransformationMethod: android.text.method.TransformationMethod;
get _hasFragments(): boolean {
return true;
}
public initNativeView(): void { public initNativeView(): void {
super.initNativeView(); super.initNativeView();
if (this.nativeViewProtected) { 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 (<Array<any>>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 } { [fontSizeProperty.getDefault](): { nativeSize: number } {
return { nativeSize: this.nativeViewProtected.getTextSize() }; return { nativeSize: this.nativeViewProtected.getTextSize() };
} }
@@ -361,6 +383,10 @@ export class TabView extends TabViewBase {
tabs.push(new WeakRef(this)); tabs.push(new WeakRef(this));
} }
get _hasFragments(): boolean {
return true;
}
public onItemsChanged(oldItems: TabViewItem[], newItems: TabViewItem[]): void { public onItemsChanged(oldItems: TabViewItem[], newItems: TabViewItem[]): void {
super.onItemsChanged(oldItems, newItems); super.onItemsChanged(oldItems, newItems);
@@ -512,6 +538,20 @@ export class TabView extends TabViewBase {
return false; return false;
} }
public _onRootViewReset(): void {
this.disposeCurrentFragments();
super._onRootViewReset();
}
private disposeCurrentFragments(): void {
const fragmentManager = this._getFragmentManager();
const transaction = fragmentManager.beginTransaction();
for (let fragment of (<Array<any>>fragmentManager.getFragments().toArray())) {
transaction.remove(fragment);
}
transaction.commitNowAllowingStateLoss();
}
private shouldUpdateAdapter(items: Array<TabViewItemDefinition>) { private shouldUpdateAdapter(items: Array<TabViewItemDefinition>) {
if (!this._pagerAdapter) { if (!this._pagerAdapter) {
return false; return false;