返回博客首页
← 所有文章

如何在 NativeScript 中制作可折叠的自定义 ActionBar

2017 年 11 月 17 日 - 作者 Shiva Prasad

当您查看遵循 Material Design 规范的应用程序时,折叠工具栏是最常见的行为之一。在这种模式下,当用户向下滚动页面时,ActionBar 会折叠,而当用户向上滚动页面时,ActionBar 会返回。
注意:我将在本文中分享的代码适用于 NativeScript Core 和 JavaScript。
如果您试图在 NativeScript 中实现这一点,如果页面只是包含一些由 ScrollView 包围的布局,那么就相当简单,因为您将拥有要监听的滚动事件,并可以相应地更改 ActionBar,例如当用户向下滚动时设置 page.actionBarHidden = true。

但在您的页面中包含 ListView 的情况下,事情并不那么简单。这就是我要分享这个技巧的原因,我使用这个技巧可以让包含 ListView 的页面拥有可折叠的 ActionBar。所以,在这篇文章的最后,您将了解如何制作一个像左侧图像一样的页面,将 ActionBar 折叠起来,以便它看起来像用户向下滚动时右侧图像。

shiva-1
首先,我不得不遗憾地告诉您,使用 NativeScript 中开箱即用的 ActionBar 来创建这种效果并不容易。所以我们将构建自己的自定义 ActionBar。

步骤 1:在 Page 元素内部,创建一个 GridLayout。这将是使这种效果成为可能的关键元素。此外,不要忘记在 Page 元素上设置 actionBarHidden=”true” 以隐藏 NativeScript 的默认 ActionBar。所以基本布局应该像这样

<Page class="page" loaded="loaded" unloaded="unloaded" navigatedTo="onNavigatedTo"
    xmlns="http://schemas.nativescript.org/tns.xsd"
    actionBarHidden="true">
<GridLayout rows="auto,*" columns="*" backgroundColor="transparent">
   <!-- Rest of the layout goes here -->
  	</GridLayout>
</Page>

步骤 2:接下来,我们将任何加载指示器或占位符文本、ListView 和自定义 ActionBar 都放在同一行和同一列中,按照这个顺序。这是 GridLayout 的内容应该看起来的样子

<ListView rowSpan="2" row="0" col="0"
     itemTemplateSelector="$index === 0 ? 'first' : 'rest'"
           visibility="{{ loaded ? 'visible' : 'collapsed' }}"
           items="{{ listItems }}" id="searchResults">
           <ListView.itemTemplates>
               <template key="first">
                   <GridLayout paddingTop="250">
                       <!-- Your layout, with a paddingTop equal to height of custom action bar -->
                   </GridLayout>
               </template>
               <template key="rest">
                   <GridLayout>
                       <!-- Your layout, without the extra padding top -->
                   </GridLayout>
               </template>
           </ListView.itemTemplates>
       </ListView>
      <ActivityIndicator row="0" col="0" marginTop="150" width="50" height="50" busy="true" visibility="{{ !loaded ? 'visible' : 'collapsed' }}" /> 
         <Label row="0" col="0" class="text-muted h2 text-center" textWrap="true" marginTop="350" text="{{ typeEmptyMsg }}" visibility="{{ isTypeEmpty && loaded ? 'visible' : 'collapsed' }}" /> 
        <!-- Below is your custom action bar, if you don’t surround it with a StackLayout, the GridLayout will take space of entire page -->
   <StackLayout row="0" col="0" id="container">
       <GridLayout id="actionBar" rows="20, 40, 80, auto" columns="75,*,75" class="action-bar p-10">
          <Label paddingTop="10" tap="onNavBtnTap" text="&#xf053;" fontSize="25" fontWeight="100"
color="white" class="fa" row="1" col="0" />
          <Label text="Search Results" color="white"
class="action-bar-title text-center" row="1" col="1"></Label>
            <!-- And rest of your action bar layout as required -->
       </GridLayout>
  </StackLayout>

步骤 3:现在我们已经准备好标记,让我解释一些关键部分。

`itemTemplateSelector="$index === 0 ? 'first' : 'rest'" 这段代码片段允许我们设置一个特殊的键,我们将使用它来区分 ListView 的第一个项目和其他项目。

请注意,我们使用的是 <listview.itemtemplates> 而不是常规的 itemTemplate。这使我们能够指定多个模板,如果满足某个条件,这些模板将被渲染。在我们的例子中,如果列表项是第一个项,那么我们希望添加一个等于自定义 ActionBar 高度的填充,以便当 ActionBar 可见时,列表的第一个项不会被隐藏在自定义 ActionBar 后面。

我们将自定义 ActionBar 包含在一个 StackLayout 中,这样我们就可以阻止 GridLayout 在具有背景色时占用整个屏幕空间。

关于标记就这些了;现在让我们进入有趣的环节,魔法将在那里发生。

步骤 4:在您的代码后端文件中导入 gestures 模块

const gestures = require("ui/gestures"); 
const app = require("application");


此外,在所有函数之外创建这两个变量:let headerCollapsed = false; let lastDelY = 0;

在代码后端文件的 loaded 方法中,您将获得对我们的 listView 元素(本例中为“searchResults”)和“container”元素的引用。将一个 'pan' 手势监听器附加到 listView 上

exports.loaded = function(args) {
   page = args.object;
   // getting view references
   searchResults = page.getViewById("searchResults");
   container = page.getViewById("container");

   searchResults.on(gestures.GestureTypes.pan, function(args) {
       if (Math.round(args.deltaY) < -100 && !headerCollapsed) {
           container.animate({
               translate: { x: 0, y: -230 },
               duration: 500
           });
           headerCollapsed = true;
           hideStatusBar();
           return;
       }
       if (Math.round(args.deltaY > 100) && headerCollapsed) {
           container.animate({
               translate: { x: 0, y: 0 },
               duration: 500
           });
           headerCollapsed = false;
           showStatusBar();
    return;
       }
       if (lastDelY !== Math.round(args.deltaY)) {
           let tmp = container.translateY;
           if (lastDelY < 0 && lastDelY < args.deltaY) {
               args.deltaY *= -1;
           }
           if (lastDelY > 0 && lastDelY > args.deltaY) {
               args.deltaY *= -1;
           }
           tmp += args.deltaY * 0.05;
           if (tmp < -230) {
               tmp = -230;
               hideStatusBar();
           } else if (tmp > 0) {
               tmp = 0;
               showStatusBar();
           }
           lastDelY = Math.round(args.deltaY);
           container.translateY = tmp;
       }
   });
};
function showStatusBar() {
   // Show Status bar android
   if (app.android) {
       const activity = app.android.startActivity;
       const win = activity.getWindow();
       win.clearFlags(
           android.view.WindowManager.LayoutParams.FLAG_FULLSCREEN
       );
   }
}

function hideStatusBar() {
   // Hide Status bar android
   if (app.android) {
       const activity = app.android.startActivity;
       const win = activity.getWindow();
       win.addFlags(
           android.view.WindowManager.LayoutParams.FLAG_FULLSCREEN
       );
   }
}
让我解释一下我在 pan 事件监听器中试图做的事情。首先,当 pan 事件触发时,我们获得 args.deltaX 和 args.deltaY,指示相应方向的变化。我们只想在列表视图中存在超过 5 个项目(根据您的喜好)时才动画化 ActionBar。

第一个“if”块处理用户简单地向下滑动的情况,即 deltaY 的变化很大,小于 -100(通过实验后选择的数值),并且标题尚未折叠。在这种情况下,我们只想将标题动画化到视图之外。
类似地,第二个“if”块处理用户简单地向上滑动的情况。我们想将标题动画化回视图中。

接下来,我们还有另一个“if”块,我允许 ActionBar 平滑地向上滑动。我首先检查我存储的最后一个 delta Y 是否与当前 delta Y 相同。因为当用户缓慢平移时,delta 的变化是十进制的。我不想为如此小的变化移动 ActionBar,因此进行了四舍五入。

这里的第一行,我将容器的 translateY 属性保存到一个临时变量中。第一个“if”块处理用户向上滑动手指并立即改变方向的情况。第二个“if”块处理用户向下滑动手指并立即改变方向的情况。基本上,通过将 delta 乘以 -1,我改变了 ActionBar 移动的方向。

现在,我们只需将 deltaY * 0.05(通过实验后选择的数值)添加到我们的临时变量中。接下来,我们检查临时变量的值是否超过了您必须平移容器才能使其移出视图的像素数量。如果超过了,那么我们将它设置回最大值。如果临时变量大于 0,我们将它设置回 0,因为我们不希望再向下平移 ActionBar。

现在,我们将当前 deltaY 的四舍五入值保存为我们的最后一个 deltaY。最后,我们将容器的 translate Y 属性设置为我们创建的临时变量。最后,不要忘记在 unloaded 事件中删除事件监听器,以防止出现任何意外行为。

exports.unloaded = function(args) {
   searchResults.off(gestures.GestureTypes.pan);
};

就这样。我能够实现类似 这样 的效果,我希望这篇文章帮助您在应用程序中创建可折叠的 ActionBar。享受 NativeScript 编程的乐趣!

注意:此技巧仅在 Android 上进行了测试。因此,在 iOS 上,pan 事件处理程序中建议的实验值可能会有所不同。