fix-next(tabview): visually pre-load tab items on android (#5495)

This commit is contained in:
Martin Yankov
2018-03-08 14:39:59 +02:00
committed by GitHub
parent 0138873ee3
commit 8141737f74
7 changed files with 347 additions and 92 deletions

View File

@@ -5,6 +5,7 @@ import * as trace from "tns-core-modules/trace";
trace.addCategories(trace.categories.NativeLifecycle);
trace.addCategories(trace.categories.Navigation);
trace.addCategories(trace.categories.Transition);
trace.enable();
var countResume = 0;

View File

@@ -1,5 +1,6 @@
import * as TKUnit from "../../TKUnit";
import * as helper from "../helper";
import { isIOS, isAndroid } from "tns-core-modules/platform";
import { Label } from "tns-core-modules/ui/label";
import { StackLayout } from "tns-core-modules/ui/layouts/stack-layout";
import * as frameModule from "tns-core-modules/ui/frame";
@@ -201,8 +202,15 @@ export function testLoadedAndUnloadedAreFired_WhenNavigatingAwayAndBack() {
topFrame.currentPage.id = null;
TKUnit.arrayAssert(loadedEventsCount, [2, 0]);
TKUnit.arrayAssert(unloadedEventsCount, [1, 0]);
if (isIOS) {
TKUnit.arrayAssert(loadedEventsCount, [2, 0]);
TKUnit.arrayAssert(unloadedEventsCount, [1, 0]);
}
if (isAndroid) {
TKUnit.arrayAssert(loadedEventsCount, [2, 2]);
TKUnit.arrayAssert(unloadedEventsCount, [1, 1]);
}
}
function _clickTheFirstButtonInTheListViewNatively(tabView: TabView) {

View File

@@ -1,81 +1,253 @@
import * as helper from "../helper";
import TKUnit = require("../../TKUnit");
import { isIOS, isAndroid } from "tns-core-modules/platform";
import { _resetRootView } from "tns-core-modules/application/";
import { Frame, NavigationEntry } from "tns-core-modules/ui/frame";
import { Page } from "tns-core-modules/ui/page";
import { TabView, TabViewItem } from "tns-core-modules/ui/tab-view";
export function test_whenChangingTabsWithFramesCorrectEventsAreRaised() {
const actualEventsRaised = [];
function attachPageEventHandlers(page: Page) {
page.on(Page.loadedEvent, () => actualEventsRaised.push(`${page.id} loaded`));
page.on(Page.unloadedEvent, () => actualEventsRaised.push(`${page.id} unloaded`));
page.on(Page.navigatingToEvent, () => actualEventsRaised.push(`${page.id} navigatingTo`));
page.on(Page.navigatingFromEvent, () => actualEventsRaised.push(`${page.id} navigatingFrom`));
page.on(Page.navigatedToEvent, () => actualEventsRaised.push(`${page.id} navigatedTo`));
page.on(Page.navigatedFromEvent, () => actualEventsRaised.push(`${page.id} navigatedFrom`));
function waitUntilNavigatedTo(pages: Page[], action: Function) {
let completed = 0;
function navigatedTo(args) {
args.object.page.off("navigatedTo", navigatedTo);
completed++;
}
function attachFrameEventHandlers(frame: Frame) {
frame.on(Frame.loadedEvent, () => actualEventsRaised.push(`${frame.id} loaded`));
frame.on(Frame.unloadedEvent, () => actualEventsRaised.push(`${frame.id} unloaded`));
pages.forEach(page => page.on("navigatedTo", navigatedTo));
action();
TKUnit.waitUntilReady(() => completed === pages.length, 100);
}
function createPage(i: number) {
const page = new Page();
page.id = `Tab${i} Frame${i} Page${i}`;
return page;
}
function createFrame(i: number, page: Page) {
const frame = new Frame();
frame.navigate(() => page);
frame.id = `Tab${i} Frame${i}`;
return frame;
}
function createTabItem(i: number, frame: Frame) {
const tabEntry = new TabViewItem();
tabEntry.title = "Tab " + i;
tabEntry.view = frame;
tabEntry["index"] = i;
return tabEntry;
}
function createTabItemsWithFrames(count: number) {
const items = [];
for (var i = 0; i < count; i++) {
const page = createPage(i);
const frame = createFrame(i, page);
const tabItem = createTabItem(i, frame);
items.push({ page, frame, tabItem });
}
const page1 = new Page();
page1.id = "Tab1 Frame1 Page1";
attachPageEventHandlers(page1);
return items;
}
const frame1 = new Frame();
frame1.navigate(() => page1);
frame1.id = "Tab1 Frame1";
attachFrameEventHandlers(frame1);
export function test_ios_and_android_offset_zero_should_raise_same_events() {
let actualEventsRaised = [];
const page2 = new Page();
page2.id = "Tab2 Frame2 Page2";
attachPageEventHandlers(page2);
function resetActualEventsRaised() {
for (var i = 0; i < actualEventsRaised.length; i++) {
actualEventsRaised[i] = [];
}
}
const frame2 = new Frame();
frame2.navigate(() => page2);
frame2.id = "Tab2 Frame2";
attachFrameEventHandlers(frame2);
function attachEventHandlers(i: number, item) {
actualEventsRaised.push([]);
const page = item.page;
page.on(Page.loadedEvent, () => actualEventsRaised[i].push(`${page.id} loaded`));
page.on(Page.unloadedEvent, () => actualEventsRaised[i].push(`${page.id} unloaded`));
page.on(Page.navigatingToEvent, () => actualEventsRaised[i].push(`${page.id} navigatingTo`));
page.on(Page.navigatingFromEvent, () => actualEventsRaised[i].push(`${page.id} navigatingFrom`));
page.on(Page.navigatedToEvent, () => actualEventsRaised[i].push(`${page.id} navigatedTo`));
page.on(Page.navigatedFromEvent, () => actualEventsRaised[i].push(`${page.id} navigatedFrom`));
const frame = item.frame;
frame.on(Frame.loadedEvent, () => actualEventsRaised[i].push(`${frame.id} loaded`));
frame.on(Frame.unloadedEvent, () => actualEventsRaised[i].push(`${frame.id} unloaded`));
}
const items = createTabItemsWithFrames(3);
items.forEach((item, i) => {
attachEventHandlers(i, item);
});
const tabView = new TabView();
const tabEntry1 = new TabViewItem();
tabEntry1.title = "frame1";
tabEntry1.view = frame1;
const tabEntry2 = new TabViewItem();
tabEntry2.title = "frame2";
tabEntry2.view = frame2;
tabView.items = [tabEntry1, tabEntry2];
tabView.items = items.map(item => item.tabItem);
// iOS cannot preload tab items
// Android preloads 1 tab item to the sides by default
// set this to 0, so that both platforms behave the same.
tabView.androidOffscreenTabLimit = 0;
const entry: NavigationEntry = {
create: () => tabView
};
helper.waitUntilNavigatedTo(page1, () => _resetRootView(entry));
helper.waitUntilNavigatedTo(page2, () => tabView.selectedIndex = 1);
tabView.selectedIndex = 0;
TKUnit.waitUntilReady(() => page1.isLoaded);
helper.waitUntilNavigatedTo(items[0].page, () => _resetRootView(entry));
const expectedEventsRaised = [
"Tab1 Frame1 loaded",
"Tab1 Frame1 Page1 navigatingTo",
"Tab1 Frame1 Page1 loaded",
"Tab1 Frame1 Page1 navigatedTo",
"Tab1 Frame1 Page1 unloaded",
"Tab1 Frame1 unloaded",
"Tab2 Frame2 loaded",
"Tab2 Frame2 Page2 navigatingTo",
"Tab2 Frame2 Page2 loaded",
"Tab2 Frame2 Page2 navigatedTo",
"Tab2 Frame2 Page2 unloaded",
"Tab2 Frame2 unloaded",
"Tab1 Frame1 Page1 loaded",
"Tab1 Frame1 loaded"
const expectedEventsRaisedAfterTabCreated = [
[
"Tab0 Frame0 loaded",
"Tab0 Frame0 Page0 navigatingTo",
"Tab0 Frame0 Page0 loaded",
"Tab0 Frame0 Page0 navigatedTo",
],
[],
[]
];
TKUnit.arrayAssert(actualEventsRaised, expectedEventsRaised);
TKUnit.assertDeepEqual(actualEventsRaised, expectedEventsRaisedAfterTabCreated);
resetActualEventsRaised();
helper.waitUntilNavigatedTo(items[2].page, () => tabView.selectedIndex = 2);
const expectedEventsRaisedAfterSelectThirdTab = [
[
"Tab0 Frame0 Page0 unloaded",
"Tab0 Frame0 unloaded",
],
[],
[
"Tab2 Frame2 loaded",
"Tab2 Frame2 Page2 navigatingTo",
"Tab2 Frame2 Page2 loaded",
"Tab2 Frame2 Page2 navigatedTo"
]
];
TKUnit.assertDeepEqual(actualEventsRaised, expectedEventsRaisedAfterSelectThirdTab);
resetActualEventsRaised();
tabView.selectedIndex = 0;
TKUnit.waitUntilReady(() => items[0].page.isLoaded);
const expectedEventsRaisedAfterReturnToFirstTab = [
[
"Tab0 Frame0 Page0 loaded",
"Tab0 Frame0 loaded",
],
[],
[
"Tab2 Frame2 Page2 unloaded",
"Tab2 Frame2 unloaded",
]
];
TKUnit.assertDeepEqual(actualEventsRaised, expectedEventsRaisedAfterReturnToFirstTab);
}
export function test_android_default_offset_should_preload_1_tab_on_each_side() {
let actualEventsRaised = [];
function resetActualEventsRaised() {
for (var i = 0; i < actualEventsRaised.length; i++) {
actualEventsRaised[i] = [];
}
}
function attachEventHandlers(i: number, item) {
actualEventsRaised.push([]);
const page = item.page;
page.on(Page.loadedEvent, () => actualEventsRaised[i].push(`${page.id} loaded`));
page.on(Page.unloadedEvent, () => actualEventsRaised[i].push(`${page.id} unloaded`));
page.on(Page.navigatingToEvent, () => actualEventsRaised[i].push(`${page.id} navigatingTo`));
page.on(Page.navigatingFromEvent, () => actualEventsRaised[i].push(`${page.id} navigatingFrom`));
page.on(Page.navigatedToEvent, () => actualEventsRaised[i].push(`${page.id} navigatedTo`));
page.on(Page.navigatedFromEvent, () => actualEventsRaised[i].push(`${page.id} navigatedFrom`));
const frame = item.frame;
frame.on(Frame.loadedEvent, () => actualEventsRaised[i].push(`${frame.id} loaded`));
frame.on(Frame.unloadedEvent, () => actualEventsRaised[i].push(`${frame.id} unloaded`));
}
if (isAndroid) {
const items = createTabItemsWithFrames(3);
items.forEach((item, i) => {
attachEventHandlers(i, item);
});
const tabView = new TabView();
tabView.items = items.map(item => item.tabItem);
const entry: NavigationEntry = {
create: () => tabView
};
waitUntilNavigatedTo([items[0].page, items[1].page], () => _resetRootView(entry));
const expectedEventsRaisedAfterTabCreated = [
[
"Tab0 Frame0 loaded",
"Tab0 Frame0 Page0 navigatingTo",
"Tab0 Frame0 Page0 loaded",
"Tab0 Frame0 Page0 navigatedTo",
],
[
"Tab1 Frame1 loaded",
"Tab1 Frame1 Page1 navigatingTo",
"Tab1 Frame1 Page1 loaded",
"Tab1 Frame1 Page1 navigatedTo",
],
[]
];
TKUnit.assertDeepEqual(actualEventsRaised, expectedEventsRaisedAfterTabCreated);
resetActualEventsRaised();
helper.waitUntilNavigatedTo(items[2].page, () => tabView.selectedIndex = 2);
const expectedEventsRaisedAfterSelectThirdTab = [
[
"Tab0 Frame0 Page0 unloaded",
"Tab0 Frame0 unloaded",
],
[],
[
"Tab2 Frame2 loaded",
"Tab2 Frame2 Page2 navigatingTo",
"Tab2 Frame2 Page2 loaded",
"Tab2 Frame2 Page2 navigatedTo"
]
];
TKUnit.assertDeepEqual(actualEventsRaised, expectedEventsRaisedAfterSelectThirdTab);
resetActualEventsRaised();
tabView.selectedIndex = 0;
TKUnit.waitUntilReady(() => items[0].page.isLoaded);
const expectedEventsRaisedAfterReturnToFirstTab = [
[
"Tab0 Frame0 Page0 loaded",
"Tab0 Frame0 loaded",
],
[],
[
"Tab2 Frame2 Page2 unloaded",
"Tab2 Frame2 unloaded",
]
];
TKUnit.assertDeepEqual(actualEventsRaised, expectedEventsRaisedAfterReturnToFirstTab);
}
}
export function tearDownModule() {

View File

@@ -176,6 +176,18 @@ export class Frame extends FrameBase {
}
}
onUnloaded() {
if (this._currentEntry && this._currentEntry.fragment) {
const manager: android.app.FragmentManager = this._getFragmentManager();
const transaction = manager.beginTransaction();
transaction.remove(this._currentEntry.fragment);
transaction.commitAllowingStateLoss();
}
super.onUnloaded();
}
private createFragment(backstackEntry: BackstackEntry, fragmentTag: string): android.app.Fragment {
ensureFragmentClass();
const newFragment = new fragmentClass();
@@ -569,7 +581,8 @@ function findPageForFragment(fragment: android.app.Fragment, frame: Frame) {
callbacks.entry = entry;
entry.fragment = fragment;
_updateTransitions(entry);
} else {
}
else {
throw new Error(`Could not find a page for ${fragmentTag}.`);
}
}
@@ -697,13 +710,7 @@ class FragmentCallbacksImplementation implements AndroidFragmentCallbacks {
this.frame._addView(page);
}
// Load page here even if root view is not loaded yet.
// Otherwise it will show as blank,
// The case is Tab->Frame->Page activity recreated, fragments are
// created before Tab loads its items.
// TODO: addCheck if the fragment is visible so we don't load pages
// that are not in the selectedIndex of the Tab!!!!!!
if (!page.isLoaded) {
if (frame.isLoaded && !page.isLoaded) {
page.callLoaded();
}

View File

@@ -71,9 +71,8 @@ export abstract class TabViewItemBase extends ViewBase implements TabViewItemDef
public loadView(view: ViewBase): void {
const tabView = this.parent as TabViewBase;
if (tabView && tabView.items) {
const index = tabView.items.indexOf(this);
// Don't load items until their fragments are instantiated.
if (index === tabView.selectedIndex && (<TabViewItemDefinition>this).canBeLoaded) {
if ((<TabViewItemDefinition>this).canBeLoaded) {
super.loadView(view);
}
}
@@ -185,21 +184,7 @@ export class TabViewBase extends View implements TabViewDefinition, AddChildFrom
}
public onSelectedIndexChanged(oldIndex: number, newIndex: number): void {
const items = this.items;
if (!items) {
return;
}
const oldItem = items[oldIndex];
if (oldItem) {
oldItem.unloadView(oldItem.view);
}
const newItem = items[newIndex];
if (newItem && this.isLoaded) {
newItem.loadView(newItem.view);
}
// to be overridden in platform specific files
this.notify(<SelectedIndexChangedEventData>{ eventName: TabViewBase.selectedIndexChangedEvent, object: this, oldIndex, newIndex });
}
}

View File

@@ -5,7 +5,7 @@ import {
TabViewBase, TabViewItemBase, itemsProperty, selectedIndexProperty,
tabTextColorProperty, tabBackgroundColorProperty, selectedTabTextColorProperty,
androidSelectedTabHighlightColorProperty, androidOffscreenTabLimitProperty,
fontSizeProperty, fontInternalProperty, View, layout, traceCategory, traceEnabled,
fontSizeProperty, fontInternalProperty, View, layout, traceCategory, traceEnabled,
traceWrite, Color
} from "./tab-view-common"
import { textTransformProperty, TextTransform, getTransformedText } from "../text-base";
@@ -55,6 +55,7 @@ function initializeNativeClasses() {
}
static newInstance(tabId: number, index: number): TabFragmentImplementation {
const args = new android.os.Bundle();
args.putInt(TABID, tabId);
args.putInt(INDEX, index);
@@ -74,7 +75,13 @@ function initializeNativeClasses() {
}
public onCreateView(inflater: android.view.LayoutInflater, container: android.view.ViewGroup, savedInstanceState: android.os.Bundle): android.view.View {
return this.tab.items[this.index].view.nativeViewProtected;
const tabItem = this.tab.items[this.index];
return tabItem.view.nativeViewProtected;
}
public onDestroyView() {
super.onDestroyView();
}
}
@@ -155,6 +162,10 @@ function initializeNativeClasses() {
const fragment: android.app.Fragment = <android.app.Fragment>object;
this.mCurTransaction.detach(fragment);
if (this.mCurrentPrimaryItem === fragment) {
this.mCurrentPrimaryItem = null;
}
const tabItems = this.owner.items;
const tabItem = tabItems ? tabItems[position] : null;
if (tabItem) {
@@ -177,13 +188,14 @@ function initializeNativeClasses() {
this.mCurrentPrimaryItem = fragment;
this.owner.selectedIndex = position;
}
const tab = this.owner;
const tabItems = tab.items;
const newTabItem = tabItems ? tabItems[position] : null;
if (newTabItem && tab.isLoaded) {
newTabItem.loadView(newTabItem.view);
const tab = this.owner;
const tabItems = tab.items;
const newTabItem = tabItems ? tabItems[position] : null;
if (newTabItem) {
tab._loadUnloadTabItems(tab.selectedIndex);
}
}
}
@@ -336,6 +348,14 @@ function setElevation(grid: org.nativescript.widgets.GridLayout, tabLayout: org.
export const tabs = new Array<WeakRef<TabView>>();
function iterateIndexRange(index: number, eps: number, lastIndex: number, callback: (i) => void) {
const rangeStart = Math.max(0, index - eps);
const rangeEnd = Math.min(index + eps, lastIndex);
for (let i = rangeStart; i <= rangeEnd; i++) {
callback(i);
}
}
export class TabView extends TabViewBase {
private _tabLayout: org.nativescript.widgets.TabLayout;
private _viewPager: android.support.v4.view.ViewPager;
@@ -429,6 +449,38 @@ export class TabView extends TabViewBase {
(<any>this._pagerAdapter).owner = this;
}
public _loadUnloadTabItems(newIndex: number) {
const items = this.items;
const lastIndex = this.items.length - 1;
const offsideItems = this.androidTabsPosition === "top" ? this.androidOffscreenTabLimit : 0;
let toUnload = [];
let toLoad = [];
iterateIndexRange(newIndex, offsideItems, lastIndex, (i) => toLoad.push(i));
items.forEach((item, i) => {
const indexOfI = toLoad.indexOf(i);
if (indexOfI < 0) {
toUnload.push(i);
}
});
toUnload.forEach(index => {
const item = items[index];
if (items[index]) {
item.unloadView(item.view);
}
});
toLoad.forEach(index => {
const item = items[index];
if (this.isLoaded && items[index]) {
item.loadView(item.view);
}
});
}
public onLoaded(): void {
super.onLoaded();
@@ -464,7 +516,7 @@ export class TabView extends TabViewBase {
if (!this._pagerAdapter) {
return false;
}
const currentPagerAdapterItems = (<any>this._pagerAdapter).items;
// if both values are null, should not update
@@ -483,7 +535,7 @@ export class TabView extends TabViewBase {
}
const matchingItems = currentPagerAdapterItems.filter((currentItem) => {
return !!items.filter((item) => {
return !!items.filter((item) => {
return item._domId === currentItem._domId
})[0];
});

View File

@@ -1,7 +1,7 @@
import { TabViewItem as TabViewItemDefinition } from ".";
import { Font } from "../styling/font";
import { ios as iosView } from "../core/view";
import { ios as iosView, ViewBase } from "../core/view";
import {
TabViewBase, TabViewItemBase, itemsProperty, selectedIndexProperty,
tabTextColorProperty, tabBackgroundColorProperty, selectedTabTextColorProperty, iosIconRenderingModeProperty,
@@ -158,6 +158,17 @@ export class TabViewItem extends TabViewItemBase {
this.setNativeView(undefined);
}
public loadView(view: ViewBase): void {
const tabView = this.parent as TabViewBase;
if (tabView && tabView.items) {
const index = tabView.items.indexOf(this);
if (index === tabView.selectedIndex) {
super.loadView(view);
}
}
}
public _update() {
const parent = <TabView>this.parent;
const controller = this.__controller;
@@ -229,6 +240,25 @@ export class TabView extends TabViewBase {
//
}
public onSelectedIndexChanged(oldIndex: number, newIndex: number): void {
const items = this.items;
if (!items) {
return;
}
const oldItem = items[oldIndex];
if (oldItem) {
oldItem.unloadView(oldItem.view);
}
const newItem = items[newIndex];
if (newItem && this.isLoaded) {
newItem.loadView(newItem.view);
}
super.onSelectedIndexChanged(oldIndex, newIndex);
}
public onMeasure(widthMeasureSpec: number, heightMeasureSpec: number): void {
const width = layout.getMeasureSpecSize(widthMeasureSpec);
const widthMode = layout.getMeasureSpecMode(widthMeasureSpec);