mirror of
https://github.com/NativeScript/NativeScript.git
synced 2025-11-05 13:26:48 +08:00
feat(android): ListView sticky header options 1
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { ItemEventData, ItemsSource } from '.';
|
import { ItemEventData, ItemsSource } from '.';
|
||||||
import { ListViewBase, separatorColorProperty, itemTemplatesProperty } from './list-view-common';
|
import { ListViewBase, separatorColorProperty, itemTemplatesProperty, stickyHeaderProperty, stickyHeaderTemplateProperty, stickyHeaderHeightProperty, sectionedProperty } from './list-view-common';
|
||||||
import { View, KeyedTemplate } from '../core/view';
|
import { View, KeyedTemplate } from '../core/view';
|
||||||
import { unsetValue } from '../core/properties/property-shared';
|
import { unsetValue } from '../core/properties/property-shared';
|
||||||
import { CoreTypes } from '../../core-types';
|
import { CoreTypes } from '../../core-types';
|
||||||
@@ -9,6 +9,9 @@ import { StackLayout } from '../layouts/stack-layout';
|
|||||||
import { ProxyViewContainer } from '../proxy-view-container';
|
import { ProxyViewContainer } from '../proxy-view-container';
|
||||||
import { LayoutBase } from '../layouts/layout-base';
|
import { LayoutBase } from '../layouts/layout-base';
|
||||||
import { profile } from '../../profiling';
|
import { profile } from '../../profiling';
|
||||||
|
import { Builder } from '../builder';
|
||||||
|
import { Template } from '../core/view';
|
||||||
|
import { Label } from '../label';
|
||||||
|
|
||||||
export * from './list-view-common';
|
export * from './list-view-common';
|
||||||
|
|
||||||
@@ -16,6 +19,10 @@ const ITEMLOADING = ListViewBase.itemLoadingEvent;
|
|||||||
const LOADMOREITEMS = ListViewBase.loadMoreItemsEvent;
|
const LOADMOREITEMS = ListViewBase.loadMoreItemsEvent;
|
||||||
const ITEMTAP = ListViewBase.itemTapEvent;
|
const ITEMTAP = ListViewBase.itemTapEvent;
|
||||||
|
|
||||||
|
// View type constants for sectioned lists
|
||||||
|
const ITEM_VIEW_TYPE = 0;
|
||||||
|
// HEADER_VIEW_TYPE will be dynamically calculated as the last index
|
||||||
|
|
||||||
interface ItemClickListener {
|
interface ItemClickListener {
|
||||||
new (owner: ListView): android.widget.AdapterView.OnItemClickListener;
|
new (owner: ListView): android.widget.AdapterView.OnItemClickListener;
|
||||||
}
|
}
|
||||||
@@ -293,6 +300,28 @@ export class ListView extends ListViewBase {
|
|||||||
this.nativeViewProtected.setAdapter(new ListViewAdapterClass(this));
|
this.nativeViewProtected.setAdapter(new ListViewAdapterClass(this));
|
||||||
this.refresh();
|
this.refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sticky header property handlers (for now just trigger refresh)
|
||||||
|
[stickyHeaderProperty.setNative](value: boolean) {
|
||||||
|
// Refresh adapter to handle sectioned vs non-sectioned display
|
||||||
|
if (this.nativeViewProtected && this.nativeViewProtected.getAdapter()) {
|
||||||
|
this.nativeViewProtected.setAdapter(new ListViewAdapterClass(this));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[stickyHeaderTemplateProperty.setNative](value: string) {
|
||||||
|
// Refresh adapter when template changes
|
||||||
|
if (this.nativeViewProtected && this.nativeViewProtected.getAdapter()) {
|
||||||
|
this.nativeViewProtected.setAdapter(new ListViewAdapterClass(this));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[sectionedProperty.setNative](value: boolean) {
|
||||||
|
// Refresh adapter to handle sectioned vs non-sectioned data
|
||||||
|
if (this.nativeViewProtected && this.nativeViewProtected.getAdapter()) {
|
||||||
|
this.nativeViewProtected.setAdapter(new ListViewAdapterClass(this));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let ListViewAdapterClass;
|
let ListViewAdapterClass;
|
||||||
@@ -310,19 +339,75 @@ function ensureListViewAdapterClass() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public getCount() {
|
public getCount() {
|
||||||
return this.owner && this.owner.items && this.owner.items.length ? this.owner.items.length : 0;
|
if (!this.owner || !this.owner.items) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.owner.sectioned) {
|
||||||
|
// Count items + section headers
|
||||||
|
let totalCount = 0;
|
||||||
|
const sectionCount = this.owner._getSectionCount();
|
||||||
|
for (let i = 0; i < sectionCount; i++) {
|
||||||
|
totalCount += 1; // Section header
|
||||||
|
totalCount += this.owner._getItemsInSection(i).length; // Items in section
|
||||||
|
}
|
||||||
|
return totalCount;
|
||||||
|
} else {
|
||||||
|
return this.owner.items.length;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public getItem(i: number) {
|
public getItem(i: number) {
|
||||||
if (this.owner && this.owner.items && i < this.owner.items.length) {
|
if (!this.owner || !this.owner.items) {
|
||||||
const getItem = (<ItemsSource>this.owner.items).getItem;
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return getItem ? getItem.call(this.owner.items, i) : this.owner.items[i];
|
if (this.owner.sectioned) {
|
||||||
|
const positionInfo = this._getPositionInfo(i);
|
||||||
|
if (positionInfo.isHeader) {
|
||||||
|
return this.owner._getSectionData(positionInfo.section);
|
||||||
|
} else {
|
||||||
|
return this.owner._getDataItemInSection(positionInfo.section, positionInfo.itemIndex);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (i < this.owner.items.length) {
|
||||||
|
const getItem = (<ItemsSource>this.owner.items).getItem;
|
||||||
|
return getItem ? getItem.call(this.owner.items, i) : this.owner.items[i];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper method to determine if position is header and get section/item info
|
||||||
|
private _getPositionInfo(position: number): { isHeader: boolean; section: number; itemIndex: number } {
|
||||||
|
if (!this.owner.sectioned) {
|
||||||
|
return { isHeader: false, section: 0, itemIndex: position };
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentPosition = 0;
|
||||||
|
const sectionCount = this.owner._getSectionCount();
|
||||||
|
|
||||||
|
for (let section = 0; section < sectionCount; section++) {
|
||||||
|
// Check if this position is the section header
|
||||||
|
if (currentPosition === position) {
|
||||||
|
return { isHeader: true, section: section, itemIndex: -1 };
|
||||||
|
}
|
||||||
|
currentPosition++; // Move past header
|
||||||
|
|
||||||
|
// Check if position is within this section's items
|
||||||
|
const itemsInSection = this.owner._getItemsInSection(section).length;
|
||||||
|
if (position < currentPosition + itemsInSection) {
|
||||||
|
const itemIndex = position - currentPosition;
|
||||||
|
return { isHeader: false, section: section, itemIndex: itemIndex };
|
||||||
|
}
|
||||||
|
currentPosition += itemsInSection; // Move past items
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback
|
||||||
|
return { isHeader: false, section: 0, itemIndex: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
public getItemId(i: number) {
|
public getItemId(i: number) {
|
||||||
const item = this.getItem(i);
|
const item = this.getItem(i);
|
||||||
let id = i;
|
let id = i;
|
||||||
@@ -338,14 +423,31 @@ function ensureListViewAdapterClass() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public getViewTypeCount() {
|
public getViewTypeCount() {
|
||||||
return this.owner._itemTemplatesInternal.length;
|
let count = this.owner._itemTemplatesInternal.length;
|
||||||
|
|
||||||
|
// Add 1 for header view type when sectioned
|
||||||
|
if (this.owner.sectioned && this.owner.stickyHeaderTemplate) {
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getItemViewType(index: number) {
|
public getItemViewType(index: number) {
|
||||||
const template = this.owner._getItemTemplate(index);
|
if (this.owner.sectioned) {
|
||||||
const itemViewType = this.owner._itemTemplatesInternal.indexOf(template);
|
const positionInfo = this._getPositionInfo(index);
|
||||||
|
if (positionInfo.isHeader) {
|
||||||
return itemViewType;
|
// Header view type is the last index (after all item template types)
|
||||||
|
return this.owner._itemTemplatesInternal.length;
|
||||||
|
} else {
|
||||||
|
// Get template for the actual item
|
||||||
|
const template = this.owner._getItemTemplate(positionInfo.itemIndex);
|
||||||
|
return this.owner._itemTemplatesInternal.indexOf(template);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const template = this.owner._getItemTemplate(index);
|
||||||
|
return this.owner._itemTemplatesInternal.indexOf(template);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@profile
|
@profile
|
||||||
@@ -356,17 +458,90 @@ function ensureListViewAdapterClass() {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const totalItemCount = this.owner.items ? this.owner.items.length : 0;
|
if (this.owner.sectioned) {
|
||||||
if (index === totalItemCount - 1) {
|
const positionInfo = this._getPositionInfo(index);
|
||||||
this.owner.notify({
|
|
||||||
eventName: LOADMOREITEMS,
|
if (positionInfo.isHeader) {
|
||||||
object: this.owner,
|
// Create section header view
|
||||||
|
return this._createHeaderView(positionInfo.section, convertView, parent);
|
||||||
|
} else {
|
||||||
|
// Create regular item view with adjusted index
|
||||||
|
return this._createItemView(positionInfo.section, positionInfo.itemIndex, convertView, parent);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Non-sectioned - use original logic
|
||||||
|
return this._createItemView(0, index, convertView, parent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _createHeaderView(section: number, convertView: android.view.View, parent: android.view.ViewGroup): android.view.View {
|
||||||
|
let headerView: View = null;
|
||||||
|
const headerViewType = this.owner._itemTemplatesInternal.length; // Same as getItemViewType for headers
|
||||||
|
|
||||||
|
// Try to reuse convertView if it's the right type
|
||||||
|
if (convertView) {
|
||||||
|
const existingData = this.owner._realizedItems.get(convertView);
|
||||||
|
if (existingData && existingData.templateKey === `header_${headerViewType}`) {
|
||||||
|
headerView = existingData.view;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new header view if we can't reuse
|
||||||
|
if (!headerView) {
|
||||||
|
if (this.owner.stickyHeaderTemplate) {
|
||||||
|
if (typeof this.owner.stickyHeaderTemplate === 'string') {
|
||||||
|
try {
|
||||||
|
headerView = Builder.parse(this.owner.stickyHeaderTemplate, this.owner);
|
||||||
|
} catch (error) {
|
||||||
|
// Fallback to simple label
|
||||||
|
headerView = new Label();
|
||||||
|
(headerView as Label).text = 'Header Error';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!headerView) {
|
||||||
|
// Default header
|
||||||
|
headerView = new Label();
|
||||||
|
(headerView as Label).text = `Section ${section}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add to parent and get native view
|
||||||
|
if (!headerView.parent) {
|
||||||
|
if (headerView instanceof LayoutBase && !(headerView instanceof ProxyViewContainer)) {
|
||||||
|
this.owner._addView(headerView);
|
||||||
|
convertView = headerView.nativeViewProtected;
|
||||||
|
} else {
|
||||||
|
const sp = new StackLayout();
|
||||||
|
sp.addChild(headerView);
|
||||||
|
this.owner._addView(sp);
|
||||||
|
convertView = sp.nativeViewProtected;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register the header view for recycling
|
||||||
|
this.owner._realizedItems.set(convertView, {
|
||||||
|
view: headerView,
|
||||||
|
templateKey: `header_${headerViewType}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recycle an existing view or create a new one if needed.
|
// Set binding context to section data (always update, even for recycled views)
|
||||||
const template = this.owner._getItemTemplate(index);
|
const sectionData = this.owner._getSectionData(section);
|
||||||
|
if (sectionData) {
|
||||||
|
headerView.bindingContext = sectionData;
|
||||||
|
} else {
|
||||||
|
headerView.bindingContext = { title: `Section ${section}`, section: section };
|
||||||
|
}
|
||||||
|
|
||||||
|
return convertView;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _createItemView(section: number, itemIndex: number, convertView: android.view.View, parent: android.view.ViewGroup): android.view.View {
|
||||||
|
// Use existing item creation logic but with sectioned data
|
||||||
|
const template = this.owner._getItemTemplate(itemIndex);
|
||||||
let view: View;
|
let view: View;
|
||||||
|
|
||||||
// convertView is of the wrong type
|
// convertView is of the wrong type
|
||||||
if (convertView && this.owner._getKeyFromView(convertView) !== template.key) {
|
if (convertView && this.owner._getKeyFromView(convertView) !== template.key) {
|
||||||
this.owner._markViewUnused(convertView); // release this view
|
this.owner._markViewUnused(convertView); // release this view
|
||||||
@@ -383,7 +558,7 @@ function ensureListViewAdapterClass() {
|
|||||||
const args: ItemEventData = {
|
const args: ItemEventData = {
|
||||||
eventName: ITEMLOADING,
|
eventName: ITEMLOADING,
|
||||||
object: this.owner,
|
object: this.owner,
|
||||||
index: index,
|
index: itemIndex,
|
||||||
view: view,
|
view: view,
|
||||||
android: parent,
|
android: parent,
|
||||||
ios: undefined,
|
ios: undefined,
|
||||||
@@ -392,7 +567,7 @@ function ensureListViewAdapterClass() {
|
|||||||
this.owner.notify(args);
|
this.owner.notify(args);
|
||||||
|
|
||||||
if (!args.view) {
|
if (!args.view) {
|
||||||
args.view = this.owner._getDefaultItemContent(index);
|
args.view = this.owner._getDefaultItemContent(itemIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (args.view) {
|
if (args.view) {
|
||||||
@@ -402,7 +577,13 @@ function ensureListViewAdapterClass() {
|
|||||||
args.view.height = <CoreTypes.LengthType>unsetValue;
|
args.view.height = <CoreTypes.LengthType>unsetValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.owner._prepareItem(args.view, index);
|
// Use sectioned item preparation
|
||||||
|
if (this.owner.sectioned) {
|
||||||
|
this.owner._prepareItemInSection(args.view, section, itemIndex);
|
||||||
|
} else {
|
||||||
|
this.owner._prepareItem(args.view, itemIndex);
|
||||||
|
}
|
||||||
|
|
||||||
if (!args.view.parent) {
|
if (!args.view.parent) {
|
||||||
// Proxy containers should not get treated as layouts.
|
// Proxy containers should not get treated as layouts.
|
||||||
// Wrap them in a real layout as well.
|
// Wrap them in a real layout as well.
|
||||||
|
|||||||
Reference in New Issue
Block a user