卡片导航

大多数基于卡片的插件都是使用多个卡片构建的,这些卡片代表插件界面的不同“页面”。为了提供有效的用户体验,您应该在插件的卡片之间使用简单自然的导航方式。

最初在 Gmail 插件中,界面的不同卡片之间的转换是通过在单个卡片堆栈之间推送和弹出卡片来处理的,其中堆栈的顶部卡片由 Gmail 显示。

首页卡片导航

Google Workspace 插件引入了首页和非内容相关卡片。为了适应内容相关卡片和非内容相关卡片,Google Workspace 插件都有一个内部卡片堆栈。在主机中打开插件时,系统会触发相应的 homepageTrigger,以创建堆栈上的第一张首页卡片(下图中的深蓝色“首页”卡片)。如果未定义 homepageTrigger,则系统会创建、显示默认卡片,并将其推送到非上下文堆栈。第一张卡是根卡。

您的插件可以创建其他非上下文卡片,并在用户浏览您的插件时,将这些卡片推送到堆栈(图中的蓝色“推送的卡片”)。插件界面会显示堆栈中的顶部卡片,因此将新卡片推送到堆栈会更改显示内容,而从堆栈中弹出卡片则会将显示返回到之前的卡片。

如果您的插件定义了定义的上下文触发器,那么当用户进入该上下文时,触发器就会触发。触发器函数会构建上下文卡片,但界面显示效果会根据新卡片的 DisplayStyle 进行更新:

  • 如果 DisplayStyleREPLACE(默认值),上下文卡片(图中的深橙色“上下文”卡片)会替换当前显示的卡片。这实际上是在非上下文卡片堆栈之上启动一个新的上下文卡片堆栈,并且此上下文卡片是上下文堆栈的根卡片。
  • 如果 DisplayStylePEEK,界面会改为创建一个短暂标题,该标题会显示在插件边栏底部,叠加在当前卡片上。提示标题会显示新卡片的标题,并提供用户按钮控件,让用户决定是否查看新卡片。如果他们点击 View 按钮,该卡片会替换当前卡片(如上文所述替换为 REPLACE)。

您可以创建更多上下文卡片,并将其推送到堆栈中(图中的黄色“推送的卡片”)。更新卡片堆栈会更改插件界面,以显示最上方的卡片。如果用户离开上下文,该堆栈中的上下文卡片就会被移除,并且显示内容会更新为最顶部的非上下文卡片或首页。

如果用户进入您的插件未定义上下文触发器的上下文,则不会创建新卡片,并且仍会显示当前卡片。

下述 Navigation 操作仅对来自同一上下文的卡片执行操作;例如,在上下文卡片中,popToRoot() 仅弹出所有其他上下文卡片,而不会影响首页卡片。

相比之下, 按钮始终可供用户从情境卡片导航到非情境卡片。

您可以在卡片堆栈中添加或移除卡片,从而在卡片之间创建过渡效果。Navigation 类提供了用于从堆栈推送和弹出卡片的函数。为了实现有效的卡片导航,请将 widget 配置为使用导航操作。您可以同时推送或弹出多张卡片,但无法移除在插件启动时首先推送到堆栈中的初始首页卡片。

如需导航到新卡片以响应用户与 widget 的互动,请按以下步骤操作:

  1. 创建一个 Action 对象并将其与您定义的回调函数关联。
  2. 调用微件的相应微件处理程序函数,以在该微件上设置 Action
  3. 实现执行导航的回调函数。系统会为此函数指定一个操作事件对象作为参数,并且必须执行以下操作:
    1. 创建 Navigation 对象以定义卡片更改。一个 Navigation 对象可以包含多个导航步骤,这些步骤按照添加到对象中的顺序进行。
    2. 使用 ActionResponseBuilder 类和 Navigation 对象构建 ActionResponse 对象。
    3. 返回构建的 ActionResponse

构建导航控件时,请使用以下 Navigation 对象函数:

函数 说明
Navigation.pushCard(Card) 将卡片推送到当前堆栈。这需要先构建完整的卡片。
Navigation.popCard() 从堆叠顶部移除一张卡片。相当于点击插件标题行中的返回箭头。此操作不会移除根卡。
Navigation.popToRoot() 从堆栈中移除除根卡以外的所有卡片。从实质上是重置该卡片堆栈。
Navigation.popToNamedCard(String) 从堆栈中弹出卡,直到到达具有指定名称或堆栈的根卡的卡。您可以使用 CardBuilder.setName(String) 函数为卡片指定名称。
Navigation.updateCard(Card) 就地替换当前卡片,刷新其在界面中的显示。

如果用户互动或事件应导致在同一上下文中重新呈现卡片,请使用 Navigation.pushCard()Navigation.popCard()Navigation.updateCard() 方法替换现有卡片。用户互动或事件可能导致在其他上下文中重新呈现卡片,请使用 ActionResponseBuilder.setStateChanged() 在这些上下文中强制重新执行插件。

以下是导航示例:

  • 如果互动或事件会更改当前卡片的状态(例如,将任务添加到任务列表),请使用 updateCard()
  • 如果互动或事件提供了更多详细信息或提示用户采取进一步操作(例如,点击某项内容的标题可查看更多详情,或按下某个按钮以创建新的日历活动),请使用 pushCard() 显示新页面,同时允许用户使用返回按钮退出新页面。
  • 如果互动或事件更新了之前卡片中的状态(例如,使用详情视图更新项目标题),请使用 popCard()popCard()pushCard(previous)pushCard(current) 等工具更新上一张卡片和当前卡片。

正在刷新卡片

借助 Google Workspace 插件,用户可以通过重新运行在清单中注册的 Apps 脚本触发器函数来刷新卡片。用户可以通过插件菜单项触发此刷新:

Google Workspace 插件边栏

系统会自动将此操作添加到由 homepageTriggercontextualTrigger 触发器函数生成的卡片,如插件的清单文件(上下文和非上下文卡片堆栈的“根目录”)所指定。

返回多张卡片

插件卡片示例

首页或上下文触发器函数用于构建并返回应用界面显示的单个 Card 对象或 Card 对象数组。

如果只有一张卡,则会将该卡作为根卡添加到非上下文堆栈或上下文堆栈中,然后主机应用界面会显示该卡。

如果返回的数组包含多个已构建的 Card 对象,则托管应用会显示一张新卡片,其中包含每张卡片标头的列表。当用户点击其中任何标题时,界面会显示相应的卡片。

当用户从列表中选择一张卡片时,该卡片将被推送到当前堆栈上,并由托管应用显示。点击 按钮后,用户会返回到卡片标题列表。

如果您的插件不需要在您创建的卡之间进行任何过渡,则这种“平面”卡片排列方式非常实用。但在大多数情况下,最好直接定义卡片转换,让首页和上下文触发器函数返回单个卡片对象。

示例

以下示例展示了如何构建多张卡片,其中有导航按钮可在卡片之间跳转。通过在特定上下文中或之外推送 createNavigationCard() 返回的卡片,可以将这些卡片添加到上下文堆栈或非上下文堆栈。

  /**
   *  Create the top-level card, with buttons leading to each of three
   *  'children' cards, as well as buttons to backtrack and return to the
   *  root card of the stack.
   *  @return {Card}
   */
  function createNavigationCard() {
    // Create a button set with actions to navigate to 3 different
    // 'children' cards.
    var buttonSet = CardService.newButtonSet();
    for(var i = 1; i <= 3; i++) {
      buttonSet.addButton(createToCardButton(i));
    }

    // Build the card with all the buttons (two rows)
    var card = CardService.newCardBuilder()
        .setHeader(CardService.newCardHeader().setTitle('Navigation'))
        .addSection(CardService.newCardSection()
            .addWidget(buttonSet)
            .addWidget(buildPreviousAndRootButtonSet()));
    return card.build();
  }

  /**
   *  Create a button that navigates to the specified child card.
   *  @return {TextButton}
   */
  function createToCardButton(id) {
    var action = CardService.newAction()
        .setFunctionName('gotoChildCard')
        .setParameters({'id': id.toString()});
    var button = CardService.newTextButton()
        .setText('Card ' + id)
        .setOnClickAction(action);
    return button;
  }

  /**
   *  Create a ButtonSet with two buttons: one that backtracks to the
   *  last card and another that returns to the original (root) card.
   *  @return {ButtonSet}
   */
  function buildPreviousAndRootButtonSet() {
    var previousButton = CardService.newTextButton()
        .setText('Back')
        .setOnClickAction(CardService.newAction()
            .setFunctionName('gotoPreviousCard'));
    var toRootButton = CardService.newTextButton()
        .setText('To Root')
        .setOnClickAction(CardService.newAction()
            .setFunctionName('gotoRootCard'));

    // Return a new ButtonSet containing these two buttons.
    return CardService.newButtonSet()
        .addButton(previousButton)
        .addButton(toRootButton);
  }

  /**
   *  Create a child card, with buttons leading to each of the other
   *  child cards, and then navigate to it.
   *  @param {Object} e object containing the id of the card to build.
   *  @return {ActionResponse}
   */
  function gotoChildCard(e) {
    var id = parseInt(e.parameters.id);  // Current card ID
    var id2 = (id==3) ? 1 : id + 1;      // 2nd card ID
    var id3 = (id==1) ? 3 : id - 1;      // 3rd card ID
    var title = 'CARD ' + id;

    // Create buttons that go to the other two child cards.
    var buttonSet = CardService.newButtonSet()
      .addButton(createToCardButton(id2))
      .addButton(createToCardButton(id3));

    // Build the child card.
    var card = CardService.newCardBuilder()
        .setHeader(CardService.newCardHeader().setTitle(title))
        .addSection(CardService.newCardSection()
            .addWidget(buttonSet)
            .addWidget(buildPreviousAndRootButtonSet()))
        .build();

    // Create a Navigation object to push the card onto the stack.
    // Return a built ActionResponse that uses the navigation object.
    var nav = CardService.newNavigation().pushCard(card);
    return CardService.newActionResponseBuilder()
        .setNavigation(nav)
        .build();
  }

  /**
   *  Pop a card from the stack.
   *  @return {ActionResponse}
   */
  function gotoPreviousCard() {
    var nav = CardService.newNavigation().popCard();
    return CardService.newActionResponseBuilder()
        .setNavigation(nav)
        .build();
  }

  /**
   *  Return to the initial add-on card.
   *  @return {ActionResponse}
   */
  function gotoRootCard() {
    var nav = CardService.newNavigation().popToRoot();
    return CardService.newActionResponseBuilder()
        .setNavigation(nav)
        .build();
  }