|
| 1 | +import { Button, Frame, Label, Page, ScrollView, StackLayout, TabView, TabViewItem, Trace } from '@nativescript/core'; |
| 2 | + |
| 3 | +/** |
| 4 | + * Playground for a Frame hosted as a TabViewItem view (issue #11444): every tab frame mounts |
| 5 | + * its first page in `loaded`, and the inner pages expose the navigation and frame-stack |
| 6 | + * probes used to reproduce the reported stalls. |
| 7 | + */ |
| 8 | +export interface NestedTabFrameOptions { |
| 9 | + tabCount: number; |
| 10 | + androidOffscreenTabLimit?: number; |
| 11 | +} |
| 12 | + |
| 13 | +const TITLES = ['Home', 'Demos', 'Third', 'Fourth']; |
| 14 | + |
| 15 | +export const registry = { tabView: null as TabView, frames: [] as Frame[] }; |
| 16 | + |
| 17 | +export function log(msg: string) { |
| 18 | + console.log(`[NTF] ${msg}`); |
| 19 | +} |
| 20 | + |
| 21 | +export function describe(frame: Frame): string { |
| 22 | + const f = frame as any; |
| 23 | + return `isLoaded=${frame.isLoaded} current=${frame.currentPage ? frame.currentPage.id : null} backStack=${frame.backStack.length} executing=${!!f._executingContext} queue=${f._navigationQueue.length} inStack=${f._isInFrameStack}`; |
| 24 | +} |
| 25 | + |
| 26 | +export function makePage(frame: Frame, depth: number): Page { |
| 27 | + const page = new Page(); |
| 28 | + page.id = `${frame.id}-page${depth}`; |
| 29 | + page.actionBarHidden = true; |
| 30 | + const stack = new StackLayout(); |
| 31 | + stack.className = 'p-20'; |
| 32 | + |
| 33 | + const label = new Label(); |
| 34 | + label.text = `${page.id}`; |
| 35 | + label.className = 'h2 text-center'; |
| 36 | + stack.addChild(label); |
| 37 | + |
| 38 | + const addButton = (text: string, cls: string, onTap: () => void) => { |
| 39 | + const btn = new Button(); |
| 40 | + btn.text = text; |
| 41 | + btn.className = cls; |
| 42 | + btn.on('tap', onTap); |
| 43 | + stack.addChild(btn); |
| 44 | + }; |
| 45 | + |
| 46 | + addButton(`Push page ${depth + 1}`, 'btn btn-primary', () => { |
| 47 | + log(`push from ${page.id}: ${describe(frame)}`); |
| 48 | + frame.navigate({ create: () => makePage(frame, depth + 1) }); |
| 49 | + log(`after push: ${describe(frame)}`); |
| 50 | + }); |
| 51 | + addButton(`Push page ${depth + 1} (animated=false)`, 'btn btn-primary', () => { |
| 52 | + log(`push (no anim) from ${page.id}: ${describe(frame)}`); |
| 53 | + frame.navigate({ create: () => makePage(frame, depth + 1), animated: false }); |
| 54 | + log(`after push: ${describe(frame)}`); |
| 55 | + }); |
| 56 | + addButton('Go back', 'btn btn-outline', () => { |
| 57 | + log(`goBack from ${page.id}: canGoBack=${frame.canGoBack()} ${describe(frame)}`); |
| 58 | + frame.goBack(); |
| 59 | + log(`after goBack: ${describe(frame)}`); |
| 60 | + }); |
| 61 | + addButton('Log status', 'btn btn-outline', () => { |
| 62 | + log(`status ${frame.id}: ${describe(frame)} topmost=${Frame.topmost()?.id}`); |
| 63 | + }); |
| 64 | + addButton('callLoaded() workaround', 'btn btn-outline', () => { |
| 65 | + frame.callLoaded(); |
| 66 | + log(`after callLoaded: ${describe(frame)}`); |
| 67 | + }); |
| 68 | + registry.frames.forEach((f, i) => { |
| 69 | + addButton(`Select tab ${TITLES[i]}`, 'btn btn-outline', () => { |
| 70 | + log(`programmatic select tab ${i} (${TITLES[i]}) from ${page.id}; topmost=${Frame.topmost()?.id}`); |
| 71 | + registry.tabView.selectedIndex = i; |
| 72 | + log(`after select: ${TITLES[i]} ${describe(registry.frames[i])} topmost=${Frame.topmost()?.id}`); |
| 73 | + }); |
| 74 | + }); |
| 75 | + addButton('Frame.topmost().navigate()', 'btn btn-outline', () => { |
| 76 | + const top = Frame.topmost() as Frame; |
| 77 | + log(`topmost navigate: topmost=${top?.id} (this page's frame=${frame.id}) before ${describe(top)}`); |
| 78 | + top.navigate({ create: () => makePage(top, (top.backStack.length || 0) + 2) }); |
| 79 | + log(`after topmost navigate: ${describe(top)}`); |
| 80 | + }); |
| 81 | + addButton('Frame.topmost().goBack()', 'btn btn-outline', () => { |
| 82 | + const top = Frame.topmost() as Frame; |
| 83 | + log(`topmost goBack: topmost=${top?.id} canGoBack=${top?.canGoBack()} ${describe(top)}`); |
| 84 | + top.goBack(); |
| 85 | + log(`after topmost goBack: ${describe(top)}`); |
| 86 | + }); |
| 87 | + addButton('Reassign items (new array)', 'btn btn-outline', () => { |
| 88 | + const tv = registry.tabView; |
| 89 | + log(`reassign items: selectedIndex=${tv.selectedIndex} before ${describe(frame)}`); |
| 90 | + tv.items = [...tv.items]; |
| 91 | + log(`after reassign: selectedIndex=${tv.selectedIndex} ${describe(frame)} topmost=${Frame.topmost()?.id}`); |
| 92 | + }); |
| 93 | + addButton('Log all frames', 'btn btn-outline', () => { |
| 94 | + registry.frames.forEach((f) => log(` ${f.id}: ${describe(f)}`)); |
| 95 | + log( |
| 96 | + ` topmost=${Frame.topmost()?.id} stack=[${(Frame as any) |
| 97 | + ._stack() |
| 98 | + .map((f: Frame) => f.id) |
| 99 | + .join(', ')}]`, |
| 100 | + ); |
| 101 | + }); |
| 102 | + addButton('Select next tab + push there', 'btn btn-outline', () => { |
| 103 | + const n = registry.frames.length; |
| 104 | + const i = (registry.tabView.selectedIndex + 1) % n; |
| 105 | + const target = registry.frames[i]; |
| 106 | + log(`select next tab ${i} + push: before ${describe(target)}`); |
| 107 | + registry.tabView.selectedIndex = i; |
| 108 | + log(`after select: ${describe(target)} topmost=${Frame.topmost()?.id}`); |
| 109 | + target.navigate({ create: () => makePage(target, (target.backStack.length || 0) + 2) }); |
| 110 | + log(`after push on ${target.id}: ${describe(target)}`); |
| 111 | + }); |
| 112 | + addButton('Push + switch tab', 'btn btn-outline', () => { |
| 113 | + const n = registry.frames.length; |
| 114 | + const i = (registry.tabView.selectedIndex + 1) % n; |
| 115 | + log(`push on ${frame.id} then switch to tab ${i}`); |
| 116 | + frame.navigate({ create: () => makePage(frame, depth + 1) }); |
| 117 | + registry.tabView.selectedIndex = i; |
| 118 | + log(`after push+switch: ${describe(frame)} topmost=${Frame.topmost()?.id}`); |
| 119 | + }); |
| 120 | + |
| 121 | + const scroll = new ScrollView(); |
| 122 | + scroll.content = stack; |
| 123 | + page.content = scroll; |
| 124 | + page.on('loaded', () => log(`${page.id} loaded; frame ${describe(frame)}`)); |
| 125 | + page.on('unloaded', () => log(`${page.id} unloaded; frame ${describe(frame)}`)); |
| 126 | + page.on('navigatedTo', () => log(`${page.id} navigatedTo; frame ${describe(frame)}`)); |
| 127 | + return page; |
| 128 | +} |
| 129 | + |
| 130 | +export function buildTabView(opts: NestedTabFrameOptions): TabView { |
| 131 | + Trace.enable(); |
| 132 | + Trace.addCategories(Trace.categories.concat(Trace.categories.Navigation, Trace.categories.Transition, Trace.categories.NativeLifecycle)); |
| 133 | + |
| 134 | + const tabView = new TabView(); |
| 135 | + if (opts.androidOffscreenTabLimit !== undefined) { |
| 136 | + tabView.androidOffscreenTabLimit = opts.androidOffscreenTabLimit; |
| 137 | + } |
| 138 | + registry.tabView = tabView; |
| 139 | + registry.frames = []; |
| 140 | + const items: TabViewItem[] = []; |
| 141 | + for (const title of TITLES.slice(0, opts.tabCount)) { |
| 142 | + const item = new TabViewItem(); |
| 143 | + item.title = title; |
| 144 | + const frame = new Frame(); |
| 145 | + frame.id = `frame-${title}`; |
| 146 | + frame.on('loaded', () => { |
| 147 | + log(`${frame.id} loaded: ${describe(frame)}`); |
| 148 | + if (!frame.currentPage && !(frame as any)._executingContext && (frame as any)._navigationQueue.length === 0) { |
| 149 | + log(`${frame.id} mounting initial page`); |
| 150 | + frame.navigate({ create: () => makePage(frame, 1) }); |
| 151 | + } |
| 152 | + }); |
| 153 | + frame.on('unloaded', () => log(`${frame.id} unloaded: ${describe(frame)}`)); |
| 154 | + item.view = frame; |
| 155 | + items.push(item); |
| 156 | + registry.frames.push(frame); |
| 157 | + } |
| 158 | + tabView.items = items; |
| 159 | + tabView.on('selectedIndexChanged', (a: any) => log(`tab selectedIndexChanged ${a.oldIndex} -> ${a.newIndex}`)); |
| 160 | + tabView.on('loaded', () => log(`tabView loaded`)); |
| 161 | + tabView.on('unloaded', () => log(`tabView unloaded`)); |
| 162 | + return tabView; |
| 163 | +} |
| 164 | + |
| 165 | +export function setupPage(page: Page, opts: NestedTabFrameOptions) { |
| 166 | + log(`---- setup ${JSON.stringify(opts)} ----`); |
| 167 | + page.content = buildTabView(opts); |
| 168 | +} |
0 commit comments