mirror of
https://github.com/NativeScript/NativeScript.git
synced 2025-11-05 13:26:48 +08:00
feat(ios): ListView sticky headers options
This commit is contained in:
65
packages/core/ui/list-view/index.d.ts
vendored
65
packages/core/ui/list-view/index.d.ts
vendored
@@ -99,6 +99,46 @@ export class ListView extends View {
|
||||
*/
|
||||
iosEstimatedRowHeight: CoreTypes.LengthType;
|
||||
|
||||
/**
|
||||
* Gets or sets a value indicating whether the ListView should display sticky headers.
|
||||
* When enabled, headers will remain visible at the top while scrolling through sections.
|
||||
*
|
||||
* @nsProperty
|
||||
*/
|
||||
stickyHeader: boolean;
|
||||
|
||||
/**
|
||||
* Gets or sets the template for sticky headers.
|
||||
*
|
||||
* @nsProperty
|
||||
*/
|
||||
stickyHeaderTemplate: string | Template;
|
||||
|
||||
/**
|
||||
* Gets or sets the height of sticky headers.
|
||||
*
|
||||
* @nsProperty
|
||||
*/
|
||||
stickyHeaderHeight: CoreTypes.LengthType;
|
||||
|
||||
/**
|
||||
* Gets or sets a value indicating whether the ListView should show default top padding above section headers.
|
||||
* When set to false (default), removes iOS default spacing for a tighter layout.
|
||||
* When set to true, preserves iOS default ~4-5px spacing above section headers.
|
||||
*
|
||||
* @nsProperty
|
||||
*/
|
||||
stickyHeaderTopPadding: boolean;
|
||||
|
||||
/**
|
||||
* Gets or sets a value indicating whether the ListView should treat items as sectioned data.
|
||||
* When enabled, items array should contain objects with 'items' property for section content.
|
||||
* Each section will have its own sticky header.
|
||||
*
|
||||
* @nsProperty
|
||||
*/
|
||||
sectioned: boolean;
|
||||
|
||||
/**
|
||||
* Forces the ListView to reload all its items.
|
||||
*/
|
||||
@@ -230,3 +270,28 @@ export const iosEstimatedRowHeightProperty: Property<ListView, CoreTypes.LengthT
|
||||
* Backing property for separator color property.
|
||||
*/
|
||||
export const separatorColorProperty: CssProperty<Style, Color>;
|
||||
|
||||
/**
|
||||
* Represents the observable property backing the stickyHeader property of each ListView instance.
|
||||
*/
|
||||
export const stickyHeaderProperty: Property<ListView, boolean>;
|
||||
|
||||
/**
|
||||
* Represents the sticky header template property of each ListView instance.
|
||||
*/
|
||||
export const stickyHeaderTemplateProperty: Property<ListView, string | Template>;
|
||||
|
||||
/**
|
||||
* Represents the observable property backing the stickyHeaderHeight property of each ListView instance.
|
||||
*/
|
||||
export const stickyHeaderHeightProperty: Property<ListView, CoreTypes.LengthType>;
|
||||
|
||||
/**
|
||||
* Represents the observable property backing the stickyHeaderTopPadding property of each ListView instance.
|
||||
*/
|
||||
export const stickyHeaderTopPaddingProperty: Property<ListView, boolean>;
|
||||
|
||||
/**
|
||||
* Represents the observable property backing the sectioned property of each ListView instance.
|
||||
*/
|
||||
export const sectionedProperty: Property<ListView, boolean>;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ItemEventData } from '.';
|
||||
import { ListViewBase, separatorColorProperty, itemTemplatesProperty, iosEstimatedRowHeightProperty } from './list-view-common';
|
||||
import { ListViewBase, separatorColorProperty, itemTemplatesProperty, iosEstimatedRowHeightProperty, stickyHeaderProperty, stickyHeaderTemplateProperty, stickyHeaderHeightProperty, sectionedProperty } from './list-view-common';
|
||||
import { CoreTypes } from '../../core-types';
|
||||
import { View, KeyedTemplate } from '../core/view';
|
||||
import { View, KeyedTemplate, Template } from '../core/view';
|
||||
import { Length } from '../styling/length-shared';
|
||||
import { Observable, EventData } from '../../data/observable';
|
||||
import { Color } from '../../color';
|
||||
@@ -10,6 +10,9 @@ import { StackLayout } from '../layouts/stack-layout';
|
||||
import { ProxyViewContainer } from '../proxy-view-container';
|
||||
import { profile } from '../../profiling';
|
||||
import { Trace } from '../../trace';
|
||||
import { Builder } from '../builder';
|
||||
import { Label } from '../label';
|
||||
import { isFunction } from '../../utils/types';
|
||||
|
||||
export * from './list-view-common';
|
||||
|
||||
@@ -61,6 +64,41 @@ class ListViewCell extends UITableViewCell {
|
||||
public owner: WeakRef<View>;
|
||||
}
|
||||
|
||||
@NativeClass
|
||||
class ListViewHeaderCell extends UITableViewHeaderFooterView {
|
||||
public static initWithEmptyBackground(): ListViewHeaderCell {
|
||||
const cell = <ListViewHeaderCell>ListViewHeaderCell.new();
|
||||
// Clear background by default - this will make headers transparent
|
||||
cell.backgroundColor = UIColor.clearColor;
|
||||
|
||||
return cell;
|
||||
}
|
||||
|
||||
initWithReuseIdentifier(reuseIdentifier: string): this {
|
||||
const cell = <this>super.initWithReuseIdentifier(reuseIdentifier);
|
||||
// Clear background by default - this will make headers transparent
|
||||
cell.backgroundColor = UIColor.clearColor;
|
||||
|
||||
return cell;
|
||||
}
|
||||
|
||||
public willMoveToSuperview(newSuperview: UIView): void {
|
||||
const parent = <ListView>(this.view ? this.view.parent : null);
|
||||
|
||||
// When inside ListView and there is no newSuperview this header is
|
||||
// removed from native visual tree so we remove it from our tree too.
|
||||
if (parent && !newSuperview) {
|
||||
parent._removeHeaderContainer(this);
|
||||
}
|
||||
}
|
||||
|
||||
public get view(): View {
|
||||
return this.owner ? this.owner.deref() : null;
|
||||
}
|
||||
|
||||
public owner: WeakRef<View>;
|
||||
}
|
||||
|
||||
function notifyForItemAtIndex(listView: ListViewBase, cell: any, view: View, eventName: string, indexPath: NSIndexPath) {
|
||||
const args = <ItemEventData>{
|
||||
eventName: eventName,
|
||||
@@ -88,10 +126,33 @@ class DataSource extends NSObject implements UITableViewDataSource {
|
||||
return dataSource;
|
||||
}
|
||||
|
||||
public numberOfSectionsInTableView(tableView: UITableView): number {
|
||||
const owner = this._owner?.deref();
|
||||
|
||||
if (!owner) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const sections = owner._getSectionCount();
|
||||
if (Trace.isEnabled()) {
|
||||
Trace.write(`ListView: numberOfSections = ${sections} (sectioned: ${owner.sectioned})`, Trace.categories.Debug);
|
||||
}
|
||||
return sections;
|
||||
}
|
||||
|
||||
public tableViewNumberOfRowsInSection(tableView: UITableView, section: number) {
|
||||
const owner = this._owner?.deref();
|
||||
|
||||
return owner && owner.items ? owner.items.length : 0;
|
||||
if (!owner) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const sectionItems = owner._getItemsInSection(section);
|
||||
const rowCount = sectionItems ? sectionItems.length : 0;
|
||||
if (Trace.isEnabled()) {
|
||||
Trace.write(`ListView: numberOfRows in section ${section} = ${rowCount}`, Trace.categories.Debug);
|
||||
}
|
||||
return rowCount;
|
||||
}
|
||||
|
||||
public tableViewCellForRowAtIndexPath(tableView: UITableView, indexPath: NSIndexPath): UITableViewCell {
|
||||
@@ -184,6 +245,55 @@ class UITableViewDelegateImpl extends NSObject implements UITableViewDelegate {
|
||||
|
||||
return layout.toDeviceIndependentPixels(height);
|
||||
}
|
||||
|
||||
public tableViewViewForHeaderInSection(tableView: UITableView, section: number): UIView {
|
||||
const owner = this._owner?.deref();
|
||||
|
||||
if (!owner || !owner.stickyHeader || !owner.stickyHeaderTemplate) {
|
||||
if (Trace.isEnabled()) {
|
||||
Trace.write(`ListView: No sticky header (stickyHeader: ${owner?.stickyHeader}, hasTemplate: ${!!owner?.stickyHeaderTemplate})`, Trace.categories.Debug);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (Trace.isEnabled()) {
|
||||
Trace.write(`ListView: Creating sticky header`, Trace.categories.Debug);
|
||||
}
|
||||
|
||||
const headerReuseIdentifier = 'stickyHeader';
|
||||
let headerCell = <ListViewHeaderCell>tableView.dequeueReusableHeaderFooterViewWithIdentifier(headerReuseIdentifier);
|
||||
|
||||
if (!headerCell) {
|
||||
// Use proper iOS initialization for registered header cells
|
||||
headerCell = <ListViewHeaderCell>ListViewHeaderCell.alloc().initWithReuseIdentifier(headerReuseIdentifier);
|
||||
headerCell.backgroundColor = UIColor.clearColor;
|
||||
}
|
||||
|
||||
owner._prepareHeader(headerCell, section);
|
||||
|
||||
return headerCell;
|
||||
}
|
||||
|
||||
public tableViewHeightForHeaderInSection(tableView: UITableView, section: number): number {
|
||||
const owner = this._owner?.deref();
|
||||
|
||||
if (!owner || !owner.stickyHeader) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let height: number;
|
||||
if (owner.stickyHeaderHeight === 'auto') {
|
||||
height = 44;
|
||||
} else {
|
||||
height = layout.toDeviceIndependentPixels(Length.toDevicePixels(owner.stickyHeaderHeight, 44));
|
||||
}
|
||||
|
||||
if (Trace.isEnabled()) {
|
||||
Trace.write(`ListView: Sticky header height: ${height}`, Trace.categories.Debug);
|
||||
}
|
||||
|
||||
return height;
|
||||
}
|
||||
}
|
||||
|
||||
@NativeClass
|
||||
@@ -233,6 +343,54 @@ class UITableViewRowHeightDelegateImpl extends NSObject implements UITableViewDe
|
||||
|
||||
return layout.toDeviceIndependentPixels(owner._effectiveRowHeight);
|
||||
}
|
||||
|
||||
public tableViewViewForHeaderInSection(tableView: UITableView, section: number): UIView {
|
||||
const owner = this._owner?.deref();
|
||||
|
||||
if (!owner || !owner.stickyHeader || !owner.stickyHeaderTemplate) {
|
||||
if (Trace.isEnabled()) {
|
||||
Trace.write(`ListView: No sticky header (stickyHeader: ${owner?.stickyHeader}, hasTemplate: ${!!owner?.stickyHeaderTemplate})`, Trace.categories.Debug);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (Trace.isEnabled()) {
|
||||
Trace.write(`ListView: Creating sticky header`, Trace.categories.Debug);
|
||||
}
|
||||
|
||||
const headerReuseIdentifier = 'stickyHeader';
|
||||
let headerCell = <ListViewHeaderCell>tableView.dequeueReusableHeaderFooterViewWithIdentifier(headerReuseIdentifier);
|
||||
|
||||
if (!headerCell) {
|
||||
headerCell = <ListViewHeaderCell>ListViewHeaderCell.alloc().initWithReuseIdentifier(headerReuseIdentifier);
|
||||
headerCell.backgroundColor = UIColor.clearColor;
|
||||
}
|
||||
|
||||
owner._prepareHeader(headerCell, section);
|
||||
|
||||
return headerCell;
|
||||
}
|
||||
|
||||
public tableViewHeightForHeaderInSection(tableView: UITableView, section: number): number {
|
||||
const owner = this._owner?.deref();
|
||||
|
||||
if (!owner || !owner.stickyHeader) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let height: number;
|
||||
if (owner.stickyHeaderHeight === 'auto') {
|
||||
height = 44;
|
||||
} else {
|
||||
height = layout.toDeviceIndependentPixels(Length.toDevicePixels(owner.stickyHeaderHeight, 44));
|
||||
}
|
||||
|
||||
if (Trace.isEnabled()) {
|
||||
Trace.write(`ListView: Sticky header height: ${height}`, Trace.categories.Debug);
|
||||
}
|
||||
|
||||
return height;
|
||||
}
|
||||
}
|
||||
|
||||
export class ListView extends ListViewBase {
|
||||
@@ -244,11 +402,15 @@ export class ListView extends ListViewBase {
|
||||
private _preparingCell: boolean;
|
||||
private _isDataDirty: boolean;
|
||||
private _map: Map<ListViewCell, ItemView>;
|
||||
private _headerMap: Map<ListViewHeaderCell, View>;
|
||||
private _preparingHeader: boolean;
|
||||
private _headerTemplateCache: View;
|
||||
widthMeasureSpec = 0;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this._map = new Map<ListViewCell, ItemView>();
|
||||
this._headerMap = new Map<ListViewHeaderCell, View>();
|
||||
this._heights = new Array<number>();
|
||||
}
|
||||
|
||||
@@ -260,10 +422,20 @@ export class ListView extends ListViewBase {
|
||||
super.initNativeView();
|
||||
const nativeView = this.nativeViewProtected;
|
||||
nativeView.registerClassForCellReuseIdentifier(ListViewCell.class(), this._defaultTemplate.key);
|
||||
nativeView.registerClassForHeaderFooterViewReuseIdentifier(ListViewHeaderCell.class(), 'stickyHeader');
|
||||
nativeView.estimatedRowHeight = DEFAULT_HEIGHT;
|
||||
nativeView.rowHeight = UITableViewAutomaticDimension;
|
||||
nativeView.dataSource = this._dataSource = DataSource.initWithOwner(new WeakRef(this));
|
||||
this._delegate = UITableViewDelegateImpl.initWithOwner(new WeakRef(this));
|
||||
|
||||
// Control section header top padding (iOS 15+)
|
||||
if (nativeView.respondsToSelector('setSectionHeaderTopPadding:')) {
|
||||
if (!this.stickyHeaderTopPadding) {
|
||||
nativeView.sectionHeaderTopPadding = 0;
|
||||
}
|
||||
// When stickyHeaderTopPadding is true, don't set the property to use iOS default
|
||||
}
|
||||
|
||||
this._setNativeClipToBounds();
|
||||
}
|
||||
|
||||
@@ -296,13 +468,16 @@ export class ListView extends ListViewBase {
|
||||
}
|
||||
|
||||
get _childrenCount(): number {
|
||||
return this._map.size;
|
||||
return this._map.size + this._headerMap.size;
|
||||
}
|
||||
|
||||
public eachChildView(callback: (child: View) => boolean): void {
|
||||
this._map.forEach((view, key) => {
|
||||
callback(view);
|
||||
});
|
||||
this._headerMap.forEach((view, key) => {
|
||||
callback(view);
|
||||
});
|
||||
}
|
||||
|
||||
public scrollToIndex(index: number) {
|
||||
@@ -340,6 +515,11 @@ export class ListView extends ListViewBase {
|
||||
view.bindingContext = null;
|
||||
}
|
||||
});
|
||||
this._headerMap.forEach((view, nativeView, map) => {
|
||||
if (!(view.bindingContext instanceof Observable)) {
|
||||
view.bindingContext = null;
|
||||
}
|
||||
});
|
||||
|
||||
if (this.isLoaded) {
|
||||
this.nativeViewProtected.reloadData();
|
||||
@@ -385,8 +565,8 @@ export class ListView extends ListViewBase {
|
||||
}
|
||||
|
||||
public requestLayout(): void {
|
||||
// When preparing cell don't call super - no need to invalidate our measure when cell desiredSize is changed.
|
||||
if (!this._preparingCell) {
|
||||
// When preparing cell or header don't call super - no need to invalidate our measure when cell/header desiredSize is changed.
|
||||
if (!this._preparingCell && !this._preparingHeader) {
|
||||
super.requestLayout();
|
||||
}
|
||||
}
|
||||
@@ -409,6 +589,9 @@ export class ListView extends ListViewBase {
|
||||
this._map.forEach((childView, listViewCell) => {
|
||||
View.measureChild(this, childView, childView._currentWidthMeasureSpec, childView._currentHeightMeasureSpec);
|
||||
});
|
||||
this._headerMap.forEach((childView, listViewHeaderCell) => {
|
||||
View.measureChild(this, childView, childView._currentWidthMeasureSpec, childView._currentHeightMeasureSpec);
|
||||
});
|
||||
}
|
||||
|
||||
public onLayout(left: number, top: number, right: number, bottom: number): void {
|
||||
@@ -423,6 +606,12 @@ export class ListView extends ListViewBase {
|
||||
View.layoutChild(this, childView, 0, 0, width, cellHeight);
|
||||
}
|
||||
});
|
||||
this._headerMap.forEach((childView, listViewHeaderCell) => {
|
||||
const headerHeight = this.stickyHeaderHeight === 'auto' ? 44 : Length.toDevicePixels(this.stickyHeaderHeight, 44);
|
||||
const width = layout.getMeasureSpecSize(this.widthMeasureSpec);
|
||||
childView.iosOverflowSafeAreaEnabled = false;
|
||||
View.layoutChild(this, childView, 0, 0, width, headerHeight);
|
||||
});
|
||||
}
|
||||
|
||||
private _layoutCell(cellView: View, indexPath: NSIndexPath): number {
|
||||
@@ -445,11 +634,21 @@ export class ListView extends ListViewBase {
|
||||
this._preparingCell = true;
|
||||
let view: ItemView = cell.view;
|
||||
if (!view) {
|
||||
view = this._getItemTemplate(indexPath.row).createView();
|
||||
if (this.sectioned) {
|
||||
// For sectioned data, we need to calculate the absolute index for template selection
|
||||
let absoluteIndex = 0;
|
||||
for (let i = 0; i < indexPath.section; i++) {
|
||||
absoluteIndex += this._getItemsInSection(i).length;
|
||||
}
|
||||
absoluteIndex += indexPath.row;
|
||||
view = this._getItemTemplate(absoluteIndex).createView();
|
||||
} else {
|
||||
view = this._getItemTemplate(indexPath.row).createView();
|
||||
}
|
||||
}
|
||||
|
||||
const args = notifyForItemAtIndex(this, cell, view, ITEMLOADING, indexPath);
|
||||
view = args.view || this._getDefaultItemContent(indexPath.row);
|
||||
view = args.view || this._getDefaultItemContent(this.sectioned ? indexPath.row : indexPath.row);
|
||||
|
||||
// Proxy containers should not get treated as layouts.
|
||||
// Wrap them in a real layout as well.
|
||||
@@ -469,8 +668,14 @@ export class ListView extends ListViewBase {
|
||||
cell.owner = new WeakRef(view);
|
||||
}
|
||||
|
||||
this._prepareItem(view, indexPath.row);
|
||||
view._listViewItemIndex = indexPath.row;
|
||||
if (this.sectioned) {
|
||||
this._prepareItemInSection(view, indexPath.section, indexPath.row);
|
||||
view._listViewItemIndex = indexPath.row; // Keep row index for compatibility
|
||||
(view as any)._listViewSectionIndex = indexPath.section;
|
||||
} else {
|
||||
this._prepareItem(view, indexPath.row);
|
||||
view._listViewItemIndex = indexPath.row;
|
||||
}
|
||||
this._map.set(cell, view);
|
||||
|
||||
// We expect that views returned from itemLoading are new (e.g. not reused).
|
||||
@@ -503,6 +708,163 @@ export class ListView extends ListViewBase {
|
||||
this._map.delete(cell);
|
||||
}
|
||||
|
||||
public _prepareHeader(headerCell: ListViewHeaderCell, section: number): number {
|
||||
let headerHeight: number;
|
||||
try {
|
||||
this._preparingHeader = true;
|
||||
let view: View = headerCell.view;
|
||||
if (!view) {
|
||||
view = this._getHeaderTemplate();
|
||||
if (!view) {
|
||||
if (Trace.isEnabled()) {
|
||||
Trace.write(`ListView: Failed to create header view for section ${section}`, Trace.categories.Debug);
|
||||
}
|
||||
// Create a fallback view
|
||||
const lbl = new Label();
|
||||
lbl.text = `Section ${section}`;
|
||||
view = lbl;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle header cell reuse
|
||||
if (!headerCell.view) {
|
||||
headerCell.owner = new WeakRef(view);
|
||||
} else if (headerCell.view !== view) {
|
||||
// Remove old view and set new one
|
||||
(<UIView>headerCell.view.nativeViewProtected)?.removeFromSuperview();
|
||||
this._removeHeaderContainer(headerCell);
|
||||
headerCell.owner = new WeakRef(view);
|
||||
}
|
||||
|
||||
// Clear existing binding context and set new one
|
||||
if (view.bindingContext) {
|
||||
view.bindingContext = null;
|
||||
}
|
||||
|
||||
if (this.sectioned) {
|
||||
const sectionData = this._getSectionData(section);
|
||||
if (sectionData) {
|
||||
view.bindingContext = sectionData;
|
||||
} else {
|
||||
// Fallback if section data is missing
|
||||
view.bindingContext = { title: `Section ${section}`, section: section };
|
||||
}
|
||||
} else {
|
||||
view.bindingContext = this.bindingContext;
|
||||
}
|
||||
|
||||
// Force immediate binding context evaluation
|
||||
if (view && typeof (view as any)._onBindingContextChanged === 'function') {
|
||||
(view as any)._onBindingContextChanged(null, view.bindingContext);
|
||||
|
||||
// Also trigger for child views
|
||||
// @ts-ignore
|
||||
if (view._childrenCount) {
|
||||
view.eachChildView((child) => {
|
||||
if (typeof (child as any)._onBindingContextChanged === 'function') {
|
||||
(child as any)._onBindingContextChanged(null, view.bindingContext);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
this._headerMap.set(headerCell, view);
|
||||
|
||||
// Add new header view to the cell
|
||||
if (view && !view.parent) {
|
||||
this._addView(view);
|
||||
headerCell.contentView.addSubview(view.nativeViewProtected);
|
||||
}
|
||||
|
||||
// Request layout and measure/layout the header
|
||||
if (view && view.bindingContext) {
|
||||
view.requestLayout();
|
||||
}
|
||||
|
||||
headerHeight = this._layoutHeader(view);
|
||||
} finally {
|
||||
this._preparingHeader = false;
|
||||
}
|
||||
|
||||
return headerHeight;
|
||||
}
|
||||
|
||||
private _layoutHeader(headerView: View): number {
|
||||
if (headerView) {
|
||||
const headerHeight = this.stickyHeaderHeight === 'auto' ? 44 : Length.toDevicePixels(this.stickyHeaderHeight, 44);
|
||||
const heightMeasureSpec: number = layout.makeMeasureSpec(headerHeight, layout.EXACTLY);
|
||||
|
||||
const measuredSize = View.measureChild(this, headerView, this.widthMeasureSpec, heightMeasureSpec);
|
||||
// Layout the header with the measured size
|
||||
View.layoutChild(this, headerView, 0, 0, measuredSize.measuredWidth, measuredSize.measuredHeight);
|
||||
|
||||
return measuredSize.measuredHeight;
|
||||
}
|
||||
|
||||
return 44;
|
||||
}
|
||||
|
||||
private _getHeaderTemplate(): View {
|
||||
if (this.stickyHeaderTemplate) {
|
||||
if (__UI_USE_EXTERNAL_RENDERER__) {
|
||||
if (isFunction(this.stickyHeaderTemplate)) {
|
||||
return (<Template>this.stickyHeaderTemplate)();
|
||||
}
|
||||
} else {
|
||||
if (typeof this.stickyHeaderTemplate === 'string') {
|
||||
try {
|
||||
const parsed = Builder.parse(this.stickyHeaderTemplate, this);
|
||||
if (!parsed) {
|
||||
// Create a simple fallback
|
||||
const fallbackLabel = new Label();
|
||||
fallbackLabel.text = 'Parse Failed';
|
||||
return fallbackLabel;
|
||||
}
|
||||
return parsed;
|
||||
} catch (error) {
|
||||
if (Trace.isEnabled()) {
|
||||
Trace.write(`ListView: Template parsing error: ${error}`, Trace.categories.Debug);
|
||||
}
|
||||
// Create a simple fallback
|
||||
const errorLabel = new Label();
|
||||
errorLabel.text = 'Template Error';
|
||||
return errorLabel;
|
||||
}
|
||||
} else {
|
||||
const view = (<Template>this.stickyHeaderTemplate)();
|
||||
if (Trace.isEnabled()) {
|
||||
Trace.write(`ListView: Created header view from template function: ${!!view} (type: ${view?.constructor?.name})`, Trace.categories.Debug);
|
||||
}
|
||||
return view;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Trace.isEnabled()) {
|
||||
Trace.write(`ListView: No sticky header template, creating default`, Trace.categories.Debug);
|
||||
}
|
||||
|
||||
// Return a default header if no template is provided
|
||||
const lbl = new Label();
|
||||
lbl.text = 'Default Header';
|
||||
return lbl;
|
||||
}
|
||||
|
||||
public _removeHeaderContainer(headerCell: ListViewHeaderCell): void {
|
||||
const view: View = headerCell.view;
|
||||
// This is to clear the StackLayout that is used to wrap ProxyViewContainer instances.
|
||||
if (!(view.parent instanceof ListView)) {
|
||||
this._removeView(view.parent);
|
||||
}
|
||||
|
||||
// No need to request layout when we are removing headers.
|
||||
const preparing = this._preparingHeader;
|
||||
this._preparingHeader = true;
|
||||
view.parent._removeView(view);
|
||||
this._preparingHeader = preparing;
|
||||
this._headerMap.delete(headerCell);
|
||||
}
|
||||
|
||||
[separatorColorProperty.getDefault](): UIColor {
|
||||
return this.nativeViewProtected.separatorColor;
|
||||
}
|
||||
@@ -533,4 +895,58 @@ export class ListView extends ListViewBase {
|
||||
const estimatedHeight = layout.toDeviceIndependentPixels(Length.toDevicePixels(value, 0));
|
||||
nativeView.estimatedRowHeight = estimatedHeight < 0 ? DEFAULT_HEIGHT : estimatedHeight;
|
||||
}
|
||||
|
||||
[stickyHeaderProperty.getDefault](): boolean {
|
||||
return false;
|
||||
}
|
||||
[stickyHeaderProperty.setNative](value: boolean) {
|
||||
if (Trace.isEnabled()) {
|
||||
Trace.write(`ListView: stickyHeader set to ${value}`, Trace.categories.Debug);
|
||||
}
|
||||
// Immediately refresh to apply changes
|
||||
if (this.isLoaded) {
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
[stickyHeaderTemplateProperty.getDefault](): string | Template {
|
||||
return null;
|
||||
}
|
||||
[stickyHeaderTemplateProperty.setNative](value: string | Template) {
|
||||
if (Trace.isEnabled()) {
|
||||
Trace.write(`ListView: stickyHeaderTemplate set: ${typeof value} ${value ? '(has value)' : '(null)'}`, Trace.categories.Debug);
|
||||
}
|
||||
// Clear any cached template
|
||||
this._headerTemplateCache = null;
|
||||
// Immediately refresh to apply changes
|
||||
if (this.isLoaded) {
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
[stickyHeaderHeightProperty.getDefault](): CoreTypes.LengthType {
|
||||
return 'auto';
|
||||
}
|
||||
[stickyHeaderHeightProperty.setNative](value: CoreTypes.LengthType) {
|
||||
if (Trace.isEnabled()) {
|
||||
Trace.write(`ListView: stickyHeaderHeight set to ${value}`, Trace.categories.Debug);
|
||||
}
|
||||
// Immediately refresh to apply changes
|
||||
if (this.isLoaded) {
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
[sectionedProperty.getDefault](): boolean {
|
||||
return false;
|
||||
}
|
||||
[sectionedProperty.setNative](value: boolean) {
|
||||
if (Trace.isEnabled()) {
|
||||
Trace.write(`ListView: sectioned set to ${value}`, Trace.categories.Debug);
|
||||
}
|
||||
// Immediately refresh to apply changes
|
||||
if (this.isLoaded) {
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ import { ObservableArray, ChangedData } from '../../data/observable-array';
|
||||
import { addWeakEventListener, removeWeakEventListener } from '../core/weak-event-listener';
|
||||
import { CoreTypes } from '../../core-types';
|
||||
import { isFunction } from '../../utils/types';
|
||||
import { Trace } from '../../trace';
|
||||
import { booleanConverter } from '../core/view-base';
|
||||
|
||||
const autoEffectiveRowHeight = -1;
|
||||
|
||||
@@ -49,6 +51,11 @@ export abstract class ListViewBase extends ContainerView implements ListViewDefi
|
||||
public items: any[] | ItemsSource;
|
||||
public itemTemplate: string | Template;
|
||||
public itemTemplates: string | Array<KeyedTemplate>;
|
||||
public stickyHeader: boolean;
|
||||
public stickyHeaderTemplate: string | Template;
|
||||
public stickyHeaderHeight: CoreTypes.LengthType;
|
||||
public stickyHeaderTopPadding: boolean;
|
||||
public sectioned: boolean;
|
||||
|
||||
get separatorColor(): Color {
|
||||
return this.style.separatorColor;
|
||||
@@ -125,12 +132,58 @@ export abstract class ListViewBase extends ContainerView implements ListViewDefi
|
||||
}
|
||||
}
|
||||
|
||||
public _prepareItemInSection(item: View, section: number, index: number) {
|
||||
if (item) {
|
||||
item.bindingContext = this._getDataItemInSection(section, index);
|
||||
}
|
||||
}
|
||||
|
||||
private _getDataItem(index: number): any {
|
||||
const thisItems = <ItemsSource>this.items;
|
||||
|
||||
return thisItems.getItem ? thisItems.getItem(index) : thisItems[index];
|
||||
}
|
||||
|
||||
public _getSectionCount(): number {
|
||||
if (!this.sectioned || !this.items) {
|
||||
return 1;
|
||||
}
|
||||
return this.items.length;
|
||||
}
|
||||
|
||||
public _getItemsInSection(section: number): any[] | ItemsSource {
|
||||
if (!this.sectioned || !this.items) {
|
||||
return this.items;
|
||||
}
|
||||
const sectionData = this.items[section];
|
||||
return sectionData?.items || [];
|
||||
}
|
||||
|
||||
public _getSectionData(section: number): any {
|
||||
if (!this.sectioned || !this.items || !Array.isArray(this.items)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (section < 0 || section >= this.items.length) {
|
||||
if (Trace.isEnabled()) {
|
||||
Trace.write(`ListView: Section ${section} out of bounds (total sections: ${this.items.length})`, Trace.categories.Debug);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const sectionData = this.items[section];
|
||||
if (Trace.isEnabled() && !sectionData) {
|
||||
Trace.write(`ListView: Section ${section} data is null/undefined`, Trace.categories.Debug);
|
||||
}
|
||||
|
||||
return sectionData;
|
||||
}
|
||||
|
||||
public _getDataItemInSection(section: number, index: number): any {
|
||||
const sectionItems = this._getItemsInSection(section);
|
||||
return (sectionItems as ItemsSource).getItem ? (sectionItems as ItemsSource).getItem(index) : sectionItems[index];
|
||||
}
|
||||
|
||||
public _getDefaultItemContent(index: number): View {
|
||||
const lbl = new Label();
|
||||
lbl.bind({
|
||||
@@ -249,3 +302,40 @@ export const separatorColorProperty = new CssProperty<Style, Color>({
|
||||
valueConverter: (v) => new Color(v),
|
||||
});
|
||||
separatorColorProperty.register(Style);
|
||||
|
||||
export const stickyHeaderProperty = new Property<ListViewBase, boolean>({
|
||||
name: 'stickyHeader',
|
||||
defaultValue: false,
|
||||
valueConverter: booleanConverter,
|
||||
});
|
||||
stickyHeaderProperty.register(ListViewBase);
|
||||
|
||||
export const stickyHeaderTemplateProperty = new Property<ListViewBase, string | Template>({
|
||||
name: 'stickyHeaderTemplate',
|
||||
valueChanged: (target) => {
|
||||
target.refresh();
|
||||
},
|
||||
});
|
||||
stickyHeaderTemplateProperty.register(ListViewBase);
|
||||
|
||||
export const stickyHeaderHeightProperty = new Property<ListViewBase, CoreTypes.LengthType>({
|
||||
name: 'stickyHeaderHeight',
|
||||
defaultValue: 'auto',
|
||||
equalityComparer: Length.equals,
|
||||
valueConverter: Length.parse,
|
||||
});
|
||||
stickyHeaderHeightProperty.register(ListViewBase);
|
||||
|
||||
export const stickyHeaderTopPaddingProperty = new Property<ListViewBase, boolean>({
|
||||
name: 'stickyHeaderTopPadding',
|
||||
defaultValue: false,
|
||||
valueConverter: booleanConverter,
|
||||
});
|
||||
stickyHeaderTopPaddingProperty.register(ListViewBase);
|
||||
|
||||
export const sectionedProperty = new Property<ListViewBase, boolean>({
|
||||
name: 'sectioned',
|
||||
defaultValue: false,
|
||||
valueConverter: (v) => !!v,
|
||||
});
|
||||
sectionedProperty.register(ListViewBase);
|
||||
|
||||
Reference in New Issue
Block a user