返回博客首页
← 所有文章

访客博文:深入了解 NativeScript 的 TabView

2016年5月24日 — 作者:Dan Wilson

这是一篇由Bradley Gore撰写的访客博文。如果您想与我们合作撰写访客博文,请在 Twitter 上与我们联系 @NativeScript

如果您正在构建移动应用,很快就会需要一个选项卡界面。选项卡界面在移动领域被广泛使用,使用 NativeScript 构建的应用也不例外。幸运的是,有一个 TabView 组件。让我们来了解一下它,好吗?

*视频教程 在此

入门

要开始使用 TabView,我们只需在 XML 文件中声明它,然后提供一些项目来填充视图。

<Page loaded="onPageLoaded">
    <TabView>
        <TabView.items>
            <TabViewItem title="Left Tab">
                <TabViewItem.view>
                    <Label text="Hi there, I'm Left Tab's Content!" />
                </TabViewItem.view>
            </TabViewItem>
            <TabViewItem title="Right Tab">
                <TabViewItem.view>
                    <Label text="Howdy, I'm Right Tab's Content!" />
                </TabViewItem.view>
            </TabViewItem>
        </TabView.items>
    </TabView>
</Page>

如您所见,我们有一个组件,它包含一组项目,TabView.items。每个项目都是一个 TabViewItem,它具有一个 title 和一个 view。标题显示在选项卡中,而 TabViewItem.view 在选择该选项卡时显示在屏幕上。您可以提供大量其他属性(selectedIndexselectedBackgroundColortabsBackgroundColor 等),但这正是使 TabView 正常工作所需的关键。

就这么简单……或者,至少看起来是这样

尽管这是给出的标准示例——实际上,它只是从 NativeScript 的食谱条目 中稍微修改的副本——但 TabView 的真相是,它是那些易于学习但难以掌握的事物之一。当您拥有更大的视图、每个选项卡更复杂的视图模型等时,事情可能会变得有点棘手……所以我们需要更好地了解我们的 TabView 以便将其屈服于我们的意志成功地使用它 :)

让我们逐步了解有效使用 TabView 的这 4 个关键要素。

  • 为每个选项卡的视图使用单独的文件 - 请相信我。没有人希望在一个包含 5 个单独选项卡的标记的 1100 行 XML 文件中工作。仅仅因为它们都位于单个 TabView 组件下,并不意味着它们都必须放在一起……养成这样做的习惯,您会为此感到高兴的。我会向您展示如何操作 :)
  • 传递选项卡更改事件 - 如果我们要将视图分离到各自的文件(XML 和 JS),那么我们将希望每个视图实例能够知道何时是活动视图。
  • 管理视图的加载/卸载 - TabView 在加载和卸载 TabViewItem 视图的时间方面具有一些独特的特性(至少在 Android 上是这样,我目前主要关注 Android),了解这一点可以带来巨大的改变。
  • 管理视图模型 - 现在我们的选项卡视图已分离出来,为每个选项卡创建单独的视图模型很容易。但是,如果我们需要一个在两个或多个选项卡中通用的模型怎么办?好吧,我很高兴您提出这个问题 ;-)

提示:我展示的管理视图模型的技术实际上不仅适用于 TabView - 它们可以应用于您组件化的任何视图 :)

TabViewItem 视图的单独文件

这是有效使用 TabView 的第一个关键,其益处将立即显现。让我们继续我们开始的示例 - 我们有两个 TabViewItems(左侧选项卡和右侧选项卡)驻留在 TabView 中,我们希望将它们分离出来。让我们从文件结构开始,因为这将有助于其他内容在我们继续时变得有意义。这是我组织它的方式

  • /app
    • /myTabView
      • myTabView.js
      • myTabView.xml
      • /leftTab
        • leftTab.js
        • leftTab.xml
      • /rightTab
        • rightTab.js
        • rightTab.xml

在组织好文件后,我将使用 XML 命名空间来引入我们的视图(请参阅 这些 NativeScript 文档我之前发布的这篇文章 以了解其工作原理)。因此,我用于 myTabView.xml 的 XML 将如下所示

<Page>
    <TabView>
        <TabView.items>
            <TabViewItem title="Left Tab" xmlns:LeftTab="myTabView/leftTab">
                <TabViewItem.view>
                    <LeftTab:leftTab />
                </TabViewItem.view>
            </TabViewItem>
            <TabViewItem title="Right Tab" xmlns:RightTab="myTabView/rightTab">
                <TabViewItem.view>
                    <RightTab:rightTab />
                </TabViewItem.view>
            </TabViewItem>
        </TabView.items>
    </TabView>
</Page>

现在可能看起来变化不大,但想象一下这两个选项卡中的每一个都包含数十行 XML,然后在相同条件下再添加几个 TabViewItem,差异就会变得很明显。

传递选定的选项卡更改

现在我们的视图已分离出来,它们各自拥有自己的 XML 和 JS 文件 - 这意味着为整个选项卡的视图设置 bindingContext 就像您对任何页面所做的那样,并且可以为我们的视图连接 loadedunloaded 等事件。但是,我们的视图如何知道它是否是被选中的视图?幸运的是,TabView 为此提供了一个 事件,我们可以在各个视图中监听它。以下是我们的 leftTab.xmlleftTab.js 文件的外观

*注意 - 我在示例中将使用 TypeScript,以便您可以看到每个对象的类型,但纯 JavaScript 的工作方式相同。

<!-- leftTab.xml --> 
<stack-layout loaded="onViewLoaded" unloaded="onViewUnloaded">  
  <!-- entirety of the tab's content -->
</stack-layout>


//leftTab.ts (TypeScript)
import {TabView, SelectedIndexChangedEventData} from 'ui/tab-view';  
import {View} from 'ui/core/view';  
import {EventData} from "data/observable";

const THIS_TAB_IDX: number = 0; //index at which this tab resides  
var thisView: View;  
var isThisTabSelected: boolean = false;

function onTabChanged(evt: SelectedIndexChangedEventData) {  
  isThisTabSelected = evt.newIndex === THIS_TAB_IDX;
}

export function onViewLoaded(args: EventData) {  
  //args.object is the reference to the <stack-layout> view in leftTab.xml
  thisView: View = <View>args.object,
  let tabView: TabView = thisView.parent;
  isThisTabSelected = tabView.selectedIndex === THIS_TAB_IDX;
  tabView.on(TabView.selectedIndexChangedEvent, onTabChanged);
}

export function onViewUnloaded(args: EventData) {  
  //TabView's items' views get loaded/unloaded as user navigates, so clean up handlers, etc...
  let tabView: TabView = thisView.parent;
  tabView.off(TabView.selectedIndexChangedEvent, onTabChanged);
}

由于任何 View 实例都具有对其父级的句柄,并且由于我们在此实例中知道我们的父级是 TabView,因此我们可以简单地利用其 selectedIndexChangeEvent。此外,请注意,我们正在 onViewUnloaded 事件中取消订阅我们的处理程序 - 这非常重要。否则,如果您有足够的选项卡来保证在用户导航时加载/卸载视图,那么您的事件处理程序将在您的 View 卸载后继续存在,并导致在每次选项卡更改事件中多次运行相同的处理程序函数。

如果您遇到更动态的情况,例如您可能根据应用程序状态隐藏/显示选项卡,并且不能依赖于静态定义的 THIS_TAB_IDX,那么您可以根据新的选中索引检查选定的 TabViewItem。例如

//update import to include TabViewItem
import {TabView, TabViewItem, SelectedIndexChangedEventData} from 'ui/tab-view';

function onTabChanged(evt: SelectedIndexChangedEventData) { 
  let tabView: TabView = <TabView>thisView.parent, selectedTabViewItem: TabViewItem = tabView.items[evt.newIndex];

  isThisTabSelected = selectedTabViewItem.view === thisView;
}

每个 TabViewItem 实例在其 .view 属性上有一个对其 View 的引用,因此我们可以只检查我们对视图的引用与选定项目所说的其视图是否相等。还可以使用 .title 属性。

 

TabViewItem 视图的加载和卸载

关于 TabView,一个有趣的事情是如何处理每个 TabViewItem 的视图的(卸载)。当 TabView 首次加载时,它会立即加载所有项目的视图。但是,当您在选项卡之间导航时,它会开始卸载距离当前选定选项卡超过一个选项卡的选项卡,以及加载任何已卸载且仅距离一个选项卡的选项卡。例如,如果您有 4 个选项卡,以下是发生的情况

TabView 加载:所有 4 个 TabViewItem 视图都已加载
选择第三个选项卡:第一个选项卡被卸载
选择第四个选项卡:第二个选项卡被卸载
选择第三个选项卡:第二个选项卡被加载
选择第二个选项卡:第一个选项卡被加载 + 第四个选项卡被卸载

这确实是那些更容易展示而不是解释的事情之一,因此我在随附此帖子的视频中详细证明了这一点。掌握这些知识可以极大地改变您与 TabView 的交互方式。

管理视图模型

这部分一开始对我来说很困难。我为此苦苦挣扎,在 NativeScript Slack 中向 Nathanael Anderson 提出了问题,并尝试了多种方法,才最终对此感到满意。归根结底,如果您理解这两个关键概念,它实际上非常简单

  1. 您可以将单独的 bindingContext 附加到您想要的任何视图上。.
  2. 每个 View 实例都有一个 _onBindingContextChanged(继承自 ui/core/bindable),如果需要,您可以对其进行修补。

让我们探索一个用例,其中我们有一个包含 TabViewPage,当导航到 Page 时,它会设置绑定上下文,并且其中一个 TabViewItem 组件将使用该绑定上下文中的某些内容作为其视图的绑定上下文。假设数据不是异步提供的,我们可以使用第一种技术,即仅为特定 View 使用与整个 Page 不同的 bindingContext

页面的 XML 和 JS

<!--myTabView.xml-->
<Page navigatingTo="onPageNavigatingTo">
    <TabView>
        <TabView.items>
            <TabViewItem title="Widgets List" xmlns:WidgetsListTab="myTabView/widgetsList">
                <TabViewItem.view>
                    <WidgetsListTab:widgetsList />
                </TabViewItem.view>
            </TabViewItem>
            <!-- more tab view items... -->
        </TabView.items>
    </TabView>
</Page>


export function onPageNavigatingTo(arg) {  
  //set the binding context for the page
  arg.object.bindingContext = {
    widgets: [
      {id: 1, name="Turtles", price: 10, qty: 276, sold: 120},
      //all teh other widgetz goez here
    ]
  };
}

Widget 列表选项卡项目的 XML 和 JS

<!--myTabView/widgetsList/widgetsList.xml-->
<stack-layout loaded="onTabViewLoaded">
    <grid-layout rows="auto, auto, auto" columns="2*, *" id="widgetsSummary">
        <label text="Total Widgets" />
        <label col="1" text="{{ widgetsCount }}" />

        <label row="1" col="0" text="Avg Widget Price" />
        <label row="1" col="1" text="{{ avgWidgetPrice }}" />

        <label row="2" col="0" text="Best Selling Widget" />
        <label row="2" col="1" text="{{ bestSellingWidget.name }}" />
    </grid-layout>
</stack-layout>


export function onTabViewLoaded(arg) {  
  let thisView = arg.object,
      allWidgets = thisView.page.bindingContext.widgets, 
 widgetsSummaryView = thisView.getViewById('widgetsSummary'),
      avgWidgetPrice,
      bestSellingWidget;

  avgWidgetPrice = allWidgets.reduce((sum, w) => sum + w.price, 0)) / allWidgets.length;

  bestSellingWidget = allWidgets
    .sort((a, b) => a.sold < b.sold ? 1 : a.sold > b.sold ? -1 : 0 )[0]

  // set the bindingContext for the <grid-layout>
  widgetsSummaryView.bindingContext = {
    widgetsCount: allWidgets.length,
    avgWidgetPrice: avgWidgetPrice,
    bestSellingWidget = bestSellingWidget    
  };
}


如果您的数据不是异步加载的(例如,来自 Web 服务)或需要从 Page 派生单独 bindingContextView 不是最初选定的选项卡,那么这可以很好地工作。但是,如果您的数据是异步的,并且最初选定的选项卡需要从 PagebindingContext 派生一些数据,该怎么办?在这里,我们可以修补 _onBindingContextChanged 处理程序。让我们采用与上面相同的示例,以便我们只需显示更新的 JavaScript 文件即可

页面

import * as myWidgetSvc from './dal/widgetsvc';

export function onPageNavigatingTo(arg) {  
  myWidgetSvc
    .getAllTheWidgetz()
    .then(widgets => arg.object.bindingContext = {widgets: widgets});
}


我们不知道这些数据何时真正到来 - 并且如果您将选项卡项目的视图隔离,它们将不知道何时可以安全地尝试从 Page 数据派生其数据,因此我们可以这样做

Widget 列表选项卡项目的 JS

var thisView;

export function onTabViewLoaded(arg) { 
  thisView = arg.object;

  if (thisView.page.bindingContext && thisView.page.bindingContext.widgets) {
    populateViewData();
  } else {
    let origBindingContextChanged = thisView._onBindingContextChanged;

    thisView._onBindingContextChanged = (old, newContext) => {
      origBindingContextChanged(old, newContext);
      thisView._onBindingContextChanged = origBindingContextChanged;
      populateViewData();
    };
  }
}

function populateViewData() { 
  //set the bindingContext of widgetSummary after deriving necessary data
}


这是因为您为 Page 设置的数据上下文适用于其中包含的任何项目。这实际上适用于任何 View。因此,我们的 View 收到了其回调的调用,因为来自 Page navigatingTo 事件的 Promise 已解析并且其数据上下文已更新,但我们基本上拦截了它,然后更改了子项(widgetSummary)的上下文。

现在,在经历了这一切之后,希望您比开始时对 TabView 有了更深入的了解!我希望这对您有所帮助!如果是,请留下评论告诉我,或在 Twitter 上与我联系!此外,请随时查看 我在 NativeScript 上的其他文章

-Bradley