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 { 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,14 +66,20 @@ 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");
waitUntilNavigatedToMaxTimeout([items[1].page], () => tabView.selectedIndex = 1);
TKUnit.assertEqual(topmost().id, "Tab1 Frame1");
}
}
export function test_offset_zero_should_raise_same_events() {
let actualEventsRaised = [];

View File

@@ -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,21 +266,53 @@ 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 {
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 {
}
// - 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) {
manager = (<android.support.v4.app.FragmentActivity>this._context).getSupportFragmentManager();
@@ -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) {

View File

@@ -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;
}

View File

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

View File

@@ -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 (<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 } {
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 (<Array<any>>fragmentManager.getFragments().toArray())) {
transaction.remove(fragment);
}
transaction.commitNowAllowingStateLoss();
}
private shouldUpdateAdapter(items: Array<TabViewItemDefinition>) {
if (!this._pagerAdapter) {
return false;