1
1
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' ;
3
3
import { View , KeyedTemplate } from '../core/view' ;
4
4
import { unsetValue } from '../core/properties/property-shared' ;
5
5
import { CoreTypes } from '../../core-types' ;
@@ -9,13 +9,20 @@ import { StackLayout } from '../layouts/stack-layout';
9
9
import { ProxyViewContainer } from '../proxy-view-container' ;
10
10
import { LayoutBase } from '../layouts/layout-base' ;
11
11
import { profile } from '../../profiling' ;
12
+ import { Builder } from '../builder' ;
13
+ import { Template } from '../core/view' ;
14
+ import { Label } from '../label' ;
12
15
13
16
export * from './list-view-common' ;
14
17
15
18
const ITEMLOADING = ListViewBase . itemLoadingEvent ;
16
19
const LOADMOREITEMS = ListViewBase . loadMoreItemsEvent ;
17
20
const ITEMTAP = ListViewBase . itemTapEvent ;
18
21
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
+
19
26
interface ItemClickListener {
20
27
new ( owner : ListView ) : android . widget . AdapterView . OnItemClickListener ;
21
28
}
@@ -293,6 +300,28 @@ export class ListView extends ListViewBase {
293
300
this . nativeViewProtected . setAdapter ( new ListViewAdapterClass ( this ) ) ;
294
301
this . refresh ( ) ;
295
302
}
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
+ }
296
325
}
297
326
298
327
let ListViewAdapterClass ;
@@ -310,19 +339,75 @@ function ensureListViewAdapterClass() {
310
339
}
311
340
312
341
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
+ }
314
358
}
315
359
316
360
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
+ }
319
364
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
+ }
321
377
}
322
378
323
379
return null ;
324
380
}
325
381
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
+
326
411
public getItemId ( i : number ) {
327
412
const item = this . getItem ( i ) ;
328
413
let id = i ;
@@ -338,14 +423,31 @@ function ensureListViewAdapterClass() {
338
423
}
339
424
340
425
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 ;
342
434
}
343
435
344
436
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
+ }
349
451
}
350
452
351
453
@profile
@@ -356,17 +458,90 @@ function ensureListViewAdapterClass() {
356
458
return null ;
357
459
}
358
460
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 } ` ,
364
526
} ) ;
365
527
}
366
528
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 ) ;
369
543
let view : View ;
544
+
370
545
// convertView is of the wrong type
371
546
if ( convertView && this . owner . _getKeyFromView ( convertView ) !== template . key ) {
372
547
this . owner . _markViewUnused ( convertView ) ; // release this view
@@ -383,7 +558,7 @@ function ensureListViewAdapterClass() {
383
558
const args : ItemEventData = {
384
559
eventName : ITEMLOADING ,
385
560
object : this . owner ,
386
- index : index ,
561
+ index : itemIndex ,
387
562
view : view ,
388
563
android : parent ,
389
564
ios : undefined ,
@@ -392,7 +567,7 @@ function ensureListViewAdapterClass() {
392
567
this . owner . notify ( args ) ;
393
568
394
569
if ( ! args . view ) {
395
- args . view = this . owner . _getDefaultItemContent ( index ) ;
570
+ args . view = this . owner . _getDefaultItemContent ( itemIndex ) ;
396
571
}
397
572
398
573
if ( args . view ) {
@@ -402,7 +577,13 @@ function ensureListViewAdapterClass() {
402
577
args . view . height = < CoreTypes . LengthType > unsetValue ;
403
578
}
404
579
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
+
406
587
if ( ! args . view . parent ) {
407
588
// Proxy containers should not get treated as layouts.
408
589
// Wrap them in a real layout as well.
0 commit comments