mirror of
https://github.com/Iconica-Development/flutter_nested_categories.git
synced 2025-05-18 23:33:44 +02:00
feat: add nested category functionality
This commit is contained in:
parent
fd6e7170bf
commit
54779ae828
4 changed files with 301 additions and 0 deletions
9
lib/flutter_nested_categories.dart
Normal file
9
lib/flutter_nested_categories.dart
Normal file
|
@ -0,0 +1,9 @@
|
|||
/// Flutter Nested Categories
|
||||
///
|
||||
/// A Flutter package that allows you to create nested categories that
|
||||
/// are very easy to use and customize.
|
||||
library flutter_nested_categories;
|
||||
|
||||
export "src/category.dart";
|
||||
export "src/category_header_styling.dart";
|
||||
export "src/category_list.dart";
|
31
lib/src/category.dart
Normal file
31
lib/src/category.dart
Normal file
|
@ -0,0 +1,31 @@
|
|||
import "package:flutter/widgets.dart";
|
||||
|
||||
/// One of the categories in the list. Each category can contain content,
|
||||
/// which can be an empty list, but also nested categories. The nested
|
||||
/// categories are the same type as the parent category which allows for
|
||||
/// infinite nesting.
|
||||
class Category {
|
||||
const Category({
|
||||
/// The name of the category.
|
||||
required this.name,
|
||||
|
||||
/// The content of the category. This can be anything, but is usually
|
||||
/// a list of widgets.
|
||||
///
|
||||
/// Default is an empty list.
|
||||
///
|
||||
/// If the category has nested categories, it will show this content
|
||||
/// before the nested categories.
|
||||
this.content = const <Widget>[],
|
||||
|
||||
/// The nested categories of this category. This can be an empty list
|
||||
/// if there are no nested categories.
|
||||
///
|
||||
/// Default is an empty list.
|
||||
this.nestedCategories = const <Category>[],
|
||||
});
|
||||
|
||||
final String name;
|
||||
final List<Category> nestedCategories;
|
||||
final List<Widget> content;
|
||||
}
|
18
lib/src/category_header_styling.dart
Normal file
18
lib/src/category_header_styling.dart
Normal file
|
@ -0,0 +1,18 @@
|
|||
import "package:flutter/widgets.dart";
|
||||
|
||||
/// This class is used to style the headers of the categories in the list.
|
||||
/// The headers are the names of the categories.
|
||||
class CategoryHeaderStyling {
|
||||
const CategoryHeaderStyling({
|
||||
/// The styles for the headers. The key is the depth of the category.
|
||||
/// The value is the text style for the header.
|
||||
required this.headerStyles,
|
||||
|
||||
/// The default style for the headers. This will be used if the depth
|
||||
/// is not found in the [headerStyles].
|
||||
this.defaultStyle,
|
||||
});
|
||||
|
||||
final Map<int, TextStyle?> headerStyles;
|
||||
final TextStyle? defaultStyle;
|
||||
}
|
243
lib/src/category_list.dart
Normal file
243
lib/src/category_list.dart
Normal file
|
@ -0,0 +1,243 @@
|
|||
import "package:flutter/material.dart";
|
||||
import "package:flutter_nested_categories/flutter_nested_categories.dart";
|
||||
|
||||
/// This widget displays a list of categories. Each category can contain
|
||||
/// content, which can be an empty list, but also nested categories. The
|
||||
/// nested categories are the same type as the parent category which allows
|
||||
/// for infinite nesting.
|
||||
///
|
||||
/// The content of the categories is displayed in the order it is given in
|
||||
/// the list. If a category has nested categories, the content of the nested
|
||||
/// categories is displayed after the content of the parent category.
|
||||
class CategoryList extends StatelessWidget {
|
||||
const CategoryList({
|
||||
/// The content of the category list. This is a list of categories.
|
||||
required this.content,
|
||||
|
||||
/// Optional header styling for the categories. This will be applied to
|
||||
/// the name of the categories. If not set, the default text style will
|
||||
/// be used.
|
||||
this.headerStyling,
|
||||
|
||||
/// Configure if the category header should be centered.
|
||||
///
|
||||
/// Default is false.
|
||||
this.headerCentered = false,
|
||||
|
||||
/// Optional custom title widget for the category list. This will be
|
||||
/// displayed at the top of the list. If set, the text title will be
|
||||
/// ignored.
|
||||
this.customTitle,
|
||||
|
||||
/// Optional title for the category list. This will be displayed at the
|
||||
/// top of the list.
|
||||
this.title,
|
||||
|
||||
/// Optional title style for the title of the category list. This will
|
||||
/// be applied to the title of the category list. If not set, the default
|
||||
/// text style will be used.
|
||||
this.titleStyle,
|
||||
|
||||
/// Configure if the title should be centered.
|
||||
///
|
||||
/// Default is false.
|
||||
this.titleCentered = false,
|
||||
|
||||
/// Configure if the category should be collapsible.
|
||||
///
|
||||
/// Default is true.
|
||||
this.isCategoryCollapsible = true,
|
||||
|
||||
/// The depth of the category list. This is used to keep track of
|
||||
/// the depth of the categories. This is used internally and should
|
||||
/// not be set manually.
|
||||
this.categoryDepth = 0,
|
||||
|
||||
/// The key for this widget.
|
||||
super.key,
|
||||
});
|
||||
|
||||
final String? title;
|
||||
final Widget? customTitle;
|
||||
final TextStyle? titleStyle;
|
||||
final bool titleCentered;
|
||||
|
||||
final CategoryHeaderStyling? headerStyling;
|
||||
final bool headerCentered;
|
||||
|
||||
final bool isCategoryCollapsible;
|
||||
final int categoryDepth;
|
||||
|
||||
final List<Category> content;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (title != null || customTitle != null)
|
||||
titleCentered
|
||||
? Center(
|
||||
child: _CategoryTitle(
|
||||
title: title,
|
||||
titleStyle: titleStyle,
|
||||
customTitle: customTitle,
|
||||
),
|
||||
)
|
||||
: _CategoryTitle(
|
||||
title: title,
|
||||
titleStyle: titleStyle,
|
||||
customTitle: customTitle,
|
||||
),
|
||||
...content.map(
|
||||
(category) => _CategoryColumn(
|
||||
category: category,
|
||||
headerCentered: headerCentered,
|
||||
headerStyling: headerStyling,
|
||||
collapsible: isCategoryCollapsible,
|
||||
categoryDepth: categoryDepth,
|
||||
),
|
||||
),
|
||||
].toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class _CategoryTitle extends StatelessWidget {
|
||||
const _CategoryTitle({
|
||||
this.customTitle,
|
||||
this.title,
|
||||
this.titleStyle,
|
||||
}) : assert(
|
||||
customTitle != null || title != null,
|
||||
"Either customTitle or title must be set.",
|
||||
);
|
||||
|
||||
final String? title;
|
||||
final Widget? customTitle;
|
||||
final TextStyle? titleStyle;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var theme = Theme.of(context);
|
||||
var styleOfTitle = titleStyle ?? theme.textTheme.titleLarge;
|
||||
|
||||
if (customTitle != null) {
|
||||
return customTitle!;
|
||||
} else {
|
||||
return Text(
|
||||
title!,
|
||||
style: styleOfTitle,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _CategoryColumn extends StatefulWidget {
|
||||
const _CategoryColumn({
|
||||
required this.category,
|
||||
required this.categoryDepth,
|
||||
required this.headerCentered,
|
||||
this.headerStyling,
|
||||
this.collapsible = true,
|
||||
});
|
||||
|
||||
final Category category;
|
||||
final CategoryHeaderStyling? headerStyling;
|
||||
final bool headerCentered;
|
||||
final int categoryDepth;
|
||||
final bool collapsible;
|
||||
|
||||
@override
|
||||
State<_CategoryColumn> createState() => _CategoryColumnState();
|
||||
}
|
||||
|
||||
class _CategoryColumnState extends State<_CategoryColumn> {
|
||||
bool _isExpanded = true;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var theme = Theme.of(context);
|
||||
|
||||
var styleOfCategory =
|
||||
widget.headerStyling?.headerStyles[widget.categoryDepth] ??
|
||||
widget.headerStyling?.defaultStyle ??
|
||||
theme.textTheme.titleMedium ??
|
||||
const TextStyle();
|
||||
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (widget.collapsible) _buildCollapsibleHeader(styleOfCategory),
|
||||
if (!widget.collapsible) _buildNonCollapsibleHeader(styleOfCategory),
|
||||
AnimatedCrossFade(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
firstChild: const SizedBox(width: double.infinity),
|
||||
secondChild: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (widget.category.content.isNotEmpty)
|
||||
...widget.category.content,
|
||||
if (widget.category.nestedCategories.isNotEmpty)
|
||||
_buildNestedCategoryList(),
|
||||
],
|
||||
),
|
||||
crossFadeState: _isExpanded
|
||||
? CrossFadeState.showSecond
|
||||
: CrossFadeState.showFirst,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCollapsibleHeader(TextStyle styleOfCategory) => InkWell(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_isExpanded = !_isExpanded;
|
||||
});
|
||||
},
|
||||
child: Row(
|
||||
mainAxisAlignment: widget.headerCentered
|
||||
? MainAxisAlignment.center
|
||||
: MainAxisAlignment.start,
|
||||
children: [
|
||||
ExpandIcon(
|
||||
onPressed: (value) => setState(() {
|
||||
_isExpanded = !_isExpanded;
|
||||
}),
|
||||
isExpanded: _isExpanded,
|
||||
padding: EdgeInsets.zero,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
widget.category.name,
|
||||
style: styleOfCategory,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
Widget _buildNonCollapsibleHeader(TextStyle styleOfCategory) =>
|
||||
widget.headerCentered
|
||||
? Center(
|
||||
child: Text(
|
||||
widget.category.name,
|
||||
style: styleOfCategory,
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
widget.category.name,
|
||||
style: styleOfCategory,
|
||||
);
|
||||
|
||||
Widget _buildNestedCategoryList() => CategoryList(
|
||||
content: widget.category.nestedCategories,
|
||||
headerCentered: widget.headerCentered,
|
||||
headerStyling: widget.headerStyling,
|
||||
isCategoryCollapsible: widget.collapsible,
|
||||
categoryDepth: widget.categoryDepth + 1,
|
||||
);
|
||||
}
|
Loading…
Reference in a new issue