From 88e0569e81efc6e61861fd03cd6e7b4ec2db1b89 Mon Sep 17 00:00:00 2001 From: Lior Agnin Date: Tue, 2 May 2023 17:26:43 +0300 Subject: [PATCH] group section style --- example/lib/section_example.dart | 94 ++++++++++++++++++++++++++++++++ lib/grouped_list.dart | 65 ++++++++++++++++------ 2 files changed, 143 insertions(+), 16 deletions(-) create mode 100644 example/lib/section_example.dart diff --git a/example/lib/section_example.dart b/example/lib/section_example.dart new file mode 100644 index 0000000..2406feb --- /dev/null +++ b/example/lib/section_example.dart @@ -0,0 +1,94 @@ +import 'package:flutter/material.dart'; +import 'package:grouped_list/grouped_list.dart'; + +void main() => runApp(const MyApp()); + +List _elements = [ + {'name': 'John', 'group': 'Team A'}, + {'name': 'Will', 'group': 'Team B'}, + {'name': 'Beth', 'group': 'Team A'}, + {'name': 'Miranda', 'group': 'Team B'}, + {'name': 'Mike', 'group': 'Team C'}, + {'name': 'Danny', 'group': 'Team C'}, +]; + +class MyApp extends StatelessWidget { + const MyApp({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return MaterialApp( + debugShowCheckedModeBanner: false, + title: 'Flutter Demo', + theme: ThemeData( + primarySwatch: Colors.blue, + ), + home: Scaffold( + appBar: AppBar( + title: const Text('Grouped List View Example'), + ), + body: _createGroupedListView(), + ), + ); + } + + _createGroupedListView() { + return GroupedListView( + elements: _elements, + groupBy: (element) => element['group'], + groupComparator: (value1, value2) => value2.compareTo(value1), + itemComparator: (item1, item2) => item1['name'].compareTo(item2['name']), + order: GroupedListOrder.DESC, + useStickyGroupSeparators: true, + groupSeparatorBuilder: (String value) => Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + value, + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + ), + ), + itemBuilder: (c, element) { + return Card( + elevation: 8.0, + margin: const EdgeInsets.symmetric(horizontal: 10.0), + child: SizedBox( + child: ListTile( + contentPadding: + const EdgeInsets.symmetric(horizontal: 20.0, vertical: 10.0), + leading: const Icon(Icons.account_circle), + title: Text(element['name']), + trailing: const Icon(Icons.arrow_forward), + ), + ), + ); + }, + groupItemBuilder: (c, element, groupStart, groupEnd) { + ShapeBorder? shapeBorder; + if (groupStart) { + shapeBorder = const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(15), topRight: Radius.circular(15))); + } else if (groupEnd) { + shapeBorder = const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(15), bottomRight: Radius.circular(15))); + } + return Card( + elevation: 8.0, + margin: const EdgeInsets.symmetric(horizontal: 10.0), + shape: shapeBorder, + child: SizedBox( + child: ListTile( + contentPadding: + const EdgeInsets.symmetric(horizontal: 20.0, vertical: 10.0), + leading: const Icon(Icons.account_circle), + title: Text(element['name']), + trailing: const Icon(Icons.arrow_forward), + ), + ), + ); + }, + ); + } +} \ No newline at end of file diff --git a/lib/grouped_list.dart b/lib/grouped_list.dart index 7b69671..3abe692 100644 --- a/lib/grouped_list.dart +++ b/lib/grouped_list.dart @@ -67,6 +67,15 @@ class GroupedListView extends StatefulWidget { final Widget Function(BuildContext context, T? previousElement, T currentElement, T? nextElement)? interdependentItemBuilder; + /// Called to build children for the list with additional information about + /// whether the item is at the start or end of a group. + /// + /// The [groupStart] parameter is `true` if the item is the first in its group. + /// The [groupEnd] parameter is `true` if the item is the last in its group. + final Widget Function( + BuildContext context, T element, bool groupStart, bool groupEnd)? + groupItemBuilder; + /// Called to build children for the list with /// 0 <= element, index < elements.length final Widget Function(BuildContext context, T element, int index)? @@ -222,6 +231,7 @@ class GroupedListView extends StatefulWidget { this.groupStickyHeaderBuilder, this.emptyPlaceholder, this.itemBuilder, + this.groupItemBuilder, this.indexedItemBuilder, this.interdependentItemBuilder, this.itemComparator, @@ -251,7 +261,8 @@ class GroupedListView extends StatefulWidget { this.footer, }) : assert(itemBuilder != null || indexedItemBuilder != null || - interdependentItemBuilder != null), + interdependentItemBuilder != null || + groupItemBuilder != null), assert(groupSeparatorBuilder != null || groupHeaderBuilder != null); @override @@ -301,6 +312,7 @@ class _GroupedListViewState extends State> { _sortedElements = _sortElements(); var hiddenIndex = widget.reverse ? _sortedElements.length * 2 - 1 : 0; var isSeparator = widget.reverse ? (int i) => i.isOdd : (int i) => i.isEven; + isValidIndex(int i) => i >= 0 && i < _sortedElements.length; _ambiguate(WidgetsBinding.instance)!.addPostFrameCallback((_) { _scrollListener(); @@ -324,16 +336,22 @@ class _GroupedListViewState extends State> { child: _buildGroupSeparator(_sortedElements[actualIndex]), ); } + var curr = widget.groupBy(_sortedElements[actualIndex]); + var preIndex = actualIndex + (widget.reverse ? 1 : -1); + var prev = isValidIndex(preIndex) + ? widget.groupBy(_sortedElements[preIndex]) + : null; + var nextIndex = actualIndex + (widget.reverse ? -1 : 1); + var next = isValidIndex(nextIndex) + ? widget.groupBy(_sortedElements[nextIndex]) + : null; if (isSeparator(index)) { - var curr = widget.groupBy(_sortedElements[actualIndex]); - var prev = widget - .groupBy(_sortedElements[actualIndex + (widget.reverse ? 1 : -1)]); if (prev != curr) { return _buildGroupSeparator(_sortedElements[actualIndex]); } return widget.separator; } - return _buildItem(context, actualIndex); + return _buildItem(context, actualIndex, prev != curr, curr != next); } return Stack( @@ -379,20 +397,35 @@ class _GroupedListViewState extends State> { /// Returns the widget for element positioned at [index]. The widget is /// retrieved either by [widget.indexedItemBuilder], [widget.itemBuilder] /// or [widget.interdependentItemBuilder]. - Widget _buildItem(context, int index) => KeyedSubtree( + Widget _buildItem(context, int index, bool groupStart, bool groupEnd) => + KeyedSubtree( key: _keys.putIfAbsent('$index', () => GlobalKey()), - child: widget.indexedItemBuilder != null - ? widget.indexedItemBuilder!(context, _sortedElements[index], index) - : widget.interdependentItemBuilder != null - ? widget.interdependentItemBuilder!( + child: widget.groupItemBuilder != null + ? widget.groupItemBuilder!( + context, + _sortedElements[index], + groupStart, + groupEnd, + ) + : widget.indexedItemBuilder != null + ? widget.indexedItemBuilder!( context, - index > 0 ? _sortedElements[index - 1] : null, _sortedElements[index], - index + 1 < _sortedElements.length - ? _sortedElements[index + 1] - : null, + index, ) - : widget.itemBuilder!(context, _sortedElements[index]), + : widget.interdependentItemBuilder != null + ? widget.interdependentItemBuilder!( + context, + index > 0 ? _sortedElements[index - 1] : null, + _sortedElements[index], + index + 1 < _sortedElements.length + ? _sortedElements[index + 1] + : null, + ) + : widget.itemBuilder!( + context, + _sortedElements[index], + ), ); /// This scroll listener is added to the lists controller if @@ -533,4 +566,4 @@ class _GroupedListViewState extends State> { return key.currentContext != null && key.currentContext!.findRenderObject() != null; } -} +} \ No newline at end of file