有时,在 NativeScript 中使用 ListView 时,您可能希望添加粘性章节标题。在本文中,我将向您展示如何使用 Vue 和 NativeScript 为您的 ListView 添加粘性章节标题。
为了实现粘性章节标题效果,除了 ListView 视图外,我们还需要以下视图
以下是我们将采取的步骤,以将粘性标题添加到 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
中,确保以下几点
GridLayout 有一行一列,用于将标题 Label 堆叠在 ListView 之上。
ListView
是 GridLayout
的第一个子元素,以便首先渲染它,而 Label
视图是第二个,以便将其渲染在 ListView
之上。
我们将 Label 视图的 verticalAlignment
属性(如果您使用 TailwindCSS,则通过 align-top
CSS 类)设置为 top
,以使 Label 视图粘贴到 GridLayout 的顶部。
<GridLayout>
<ListView for="item in items" @itemTap="onItemTap">
<v-template>
<label :text="item" />
</v-template>
</ListView>
<label text="Header 1" class="align-top" />👈
</GridLayout>
text
属性。::: tip 提示 使 Label 视图的大小与包含标题的 ListView 行的大小相同。这样,当用户在 ListView 中滚动时,当标题是可见项目列表中的第一个时,Label 将正确覆盖标题行。::
要监听 ListView 的原生滚动事件,我们可以编写一个 JavaScript 类来包装原生代码,并充当原生代码和 Vue 组件之间的桥梁。对于本文,我将该包装类命名为 CurrentHeaderSetter
。
为了在 iOS 和 Android 两个平台上共享代码,CurrentHeaderSetter
类扩展了 CurrentHeaderSetterCommon
类。CurrentHeaderSetterCommon
类包含以下成员
_headerLabelView
属性,它保存对标题 Label
视图的引用_headers
属性,它保存列表视图标题的列表setCurrentHeader()
方法负责接收来自原生平台的原生可见行数据,并使用该数据检查标题列表与可见行,以确定 Label 视图的当前标题值。要在 iOS 上监听滚动事件,我们需要实现 UITableViewDelegate 协议的 scrollViewDidScroll
方法。要使用类定义语法执行此操作,我们创建一个用 @NativeClass() 装饰器装饰的类,扩展 NSObject 类,实现 UITableViewDelegate
协议,并具有一个静态的 ObjCProtocols
属性,该属性绑定到包含 UITableViewDelegate
协议的数组,如下所示
@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 核心工程师实现。
因此,我们无需从头开始在自定义委托中编写所有这些方法的实现代码,而只需在我们的自定义委托类中调用原始委托方法的对应方法,如下所示
{
// ...
tableViewDidSelectRowAtIndexPath(tableView, indexPath) {
this._originalDelegate.tableViewDidSelectRowAtIndexPath(
tableView,
indexPath
);
}
// ...
}
我们通过 ListView
对象的 _delegate
属性获取对该原始委托的引用,如下所示
this._originalDelegate = (<any>owner.get())._delegate;
// see line 14 in on-listview-scroll.ios.ts
在将必要的方法添加到自定义委托后,我们实现我们感兴趣的 scrollViewDidScroll
方法,如下所示
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
方法后,我们将列表视图的 _delegate
属性设置为自定义委托的实例,从而用我们的自定义委托替换原始委托,如下所示
const del = new UITableViewDelegateImp(
new WeakRef(listView),
new WeakRef(this)
);
(listView as any)._delegate = del;
要获取可见行的数据,我们首先通过 indexPathsForVisibleRows
属性从 UITableView
对象获取包含 NSIndexPath 对象的数组,如下所示
const indexPathsForVisibleRows = this._owner.get().ios.indexPathsForVisibleRows;
然后,我们使用 indexPathsForVisibleRows
变量的值和 items
数组(完整列表视图的数据)来创建一个仅包含可见行数据的 JavaScript 数组。
我们如下获取 ListView 的完整原始数据
const items = (this._owner.deref() as ListView).items as string[];
我们过滤可见行的数据并将其传递给 CurrentHeaderSetter
对象的 setCurrentHeader()
方法,如下所示
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 上监听滚动事件,我们调用 ListView 对象的 setOnScrollListener
方法。setOnScrollListener
方法接受 AbsListView.OnScrollListener 构造函数的一个实例,该构造函数在 NativeScript 中包装了 AbsListView.OnScrollListener 接口。我们使用接口的实现对象创建一个 android.widget.AbsListView.OnScrollListener
构造函数的实例,如下所示
listViewAndroid.setOnScrollListener(
new android.widget.AbsListView.OnScrollListener({
onScrollStateChanged: (view, scrollState) => {
// ...
},
onScroll: (view, firstVisibleItem, visibleItemCount, totalItemCount) => {
// ...
},
})
);
::: tip 注意 有关完整实现,请参阅 listview-scroll.android.ts。::
在 onScroll
方法实现中,我们调用 CurrentHeaderSetter
对象的 setCurrentHeader()
方法将数据传递到 JavaScript 世界。
要在 Android 上获取可见行数据,在 onScroll
方法中,我们使用 AbsListView 类的原生 getChildCount()
和 getChildAt(i)
方法创建一个可见行的 JavaScript 数组,如下所示
const visibleItems = Array.from(
{ length: view.getChildCount() },
(_, i) => i
).map((i) => {
const child = view.getChildAt(i) as org.nativescript.widgets.GridLayout; // row layout container
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()
方法,如下所示
this.setCurrentHeader(visibleItems);
setCurrentHeader()
方法遍历标题列表,对于向上滚动,检查可见项目列表是否包含标题。如果包含并且标题位于列表顶部,我们如下将标题 Label
视图的 text
属性设置为该迭代的标题值
if (visibleRows.includes(header) && visibleRows.indexOf(header) == 0) {
// Upward scroll
this._headerLabelView.text = header;
}
否则,对于向下滚动,如果迭代的标题值位于可见项目列表中并且它不在列表顶部,我们如下将标题 Label
视图的 text
属性设置为其前面迭代的标题值
else { // Downward scroll
if (visibleRows.includes(header) && visibleRows.indexOf(header) !== 0) {
this._headerLabelView.text = this._headers[index - 1];
}
}
我们已经使用 Vue 和 NativeScript 为 ListView 添加了粘性章节标题。您可以在 此 StackBlitz 中找到完整的代码。
此 StackBlitz 也经过了一些修改以匹配 Vue 颜色,例如:https://stackblitz.com/edit/nativescript-vue-nativescript-vue-ck4ywz?file=src%2Fcomponents%2FStickyHeadersListview.vue