Skip to content

Commit 4b51e0b

Browse files
committed
feat(android): ListView sticky header options 1
1 parent 783f8ed commit 4b51e0b

File tree

1 file changed

+201
-20
lines changed

1 file changed

+201
-20
lines changed

packages/core/ui/list-view/index.android.ts

Lines changed: 201 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ItemEventData, ItemsSource } from '.';
2-
import { ListViewBase, separatorColorProperty, itemTemplatesProperty } from './list-view-common';
2+
import { ListViewBase, separatorColorProperty, itemTemplatesProperty, stickyHeaderProperty, stickyHeaderTemplateProperty, stickyHeaderHeightProperty, sectionedProperty } from './list-view-common';
33
import { View, KeyedTemplate } from '../core/view';
44
import { unsetValue } from '../core/properties/property-shared';
55
import { CoreTypes } from '../../core-types';
@@ -9,13 +9,20 @@ import { StackLayout } from '../layouts/stack-layout';
99
import { ProxyViewContainer } from '../proxy-view-container';
1010
import { LayoutBase } from '../layouts/layout-base';
1111
import { profile } from '../../profiling';
12+
import { Builder } from '../builder';
13+
import { Template } from '../core/view';
14+
import { Label } from '../label';
1215

1316
export * from './list-view-common';
1417

1518
const ITEMLOADING = ListViewBase.itemLoadingEvent;
1619
const LOADMOREITEMS = ListViewBase.loadMoreItemsEvent;
1720
const ITEMTAP = ListViewBase.itemTapEvent;
1821

22+
// View type constants for sectioned lists
23+
const ITEM_VIEW_TYPE = 0;
24+
// HEADER_VIEW_TYPE will be dynamically calculated as the last index
25+
1926
interface ItemClickListener {
2027
new (owner: ListView): android.widget.AdapterView.OnItemClickListener;
2128
}
@@ -293,6 +300,28 @@ export class ListView extends ListViewBase {
293300
this.nativeViewProtected.setAdapter(new ListViewAdapterClass(this));
294301
this.refresh();
295302
}
303+
304+
// Sticky header property handlers (for now just trigger refresh)
305+
[stickyHeaderProperty.setNative](value: boolean) {
306+
// Refresh adapter to handle sectioned vs non-sectioned display
307+
if (this.nativeViewProtected && this.nativeViewProtected.getAdapter()) {
308+
this.nativeViewProtected.setAdapter(new ListViewAdapterClass(this));
309+
}
310+
}
311+
312+
[stickyHeaderTemplateProperty.setNative](value: string) {
313+
// Refresh adapter when template changes
314+
if (this.nativeViewProtected && this.nativeViewProtected.getAdapter()) {
315+
this.nativeViewProtected.setAdapter(new ListViewAdapterClass(this));
316+
}
317+
}
318+
319+
[sectionedProperty.setNative](value: boolean) {
320+
// Refresh adapter to handle sectioned vs non-sectioned data
321+
if (this.nativeViewProtected && this.nativeViewProtected.getAdapter()) {
322+
this.nativeViewProtected.setAdapter(new ListViewAdapterClass(this));
323+
}
324+
}
296325
}
297326

298327
let ListViewAdapterClass;
@@ -310,19 +339,75 @@ function ensureListViewAdapterClass() {
310339
}
311340

312341
public getCount() {
313-
return this.owner && this.owner.items && this.owner.items.length ? this.owner.items.length : 0;
342+
if (!this.owner || !this.owner.items) {
343+
return 0;
344+
}
345+
346+
if (this.owner.sectioned) {
347+
// Count items + section headers
348+
let totalCount = 0;
349+
const sectionCount = this.owner._getSectionCount();
350+
for (let i = 0; i < sectionCount; i++) {
351+
totalCount += 1; // Section header
352+
totalCount += this.owner._getItemsInSection(i).length; // Items in section
353+
}
354+
return totalCount;
355+
} else {
356+
return this.owner.items.length;
357+
}
314358
}
315359

316360
public getItem(i: number) {
317-
if (this.owner && this.owner.items && i < this.owner.items.length) {
318-
const getItem = (<ItemsSource>this.owner.items).getItem;
361+
if (!this.owner || !this.owner.items) {
362+
return null;
363+
}
319364

320-
return getItem ? getItem.call(this.owner.items, i) : this.owner.items[i];
365+
if (this.owner.sectioned) {
366+
const positionInfo = this._getPositionInfo(i);
367+
if (positionInfo.isHeader) {
368+
return this.owner._getSectionData(positionInfo.section);
369+
} else {
370+
return this.owner._getDataItemInSection(positionInfo.section, positionInfo.itemIndex);
371+
}
372+
} else {
373+
if (i < this.owner.items.length) {
374+
const getItem = (<ItemsSource>this.owner.items).getItem;
375+
return getItem ? getItem.call(this.owner.items, i) : this.owner.items[i];
376+
}
321377
}
322378

323379
return null;
324380
}
325381

382+
// Helper method to determine if position is header and get section/item info
383+
private _getPositionInfo(position: number): { isHeader: boolean; section: number; itemIndex: number } {
384+
if (!this.owner.sectioned) {
385+
return { isHeader: false, section: 0, itemIndex: position };
386+
}
387+
388+
let currentPosition = 0;
389+
const sectionCount = this.owner._getSectionCount();
390+
391+
for (let section = 0; section < sectionCount; section++) {
392+
// Check if this position is the section header
393+
if (currentPosition === position) {
394+
return { isHeader: true, section: section, itemIndex: -1 };
395+
}
396+
currentPosition++; // Move past header
397+
398+
// Check if position is within this section's items
399+
const itemsInSection = this.owner._getItemsInSection(section).length;
400+
if (position < currentPosition + itemsInSection) {
401+
const itemIndex = position - currentPosition;
402+
return { isHeader: false, section: section, itemIndex: itemIndex };
403+
}
404+
currentPosition += itemsInSection; // Move past items
405+
}
406+
407+
// Fallback
408+
return { isHeader: false, section: 0, itemIndex: 0 };
409+
}
410+
326411
public getItemId(i: number) {
327412
const item = this.getItem(i);
328413
let id = i;
@@ -338,14 +423,31 @@ function ensureListViewAdapterClass() {
338423
}
339424

340425
public getViewTypeCount() {
341-
return this.owner._itemTemplatesInternal.length;
426+
let count = this.owner._itemTemplatesInternal.length;
427+
428+
// Add 1 for header view type when sectioned
429+
if (this.owner.sectioned && this.owner.stickyHeaderTemplate) {
430+
count += 1;
431+
}
432+
433+
return count;
342434
}
343435

344436
public getItemViewType(index: number) {
345-
const template = this.owner._getItemTemplate(index);
346-
const itemViewType = this.owner._itemTemplatesInternal.indexOf(template);
347-
348-
return itemViewType;
437+
if (this.owner.sectioned) {
438+
const positionInfo = this._getPositionInfo(index);
439+
if (positionInfo.isHeader) {
440+
// Header view type is the last index (after all item template types)
441+
return this.owner._itemTemplatesInternal.length;
442+
} else {
443+
// Get template for the actual item
444+
const template = this.owner._getItemTemplate(positionInfo.itemIndex);
445+
return this.owner._itemTemplatesInternal.indexOf(template);
446+
}
447+
} else {
448+
const template = this.owner._getItemTemplate(index);
449+
return this.owner._itemTemplatesInternal.indexOf(template);
450+
}
349451
}
350452

351453
@profile
@@ -356,17 +458,90 @@ function ensureListViewAdapterClass() {
356458
return null;
357459
}
358460

359-
const totalItemCount = this.owner.items ? this.owner.items.length : 0;
360-
if (index === totalItemCount - 1) {
361-
this.owner.notify({
362-
eventName: LOADMOREITEMS,
363-
object: this.owner,
461+
if (this.owner.sectioned) {
462+
const positionInfo = this._getPositionInfo(index);
463+
464+
if (positionInfo.isHeader) {
465+
// Create section header view
466+
return this._createHeaderView(positionInfo.section, convertView, parent);
467+
} else {
468+
// Create regular item view with adjusted index
469+
return this._createItemView(positionInfo.section, positionInfo.itemIndex, convertView, parent);
470+
}
471+
} else {
472+
// Non-sectioned - use original logic
473+
return this._createItemView(0, index, convertView, parent);
474+
}
475+
}
476+
477+
private _createHeaderView(section: number, convertView: android.view.View, parent: android.view.ViewGroup): android.view.View {
478+
let headerView: View = null;
479+
const headerViewType = this.owner._itemTemplatesInternal.length; // Same as getItemViewType for headers
480+
481+
// Try to reuse convertView if it's the right type
482+
if (convertView) {
483+
const existingData = this.owner._realizedItems.get(convertView);
484+
if (existingData && existingData.templateKey === `header_${headerViewType}`) {
485+
headerView = existingData.view;
486+
}
487+
}
488+
489+
// Create new header view if we can't reuse
490+
if (!headerView) {
491+
if (this.owner.stickyHeaderTemplate) {
492+
if (typeof this.owner.stickyHeaderTemplate === 'string') {
493+
try {
494+
headerView = Builder.parse(this.owner.stickyHeaderTemplate, this.owner);
495+
} catch (error) {
496+
// Fallback to simple label
497+
headerView = new Label();
498+
(headerView as Label).text = 'Header Error';
499+
}
500+
}
501+
}
502+
503+
if (!headerView) {
504+
// Default header
505+
headerView = new Label();
506+
(headerView as Label).text = `Section ${section}`;
507+
}
508+
509+
// Add to parent and get native view
510+
if (!headerView.parent) {
511+
if (headerView instanceof LayoutBase && !(headerView instanceof ProxyViewContainer)) {
512+
this.owner._addView(headerView);
513+
convertView = headerView.nativeViewProtected;
514+
} else {
515+
const sp = new StackLayout();
516+
sp.addChild(headerView);
517+
this.owner._addView(sp);
518+
convertView = sp.nativeViewProtected;
519+
}
520+
}
521+
522+
// Register the header view for recycling
523+
this.owner._realizedItems.set(convertView, {
524+
view: headerView,
525+
templateKey: `header_${headerViewType}`,
364526
});
365527
}
366528

367-
// Recycle an existing view or create a new one if needed.
368-
const template = this.owner._getItemTemplate(index);
529+
// Set binding context to section data (always update, even for recycled views)
530+
const sectionData = this.owner._getSectionData(section);
531+
if (sectionData) {
532+
headerView.bindingContext = sectionData;
533+
} else {
534+
headerView.bindingContext = { title: `Section ${section}`, section: section };
535+
}
536+
537+
return convertView;
538+
}
539+
540+
private _createItemView(section: number, itemIndex: number, convertView: android.view.View, parent: android.view.ViewGroup): android.view.View {
541+
// Use existing item creation logic but with sectioned data
542+
const template = this.owner._getItemTemplate(itemIndex);
369543
let view: View;
544+
370545
// convertView is of the wrong type
371546
if (convertView && this.owner._getKeyFromView(convertView) !== template.key) {
372547
this.owner._markViewUnused(convertView); // release this view
@@ -383,7 +558,7 @@ function ensureListViewAdapterClass() {
383558
const args: ItemEventData = {
384559
eventName: ITEMLOADING,
385560
object: this.owner,
386-
index: index,
561+
index: itemIndex,
387562
view: view,
388563
android: parent,
389564
ios: undefined,
@@ -392,7 +567,7 @@ function ensureListViewAdapterClass() {
392567
this.owner.notify(args);
393568

394569
if (!args.view) {
395-
args.view = this.owner._getDefaultItemContent(index);
570+
args.view = this.owner._getDefaultItemContent(itemIndex);
396571
}
397572

398573
if (args.view) {
@@ -402,7 +577,13 @@ function ensureListViewAdapterClass() {
402577
args.view.height = <CoreTypes.LengthType>unsetValue;
403578
}
404579

405-
this.owner._prepareItem(args.view, index);
580+
// Use sectioned item preparation
581+
if (this.owner.sectioned) {
582+
this.owner._prepareItemInSection(args.view, section, itemIndex);
583+
} else {
584+
this.owner._prepareItem(args.view, itemIndex);
585+
}
586+
406587
if (!args.view.parent) {
407588
// Proxy containers should not get treated as layouts.
408589
// Wrap them in a real layout as well.

0 commit comments

Comments
 (0)