返回博客首页
← 所有文章

Harness the power of CollectionView(第一部分) - iOS 的可滑动单元格

2023 年 5 月 5 日 — 作者:Nathan Walker

这是系列文章的第 1 部分,重点介绍CollectionView,这是一个功能强大的列表控件,它使用可回收的行,在每个平台上都能提供最佳的体验,适合各种需求。

👉 演示仓库

截至本系列文章撰写之时,可以使用多种列表控件与 NativeScript 配合使用,包括 @nativescript/core 的 ListViewRadListView,以及社区的 CollectionView。根据您的需要,任何一种都可能是合适的。

  • Core 的ListView 最适合简单的用例,只需要一个带可自定义行样式的列表。
  • RadListView 曾经是一个功能强大的列表控件,具有高级功能,例如可滑动行、排序、分组、下拉刷新、按需加载等,但它随着时间的推移而老化,社区已经出现了功能更强大、可扩展性更高的替代者。
  • CollectionView 由才华横溢的长期贡献者 Martin Guillon 带给社区,它提供了现代的 iOS 和 Android 平台功能,并具有理想的性能。

CollectionView 是 RadListView 的自然继承者,因为它在 iOS 上使用 UICollectionView,在 Android 上使用 RecyclerView,两者都是针对列表处理的高度优化的平台控件。

利用 layoutStyle 自定义行为

CollectionView 支持layoutStyle 属性,允许您针对控件注册自定义平台行为,例如

<CollectionView layoutStyle="swipe" />

这可以是任何string 值,用于识别自定义注册的布局样式。

CollectionView.registerLayoutStyle('swipe', {
  createLayout: () => {
    // return a customized platform layout here!
    return layout;
  },
});

iOS 上可能的布局可以是 UICollectionViewLayout 家族中的任何布局,该家族非常丰富且广泛。它提供了一个抽象的基类,用于各种布局,例如 UICollectionViewFlowLayoutUICollectionViewCompositionalLayout

我们不会在这里介绍所有可用的布局,而是将重点放在使用其中一个布局实现滑动单元格。恰好是所有 iOS 设备上官方 iOS 邮件应用所使用的相同布局,即 UICollectionViewCompositionalLayout

您可以找到许多关于此主题的教程,以下是一些示例:

NativeScript 最棒的一点是,您想做的事情的文档和教程已经在各种平台资源中提供,因为毕竟,NativeScript 就是用 JavaScript 提供了这些平台功能!

实现 UICollectionViewCompositionalLayout 以创建可滑动单元格

我们可以使用一个 API 创建一个。

UICollectionViewCompositionalLayout.layoutWithListConfiguration(config);

那么我们想要什么配置呢?...一个带有引导和结尾滑动操作的配置!

使用上面提到的教程作为指南,我们可以实现一个引导滑动操作和一个结尾滑动操作,如下所示:

CollectionView.registerLayoutStyle('swipe', {
  createLayout: () => {
    const config =
      UICollectionLayoutListConfiguration.alloc().initWithAppearance(
        UICollectionLayoutListAppearance.Plain
      );
    config.showsSeparators = true;

    config.leadingSwipeActionsConfigurationProvider = (p1: NSIndexPath) => {
      const readAction =
        UIContextualAction.contextualActionWithStyleTitleHandler(
          UIContextualActionStyle.Normal,
          'Read',
          (
            action: UIContextualAction,
            sourceView: UIView,
            actionPerformed: (p1: boolean) => void
          ) => {
            console.log('read actionPerformed!');
            actionPerformed(true);
          }
        );
      readAction.backgroundColor = UIColor.systemBlueColor;
      readAction.image = UIImage.systemImageNamed('envelope.badge.fill');
      return UISwipeActionsConfiguration.configurationWithActions([readAction]);
    };

    config.trailingSwipeActionsConfigurationProvider = (p1: NSIndexPath) => {
      const moreAction =
        UIContextualAction.contextualActionWithStyleTitleHandler(
          UIContextualActionStyle.Normal,
          'More',
          (
            action: UIContextualAction,
            sourceView: UIView,
            actionPerformed: (p1: boolean) => void
          ) => {
            console.log('more actionPerformed!');
            actionPerformed(true);
          }
        );
      moreAction.backgroundColor = UIColor.systemGray4Color;
      moreAction.image = UIImage.systemImageNamed('ellipsis.circle.fill');
      return UISwipeActionsConfiguration.configurationWithActions([moreAction]);
    };

    return UICollectionViewCompositionalLayout.layoutWithListConfiguration(
      config
    );
  },
});

如果我们在 iOS 上运行它(ns debug ios),并使用 CollectionView 标记语言,用 TailwindCSS 样式化,像这样

<CollectionView [items]="items" layoutStyle="swipe">
  <ng-template let-item="item">
    <GridLayout class="p-2">
      <Label text="CollectionView is really great" class="text-lg"></Label>
    </GridLayout>
  </ng-template>
</CollectionView>

我们将看到以下结果:

就这么简单?没错。

你的意思是,只需要这么做就可以拥有与 iOS 邮件应用相同的 UX 的可滑动单元格?是的。

使用的图像图标是 iOS 上已经可用的内置系统图标。

NativeScript 的基本原则之一是,它的方法旨在让平台始终成为指导灯塔。如果您要使用跨平台方法,它希望确保平台始终如一地展现自己的真实面貌,绝不限制您,而是为您提供更多选择。

进一步自定义 CollectionView

NativeScript 社区高度可扩展。如果您遇到意外情况该怎么办?

让我们以此为例说明这一点。扩展我们的示例,在我们的行布局中添加更多细节,以更全面地反映 iOS 邮件应用本身的布局,我们可以这样表示布局:

<CollectionView [items]="items" layoutStyle="swipe">
  <ng-template let-item="item">
    <GridLayout rows="auto,auto,auto" class="pl-4 pr-2 py-4 v-center">
      <Label
        [text]="item.name"
        class="text-[17px] font-bold align-middle"
      ></Label>
      <Label
        row="1"
        [text]="item.subject"
        class="text-base leading-none align-middle"
      ></Label>
      <Label
        row="2"
        [text]="item.body"
        class="text-base leading-none text-gray-500"
        maxLines="2"
      ></Label>
      <GridLayout rowSpan="3" columns="auto,auto" class="align-top h-right">
        <Label
          [text]="item.date | date : 'shortDate'"
          class="text-sm align-middle text-gray-500"
        ></Label>
      </GridLayout>
    </GridLayout>
  </ng-template>
</CollectionView>

现在我们将看到这个问题:

CollectionView row height issue

这恰好是 UICollectionViewCompositionalLayout 中动态单元格高度的自然 iOS 行为,部分描述如下:

问题是,当使用包含自定义布局并支持滑动的行列表样式的组合布局时,布局逻辑需要一些额外的考虑,才能正确计算行高。

在这些情况下,我们有许多选择。

  • A.NativeScript 社区 Discord 上寻求帮助
  • B. 修改node_modules 中的插件,找到一个可行的解决方案
  • C. 分叉插件仓库,尝试修改源代码
  • D. 如果无法运行插件仓库,请将源代码移到一个方便您进行尝试的位置
  • E. 找到解决方案后,将解决方案贡献给原始插件作者,供其考虑在未来版本中加入
  • F. 在插件仓库中报告问题
  • G.专业合作伙伴 寻求帮助

您可能会发现这些方法中的任何一个都有帮助,我们鼓励您尝试任何一种。在本例中,我们将直接转到 E,因为在本系列文章中,我们将涵盖相当广泛的内容,并希望确保能够轻松地自定义任何内容。我们只需将源代码从 nativescript-community/ui-collectionview 移动到 nstudio/nativescript-ui-kit,该仓库使用推荐的 插件工作区。这样就将其置于我们进行任何修改时都感到舒适的设置中。然后,我们可以稍后决定原始作者可能希望通过向原始源代码仓库提交拉取请求来实现哪些自定义功能。

在很多情况下,patch-package 甚至可以用来调整node_modules 中的插件,如果需要的话,它在这个案例中也同样适用。

为了清楚起见,让我们看一下所需的源代码更改。我们需要在 CollectionViewCell 中添加一个方法,该方法目前没有被导出用于自定义(可能会成为作者未来的 PR!)。

@NativeClass
class CollectionViewCell extends UICollectionViewCell {
  owner: WeakRef<ItemView>;

  get view(): ItemView {
    return this.owner ? this.owner.deref() : null;
  }

  // We needed this!
  systemLayoutSizeFittingSizeWithHorizontalFittingPriorityVerticalFittingPriority(
    targetSize: CGSize,
    horizontalFittingPriority: number,
    verticalFittingPriority: number
  ): CGSize {
    const owner = this.owner?.deref();
    if (owner) {
      const dimensions = {
        measuredWidth: owner.getMeasuredWidth(),
        measuredHeight: owner.getMeasuredHeight(),
      };
      return CGSizeMake(
        Utils.layout.toDeviceIndependentPixels(dimensions.measuredWidth),
        Utils.layout.toDeviceIndependentPixels(dimensions.measuredHeight)
      );
    }
    return targetSize;
  }
}

有了这个,我们就得到了带有可滑动单元格的自定义单元格布局,UX 非常棒 👌

自定义和发布,还是 PR 等待?

这是一个每次都会遇到的难题。通常情况下,我们始终尽最大努力对插件更改进行 PR,并等待作者审核以便在未来的版本中发布 - 在这种情况下,如果可能的话,我们真的很喜欢与作者进行沟通。以下是一些我们推荐的准则:

  1. 通过上述 A-G 选项中的任何一种找到解决方案。
  2. 使用您的项目中的patch-package 集成解决方案以解除自己的阻塞,或者如果您克隆了源代码以准备拉取请求,则可以将更改npm pack 到一个.tgz 文件中,并在同时将其引用到您的项目中。
  3. 提交拉取请求,开始与插件作者进行对话。

我们发布了这个自定义的CollectionView 到 npm 上(@nstudio/ui-collectionview),因为我们将开始在本系列文章中使用它,包括在 StackBlitz 上,甚至在探索其他方向的一些项目中使用。我们甚至可能在本系列文章中转向全新的方向,因此我们将有一个自定义的目标来安全地做到这一点,不会影响社区。在我们完成所有想涵盖的内容后,我们可以准备一个拉取请求,其中包含我们认为可能对原始作者以及更广泛的社区都有益的所有更改。

关于 nStudio

nStudio 成立于 2016 年,由开源合作者创建,以建立健康的开源治理模式,为围绕 NativeScript 的全球社区利益服务。如果您需要专业帮助来完成您的项目,nStudio 提供涵盖多个学科的服务,可以通过以下地址联系:[email protected].