NativeScript ListView 实现固定分区标题

有时在 NativeScript 中使用 ListView 时你可能想要添加粘性分区标题。在本文中我们看看如何用 Vue 和 NativeScript 来实现。

为实现固定分区标题,除了 ListView 视图外还需要以下视图:

  • Label 用于显示标题
  • GridLayout 用于将 Label 视图堆叠在 ListView 的第一行之上。

以下是将粘性标题添加到 ListView 的步骤:

  1. 准备数据
  2. 将 ListView 和标题 Label 包装在 GridLayout
  3. 监听 ListView 的原生滚动事件

准备数据

我们按如下方式准备 ListView 数据:添加(如果尚未添加)标题数据到 ListView 的 items 数组中,使每个标题出现在 ListView 中对应数据分区的开头。即,数组中的第一个项目将是第一个分区的标题,第二个分区的标题将在第一个分区最后一个项目之后,第三个标题将在第二个分区最后一个项目之后,以此类推。包含标题数据的 items 数组如下所示:

复制代码
[
  "Header 1",
  "item 1",
  "item 2",
  "item 3",
  "Header 2",
  "item 4",
  "item 5",
  "item 6",
  "Header 3",
  "item 7",
  "item 8",
  "item 9",
];

将 ListView 和标题 Label 包装在 GridLayout

将 ListView 和将显示粘性标题的 Label 视图添加到 GridLayout 中,确保以下几点:

  • GridLayout 有一行一列,将标题 Label 堆叠在 ListView 之上。
  • ListView 是 GridLayout 的第一个子元素,以便首先渲染,Label 视图是第二个,以便在 ListView 之上渲染。
  • 设置 Label 视图的 verticalAlignment 属性(如果使用 TailwindCSS,可通过 align-top CSS 类)为 top,使 Label 视图粘贴到 GridLayout 的顶部。
xml 复制代码
<GridLayout>
  <ListView for="item in items" @itemTap="onItemTap">
    <v-template>
      <label :text="item" />
    </v-template>
  </ListView>
  <label text="Header 1" class="align-top" />👈
</GridLayout>
  • Label 视图从第一个标题的文本开始。当用户滚动 ListView 时,我们使用其他标题值更新 Label 视图的文本属性。

Label 视图与包含标题的 ListView 行大小相同。这样当用户滚动 ListView 时,当标题是可见项目列表中的第一个时,Label 将正确覆盖标题行。

监听 ListView 的原生滚动事件

包装类

要监听 ListView 的原生滚动事件,我们可以编写一个 JavaScript 类CurrentHeaderSetter来包装原生代码,并充当原生代码和 Vue 组件之间的桥梁。为了在 iOS 和 Android 平台上共享代码,CurrentHeaderSetter 类扩展了 CurrentHeaderSetterCommon 类。CurrentHeaderSetterCommon 类包含以下成员:

  • _headerLabelView 属性,保存对标题 Label 视图的引用
  • _headers 属性,保存 ListView 标题列表
  • 一个 setCurrentHeader() 方法负责接收来自原生平台的可见行数据,并使用数据检查标题列表与可见行,确定 Label 视图的当前标题值。

监听 iOS 上的滚动事件

要监听 iOS 上的滚动事件,我们需要实现 UITableViewDelegate 协议的 scrollViewDidScroll 方法。为此我们将创建一个用 @NativeClass() 装饰器装饰的类,扩展 NSObject 类,实现 UITableViewDelegate 协议,并具有一个静态 ObjCProtocols 属性绑定到包含 UITableViewDelegate 协议的数组,如下所示:

typescript 复制代码
@NativeClass()
class UITableViewDelegateImpl extends NSObject implements UITableViewDelegate {
  public static ObjCProtocols = [UITableViewDelegate];

  //
}

完整实现,请参见 listview-scroll.ios.ts

原始委托

UITableViewDelegate 协议有许多方法,如 tableView:didSelectRowAtIndexPath:tableView:heightForRowAtIndexPath:,可以启用某些功能。例如,tableView:didSelectRowAtIndexPath: 方法允许您处理用户点击行,而 tableView:heightForRowAtIndexPath: 启用行高自定义。这些方法及更多方法已由 NativeScript Core 工程师实现。因此,我们不需要从头开始编写所有这些方法的实现代码,而是只需在我们的自定义委托类中调用原始委托方法的相应方法,如下所示:

typescript 复制代码
{
  // ...
  tableViewDidSelectRowAtIndexPath(tableView, indexPath) {
    this._originalDelegate.tableViewDidSelectRowAtIndexPath(
      tableView,
      indexPath
    );
  }
  // ...
}

我们通过 ListView 对象的 _delegate 属性获得对该原始委托的引用,如下所示:

typescript 复制代码
this._originalDelegate = (<any>owner.get())._delegate;
// 参见 on-listview-scroll.ios.ts 第14行

scrollViewDidScroll

一旦我们在自定义委托中添加了必要的方法,我们就能实现我们感兴趣的 scrollViewDidScroll 方法,如下所示:

typescript 复制代码
public scrollViewDidScroll(scrollView: UIScrollView): void {
  const items = (this._owner.deref() as ListView).items as string[];
  const indexPathsForVisibleRows = (this._owner.deref() as ListView).ios.indexPathsForVisibleRows as NSArray<NSIndexPath>;

  const visibleItems = Array.from({ length: indexPathsForVisibleRows.count }, (_, i) => i).map(i => {
    const visItem = indexPathsForVisibleRows[i];
    return items[visItem.row] as string;
  });

  (<CurrentHeaderSetter>this._headerSetter.deref()).setCurrentHeader(visibleItems)
}

实现 scrollViewDidScroll 方法后,我们将 listview 的 _delegate 属性设置为我们的自定义委托实例,从而用我们的自定义委托替换原始委托,如下所示:

typescript 复制代码
const del = new UITableViewDelegateImpl(
  new WeakRef(listView),
  new WeakRef(this)
);
(listView as any)._delegate = del;

在 iOS 上获取可见行数据

要获取可见行的数据,我们首先从 UITableView 对象通过 indexPathsForVisibleRows 属性获取包含 NSIndexPath 对象的数组,如下所示:

typescript 复制代码
const indexPathsForVisibleRows = this._owner.get().ios.indexPathsForVisibleRows;

然后我们使用 indexPathsForVisibleRows 变量的值和 items 数组(完整的 ListView 数据)创建一个只包含可见行数据的 JavaScript 数组。

我们按如下方式获取 ListView 的完整原始数据:

typescript 复制代码
const items = (this._owner.deref() as ListView).items as string[];

我们过滤可见行的数据并将其传递给 CurrentHeaderSetter 对象的 setCurrentHeader() 方法,如下所示:

typescript 复制代码
const visibleItems = Array.from(
  { length: indexPathsForVisibleRows.count },
  (_, i) => i
).map((i) => {
  const visItem = indexPathsForVisibleRows[i];
  return items[visItem.row] as string;
});

(<CurrentHeaderSetter>this._headerSetter.deref()).setCurrentHeader(
  visibleItems
);

监听 Android 上的滚动事件

要监听 Android 上的滚动事件,我们调用 ListView 对象的 setOnScrollListener 方法。setOnScrollListener 方法接受一个 android.widget.AbsListView.OnScrollListener 构造函数的实例,该构造函数在 NativeScript 中包装了 AbsListView.OnScrollListener 接口。我们用接口的实现对象创建一个 android.widget.AbsListView.OnScrollListener 构造函数的实例,如下所示:

typescript 复制代码
listViewAndroid.setOnScrollListener(
  new android.widget.AbsListView.OnScrollListener({
    onScrollStateChanged: (view, scrollState) => {
      // ...
    },
    onScroll: (view, firstVisibleItem, visibleItemCount, totalItemCount) => {
      // ...
    },
  })
);

注意 完整实现请参见 listview-scroll.android.ts

onScroll 方法实现中,我们调用 CurrentHeaderSetter 对象的 setCurrentHeader() 方法将数据传递到 JavaScript 世界。

获取 Android 上的可见行数据

要在 Android 上获取可见行数据,在 onScroll 方法中我们使用 AbsListView 类的原生 getChildCount()getChildAt(i) 方法创建可见行的 JavaScript 数组,如下所示:

typescript 复制代码
const visibleItems = Array.from(
  { length: view.getChildCount() },
  (_, i) => i
).map((i) => {
  const child = view.getChildAt(i) as org.nativescript.widgets.GridLayout; // 行布局容器
  const textView = child.getChildAt(0) as android.widget.TextView; // Label
  return textView.getText();
});

getChildCount() 方法返回可见行的数量,getChildAt(i) 方法返回指定索引处的行。在这种情况下,view.getChildAt(i) 返回行布局容器,这是一个 GridLayout 视图。然后从 GridLayout 视图中获取 (child.getChildAt(0)) Label 视图并提取 Label 视图的文本。接着我们将 Label 视图的文本作为 map 回调函数的值返回。map 函数返回可见行数据的数组。最后我们将可见行数据传递给 CurrentHeaderSetter 对象的 setCurrentHeader() 方法,如下所示:

typescript 复制代码
this.setCurrentHeader(visibleItems);

setCurrentHeader() 方法

setCurrentHeader() 方法遍历标题列表,对于向上滚动,检查可见项目列表是否包含标题。如果是,并且标题位于列表顶部,我们将标题 Label 视图的文本属性设置为该迭代的标题值,如下所示:

typescript 复制代码
if (visibleRows.includes(header) && visibleRows.indexOf(header) == 0) {
  // 向上滚动
  this._headerLabelView.text = header;
}

否则,对于向下滚动,如果迭代的标题值在可见项目列表中并且不在列表顶部,我们将标题 Label 视图的文本属性设置为前一个迭代的标题值,如下所示:

typescript 复制代码
else { // 向下滚动
  if (visibleRows.includes(header) && visibleRows.indexOf(header) !== 0) {
    this._headerLabelView.text = this._headers[index - 1];
  }
}

https://blog.nativescript.org/listview-sticky-headers/

相关推荐
双普拉斯17 小时前
打造工业级全栈文件管理器:深度解析上传、回收站与三重下载流控技术
spring·vue·js
码界筑梦坊18 小时前
356-基于Python的网易新闻数据分析系统
python·mysql·信息可视化·数据分析·django·vue·毕业设计
吴声子夜歌1 天前
Vue3——渲染函数
前端·vue.js·vue·es6
2501_913680001 天前
Vue3项目快速接入AI助手的终极方案 - 让你的应用智能升级
前端·vue.js·人工智能·ai·vue·开源软件
吕永强1 天前
基于SpringBoot+Vue校园报修系统的设计与实现(源码+论文+部署)
vue·毕业设计·springboot·毕业论文·报修系统·校园报修
阿部多瑞 ABU2 天前
《智能学号抽取系统》V5.9.5 发布:精简代码,修复移动端文件读取核心 Bug
vue·html·bug
sp424 天前
NativeScript 的 SwiftUI 入门指南
nativescript
吴声子夜歌4 天前
Vue3——表单元素绑定
前端·vue·es6
DazedMen5 天前
前端自定义接口返回,想咋玩就咋玩
前端·vue·接口拦截