blob: bdcb445c861433bcc1f02d2ed6d0e5bd448a0720 [file] [log] [blame]
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/widgets.dart';
import 'colors.dart';
import 'theme.dart';
// Margin on top of the list section. This was eyeballed from iOS 14.4 Simulator
// and should be always present on top of the edge-to-edge variant.
const double _kMarginTop = 22.0;
// Standard header margin, determined from SwiftUI's Forms in iOS 14.2 SDK.
const EdgeInsetsDirectional _kDefaultHeaderMargin = EdgeInsetsDirectional.fromSTEB(20.0, 0.0, 20.0, 6.0);
// Header margin for inset grouped variant, determined from iOS 14.4 Simulator.
const EdgeInsetsDirectional _kInsetGroupedDefaultHeaderMargin = EdgeInsetsDirectional.fromSTEB(20.0, 16.0, 20.0, 6.0);
// Standard footer margin, determined from SwiftUI's Forms in iOS 14.2 SDK.
const EdgeInsetsDirectional _kDefaultFooterMargin = EdgeInsetsDirectional.fromSTEB(20.0, 0.0, 20.0, 0.0);
// Footer margin for inset grouped variant, determined from iOS 14.4 Simulator.
const EdgeInsetsDirectional _kInsetGroupedDefaultFooterMargin = EdgeInsetsDirectional.fromSTEB(20.0, 0.0, 20.0, 10.0);
// Margin around children in edge-to-edge variant, determined from iOS 14.4
// Simulator.
const EdgeInsets _kDefaultRowsMargin = EdgeInsets.only(bottom: 8.0);
// Used for iOS "Inset Grouped" margin, determined from SwiftUI's Forms in
// iOS 14.2 SDK.
const EdgeInsetsDirectional _kDefaultInsetGroupedRowsMargin = EdgeInsetsDirectional.fromSTEB(20.0, 20.0, 20.0, 10.0);
// Used for iOS "Inset Grouped" margin, determined from SwiftUI's Forms in
// iOS 14.2 SDK.
const EdgeInsetsDirectional _kDefaultInsetGroupedRowsMarginWithHeader = EdgeInsetsDirectional.fromSTEB(20.0, 0.0, 20.0, 10.0);
// Used for iOS "Inset Grouped" border radius, estimated from SwiftUI's Forms in
// iOS 14.2 SDK.
// TODO(edrisian): This should be a rounded rectangle once that shape is added.
const BorderRadius _kDefaultInsetGroupedBorderRadius = BorderRadius.all(Radius.circular(10.0));
// The margin of divider used in base list section. Estimated from iOS 14.4 SDK
// Settings app.
const double _kBaseDividerMargin = 20.0;
// Additional margin of divider used in base list section with list tiles with
// leading widgets. Estimated from iOS 14.4 SDK Settings app.
const double _kBaseAdditionalDividerMargin = 44.0;
// The margin of divider used in inset grouped version of list section.
// Estimated from iOS 14.4 SDK Reminders app.
const double _kInsetDividerMargin = 14.0;
// Additional margin of divider used in inset grouped version of list section.
// Estimated from iOS 14.4 SDK Reminders app.
const double _kInsetAdditionalDividerMargin = 42.0;
// Additional margin of divider used in inset grouped version of list section
// when there is no leading widgets. Estimated from iOS 14.4 SDK Notes app.
const double _kInsetAdditionalDividerMarginWithoutLeading = 14.0;
// Color of header and footer text in edge-to-edge variant.
const Color _kHeaderFooterColor = CupertinoDynamicColor(
color: Color.fromRGBO(108, 108, 108, 1.0),
darkColor: Color.fromRGBO(142, 142, 146, 1.0),
highContrastColor: Color.fromRGBO(74, 74, 77, 1.0),
darkHighContrastColor: Color.fromRGBO(176, 176, 183, 1.0),
elevatedColor: Color.fromRGBO(108, 108, 108, 1.0),
darkElevatedColor: Color.fromRGBO(142, 142, 146, 1.0),
highContrastElevatedColor: Color.fromRGBO(108, 108, 108, 1.0),
darkHighContrastElevatedColor: Color.fromRGBO(142, 142, 146, 1.0),
);
/// Denotes what type of the list section a [CupertinoListSection] is.
///
/// This is for internal use only.
enum CupertinoListSectionType {
/// A basic form of [CupertinoListSection].
base,
/// An inset-grouped style of [CupertinoListSection].
insetGrouped,
}
/// An iOS-style list section.
///
/// The [CupertinoListSection] is a container for children widgets. These are
/// most often [CupertinoListTile]s.
///
/// The base constructor for [CupertinoListSection] constructs an
/// edge-to-edge style section which includes an iOS-style header, the dividers
/// between rows, and borders on top and bottom of the rows. An example of such
/// list section are sections in iOS Settings app.
///
/// The [CupertinoListSection.insetGrouped] constructor creates a round-edged
/// and padded section that is seen in iOS Notes and Reminders apps. It creates
/// an iOS-style header, and the dividers between rows. Does not create borders
/// on top and bottom of the rows.
///
/// The section [header] lies above the [children] rows, with margins and style
/// that match the iOS style.
///
/// The section [footer] lies below the [children] rows and is used to provide
/// additional information for current list section.
///
/// The [children] is the list of widgets to be displayed in this list section.
/// Typically, the children are of type [CupertinoListTile], however these is
/// not enforced.
///
/// The [margin] is used to provide spacing around the content area of the
/// section encapsulating [children].
///
/// The [decoration] of [children] specifies how they should be decorated. If it
/// is not provided in constructor, the background color of [children] defaults
/// to [CupertinoColors.secondarySystemGroupedBackground] and border radius of
/// children group defaults to 10.0 circular radius when constructing with
/// [CupertinoListSection.insetGrouped]. Defaults to zero radius for the
/// standard [CupertinoListSection] constructor.
///
/// The [dividerMargin] and [additionalDividerMargin] specify the starting
/// margin of the divider between list tiles. The [dividerMargin] is always
/// present, but [additionalDividerMargin] is only added to the [dividerMargin]
/// if `hasLeading` is set to true in the constructor, which is the default
/// value.
///
/// The [backgroundColor] of the section defaults to
/// [CupertinoColors.systemGroupedBackground].
///
/// {@macro flutter.material.Material.clipBehavior}
///
/// {@tool dartpad}
/// Creates a base [CupertinoListSection] containing [CupertinoListTile]s with
/// `leading`, `title`, `additionalInfo` and `trailing` widgets.
///
/// ** See code in examples/api/lib/cupertino/list_section/list_section_base.0.dart **
/// {@end-tool}
///
/// {@tool dartpad}
/// Creates an "Inset Grouped" [CupertinoListSection] containing
/// notched [CupertinoListTile]s with `leading`, `title`, `additionalInfo` and
/// `trailing` widgets.
///
/// ** See code in examples/api/lib/cupertino/list_section/list_section_inset.0.dart **
/// {@end-tool}
///
/// See also:
///
/// * [CupertinoListTile], an iOS-style list tile, a typical child of
/// [CupertinoListSection].
/// * [CupertinoFormSection], an iOS-style form section.
class CupertinoListSection extends StatelessWidget {
/// Creates a section that mimics standard iOS forms.
///
/// The base constructor for [CupertinoListSection] constructs an
/// edge-to-edge style section which includes an iOS-style header, the dividers
/// between rows, and borders on top and bottom of the rows. An example of such
/// list section are sections in iOS Settings app.
///
/// The [header] parameter sets the form section header. The section header
/// lies above the [children] rows, with margins that match the iOS style.
///
/// The [footer] parameter sets the form section footer. The section footer
/// lies below the [children] rows.
///
/// The [children] parameter is required and sets the list of rows shown in
/// the section. The [children] parameter takes a list, as opposed to a more
/// efficient builder function that lazy builds, because forms are intended to
/// be short in row count. It is recommended that only [CupertinoFormRow] and
/// [CupertinoTextFormFieldRow] widgets be included in the [children] list in
/// order to retain the iOS look.
///
/// The [margin] parameter sets the spacing around the content area of the
/// section encapsulating [children], and defaults to zero padding.
///
/// The [decoration] parameter sets the decoration around [children].
/// If null, defaults to [CupertinoColors.secondarySystemGroupedBackground].
/// If null, defaults to 10.0 circular radius when constructing with
/// [CupertinoListSection.insetGrouped]. Defaults to zero radius for the
/// standard [CupertinoListSection] constructor.
///
/// The [backgroundColor] parameter sets the background color behind the
/// section. If null, defaults to [CupertinoColors.systemGroupedBackground].
///
/// The [dividerMargin] parameter sets the starting offset of the divider
/// between rows.
///
/// The [additionalDividerMargin] parameter adds additional margin to existing
/// [dividerMargin] when [hasLeading] is set to true. By default, it offsets
/// for the width of leading and space between leading and title of
/// [CupertinoListTile], but it can be overwritten for custom look.
///
/// The [hasLeading] parameter specifies whether children [CupertinoListTile]
/// widgets contain leading or not. Used for calculating correct starting
/// margin for the divider between rows.
///
/// The [topMargin] is used to specify the margin above the list section. It
/// matches the iOS look by default.
///
/// {@macro flutter.material.Material.clipBehavior}
const CupertinoListSection({
super.key,
this.children,
this.header,
this.footer,
this.margin = _kDefaultRowsMargin,
this.backgroundColor = CupertinoColors.systemGroupedBackground,
this.decoration,
this.clipBehavior = Clip.none,
this.dividerMargin = _kBaseDividerMargin,
double? additionalDividerMargin,
this.topMargin = _kMarginTop,
bool hasLeading = true,
}) : assert((children != null && children.length > 0) || header != null),
type = CupertinoListSectionType.base,
additionalDividerMargin = additionalDividerMargin ??
(hasLeading ? _kBaseAdditionalDividerMargin : 0.0);
/// Creates a section that mimics standard "Inset Grouped" iOS list section.
///
/// The [CupertinoListSection.insetGrouped] constructor creates a round-edged
/// and padded section that is seen in iOS Notes and Reminders apps. It creates
/// an iOS-style header, and the dividers between rows. Does not create borders
/// on top and bottom of the rows.
///
/// The [header] parameter sets the form section header. The section header
/// lies above the [children] rows, with margins that match the iOS style.
///
/// The [footer] parameter sets the form section footer. The section footer
/// lies below the [children] rows.
///
/// The [children] parameter is required and sets the list of rows shown in
/// the section. The [children] parameter takes a list, as opposed to a more
/// efficient builder function that lazy builds, because forms are intended to
/// be short in row count. It is recommended that only [CupertinoListTile]
/// widget be included in the [children] list in order to retain the iOS look.
///
/// The [margin] parameter sets the spacing around the content area of the
/// section encapsulating [children], and defaults to the standard
/// notched-style iOS form padding.
///
/// The [decoration] parameter sets the decoration around [children].
/// If null, defaults to [CupertinoColors.secondarySystemGroupedBackground].
/// If null, defaults to 10.0 circular radius when constructing with
/// [CupertinoListSection.insetGrouped]. Defaults to zero radius for the
/// standard [CupertinoListSection] constructor.
///
/// The [backgroundColor] parameter sets the background color behind the
/// section. If null, defaults to [CupertinoColors.systemGroupedBackground].
///
/// The [dividerMargin] parameter sets the starting offset of the divider
/// between rows.
///
/// The [additionalDividerMargin] parameter adds additional margin to existing
/// [dividerMargin] when [hasLeading] is set to true. By default, it offsets
/// for the width of leading and space between leading and title of
/// [CupertinoListTile], but it can be overwritten for custom look.
///
/// The [hasLeading] parameter specifies whether children [CupertinoListTile]
/// widgets contain leading or not. Used for calculating correct starting
/// margin for the divider between rows.
///
/// {@macro flutter.material.Material.clipBehavior}
const CupertinoListSection.insetGrouped({
super.key,
this.children,
this.header,
this.footer,
EdgeInsetsGeometry? margin,
this.backgroundColor = CupertinoColors.systemGroupedBackground,
this.decoration,
this.clipBehavior = Clip.hardEdge,
this.dividerMargin = _kInsetDividerMargin,
double? additionalDividerMargin,
this.topMargin,
bool hasLeading = true,
}) : assert((children != null && children.length > 0) || header != null),
type = CupertinoListSectionType.insetGrouped,
additionalDividerMargin = additionalDividerMargin ??
(hasLeading
? _kInsetAdditionalDividerMargin
: _kInsetAdditionalDividerMarginWithoutLeading),
margin = margin ?? (header == null ? _kDefaultInsetGroupedRowsMargin : _kDefaultInsetGroupedRowsMarginWithHeader);
/// The type of list section, either base or inset grouped.
///
/// This member is public for testing purposes only and cannot be set
/// manually. Instead, use a corresponding constructors.
@visibleForTesting
final CupertinoListSectionType type;
/// Sets the form section header. The section header lies above the [children]
/// rows. Usually a [Text] widget.
final Widget? header;
/// Sets the form section footer. The section footer lies below the [children]
/// rows. Usually a [Text] widget.
final Widget? footer;
/// Margin around the content area of the section encapsulating [children].
///
/// Defaults to zero padding if constructed with standard
/// [CupertinoListSection] constructor. Defaults to the standard notched-style
/// iOS margin when constructing with [CupertinoListSection.insetGrouped].
final EdgeInsetsGeometry margin;
/// The list of rows in the section. Usually a list of [CupertinoListTile]s.
///
/// This takes a list, as opposed to a more efficient builder function that
/// lazy builds, because such lists are intended to be short in row count.
/// It is recommended that only [CupertinoListTile] widget be included in the
/// [children] list in order to retain the iOS look.
final List<Widget>? children;
/// Sets the decoration around [children].
///
/// If null, background color defaults to
/// [CupertinoColors.secondarySystemGroupedBackground].
///
/// If null, border radius defaults to 10.0 circular radius when constructing
/// with [CupertinoListSection.insetGrouped]. Defaults to zero radius for the
/// standard [CupertinoListSection] constructor.
final BoxDecoration? decoration;
/// Sets the background color behind the section.
///
/// Defaults to [CupertinoColors.systemGroupedBackground].
final Color backgroundColor;
/// {@macro flutter.material.Material.clipBehavior}
///
/// Defaults to [Clip.hardEdge].
final Clip clipBehavior;
/// The starting offset of a margin between two list tiles.
final double dividerMargin;
/// Additional starting inset of the divider used between rows. This is used
/// when adding a leading icon to children and a divider should start at the
/// text inset instead of the icon.
final double additionalDividerMargin;
/// Margin above the list section. Only used in edge-to-edge variant and it
/// matches iOS style by default.
final double? topMargin;
@override
Widget build(BuildContext context) {
final Color dividerColor = CupertinoColors.separator.resolveFrom(context);
final double dividerHeight = 1.0 / MediaQuery.of(context).devicePixelRatio;
// Long divider is used for wrapping the top and bottom of rows.
// Only used in CupertinoListSectionType.base mode.
final Widget longDivider = Container(
color: dividerColor,
height: dividerHeight,
);
// Short divider is used between rows.
final Widget shortDivider = Container(
margin: EdgeInsetsDirectional.only(
start: dividerMargin + additionalDividerMargin),
color: dividerColor,
height: dividerHeight,
);
Widget? headerWidget;
if (header != null) {
headerWidget = DefaultTextStyle(
style: CupertinoTheme.of(context).textTheme.textStyle.merge(
type == CupertinoListSectionType.base
? TextStyle(
fontSize: 13.0,
color: CupertinoDynamicColor.resolve(
_kHeaderFooterColor, context))
: const TextStyle(
fontSize: 20.0, fontWeight: FontWeight.bold),
),
child: header!,
);
}
Widget? footerWidget;
if (footer != null) {
footerWidget = DefaultTextStyle(
style: type == CupertinoListSectionType.base
? CupertinoTheme.of(context).textTheme.textStyle.merge(TextStyle(
fontSize: 13.0,
color: CupertinoDynamicColor.resolve(
_kHeaderFooterColor, context),
))
: CupertinoTheme.of(context).textTheme.textStyle,
child: footer!,
);
}
BorderRadius? childrenGroupBorderRadius;
DecoratedBox? decoratedChildrenGroup;
if (children != null && children!.isNotEmpty) {
// We construct childrenWithDividers as follows:
// Insert a short divider between all rows.
// If it is a `CupertinoListSectionType.base` type, add a long divider
// to the top and bottom of the rows.
final List<Widget> childrenWithDividers = <Widget>[];
if (type == CupertinoListSectionType.base) {
childrenWithDividers.add(longDivider);
}
children!.sublist(0, children!.length - 1).forEach((Widget widget) {
childrenWithDividers.add(widget);
childrenWithDividers.add(shortDivider);
});
childrenWithDividers.add(children!.last);
if (type == CupertinoListSectionType.base) {
childrenWithDividers.add(longDivider);
}
switch (type) {
case CupertinoListSectionType.insetGrouped:
childrenGroupBorderRadius = _kDefaultInsetGroupedBorderRadius;
break;
case CupertinoListSectionType.base:
childrenGroupBorderRadius = BorderRadius.zero;
break;
}
// Refactored the decorate children group in one place to avoid repeating it
// twice down bellow in the returned widget.
decoratedChildrenGroup = DecoratedBox(
decoration: decoration ??
BoxDecoration(
color: CupertinoDynamicColor.resolve(
decoration?.color ??
CupertinoColors.secondarySystemGroupedBackground,
context),
borderRadius: childrenGroupBorderRadius,
),
child: Column(children: childrenWithDividers),
);
}
return DecoratedBox(
decoration: BoxDecoration(
color: CupertinoDynamicColor.resolve(backgroundColor, context)),
child: Column(
children: <Widget>[
if (type == CupertinoListSectionType.base)
SizedBox(height: topMargin),
if (headerWidget != null)
Align(
alignment: AlignmentDirectional.centerStart,
child: Padding(
padding: type == CupertinoListSectionType.base
? _kDefaultHeaderMargin
: _kInsetGroupedDefaultHeaderMargin,
child: headerWidget,
),
),
if (children != null && children!.isNotEmpty)
Padding(
padding: margin,
child: clipBehavior == Clip.none
? decoratedChildrenGroup
: ClipRRect(
borderRadius: childrenGroupBorderRadius,
clipBehavior: clipBehavior,
child: decoratedChildrenGroup,
),
),
if (footerWidget != null)
Align(
alignment: AlignmentDirectional.centerStart,
child: Padding(
padding: type == CupertinoListSectionType.base
? _kDefaultFooterMargin
: _kInsetGroupedDefaultFooterMargin,
child: footerWidget,
),
),
],
),
);
}
}