// 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 'dart:async'; import 'package:flutter/widgets.dart'; import 'colors.dart'; import 'icons.dart'; import 'theme.dart'; // These constants were eyeballed from iOS 14.4 Settings app for base, Notes for // notched without leading, and Reminders app for notched with leading. const double _kLeadingSize = 28.0; const double _kNotchedLeadingSize = 30.0; const double _kMinHeight = _kLeadingSize + 2 * 8.0; const double _kMinHeightWithSubtitle = _kLeadingSize + 2 * 10.0; const double _kNotchedMinHeight = _kNotchedLeadingSize + 2 * 12.0; const double _kNotchedMinHeightWithoutLeading = _kNotchedLeadingSize + 2 * 10.0; const EdgeInsetsDirectional _kPadding = EdgeInsetsDirectional.only(start: 20.0, end: 14.0); const EdgeInsetsDirectional _kPaddingWithSubtitle = EdgeInsetsDirectional.only(start: 20.0, end: 14.0); const EdgeInsets _kNotchedPadding = EdgeInsets.symmetric(horizontal: 14.0); const EdgeInsetsDirectional _kNotchedPaddingWithoutLeading = EdgeInsetsDirectional.fromSTEB(28.0, 10.0, 14.0, 10.0); const double _kLeadingToTitle = 16.0; const double _kNotchedLeadingToTitle = 12.0; const double _kNotchedTitleToSubtitle = 3.0; const double _kAdditionalInfoToTrailing = 6.0; const double _kNotchedTitleWithSubtitleFontSize = 16.0; const double _kSubtitleFontSize = 12.0; const double _kNotchedSubtitleFontSize = 14.0; enum _CupertinoListTileType { base, notched } /// An iOS-style list tile. /// /// The [CupertinoListTile] is a Cupertino equivalent of Material [ListTile]. /// It comes in two forms, an old-fashioned edge-to-edge variant known from iOS /// Settings app and in a new, "Inset Grouped" form, known from either iOS Notes /// or Reminders app. The first is constructed using default constructor, and /// the latter using named constructor [CupertinoListTile.notched]. /// /// The [title], [subtitle], and [additionalInfo] are usually [Text] widgets. /// They are all limited to one line so it is a responsibility of the caller to /// take care of text wrapping. /// /// The size of [leading] is by default constrained to match the iOS size, /// depending of the type of list tile. This can however be overridden by /// providing [leadingSize]. The [trailing] widget is not constrained and is /// therefore a responsibility of the caller to ensure reasonable size of the /// [trailing] widget. /// /// The background color of the tile can be set with [backgroundColor] for the /// state before tile was tapped and with [backgroundColorActivated] for the /// state after the tile was tapped. By default, both values are set to match /// the default iOS appearance. /// /// The [padding] and [leadingToTitle] are by default set to match iOS but can /// be overwritten if necessary. /// /// The [onTap] callback provides an option to react to taps anywhere inside the /// list tile. This can be used to navigate routes and according to iOS /// behavior it should not be used for example to toggle the [CupertinoSwitch] /// in the trailing widget. /// /// See also: /// /// * [CupertinoListSection], an iOS-style list that is a typical container for /// [CupertinoListTile]. /// * [ListTile], a Material Design list tile. class CupertinoListTile extends StatefulWidget { /// Creates an edge-to-edge iOS-style list tile like the tiles in iOS Settings /// app. /// /// The [title] parameter is required. It is used to convey the most important /// information of list tile. It is typically a [Text]. /// /// The [subtitle] parameter is used to display additional information. It is /// placed below the [title]. /// /// The [additionalInfo] parameter is used to display additional information. /// It is placed at the end of the tile, before the [trailing] if supplied. /// /// The [leading] parameter is typically an [Icon] or an [Image] and it comes /// at the start of the tile. If omitted in all list tiles, a `hasLeading` of /// enclosing [CupertinoListSection] should be set to `false` to ensure /// correct margin of divider between tiles. /// /// The [trailing] parameter is typically a [CupertinoListTileChevron], an /// [Icon], or a [CupertinoButton]. It is placed at the very end of the tile. /// /// The [onTap] parameter is used to provide an action that is called when the /// tile is tapped. It is mainly used for navigating to a new route. It should /// not be used to toggle a trailing [CupertinoSwitch] and similar use cases /// because when tile is tapped, it switches the background color and remains /// changed. This is according to iOS behavior. /// /// The [backgroundColor] provides a custom background color for the tile in /// a state before tapped. By default, it matches the theme's background color /// which is by default a [CupertinoColors.systemBackground]. /// /// The [backgroundColorActivated] provides a custom background color for the /// tile after it was tapped. By default, it matches the theme's background /// color which is by default a [CupertinoColors.systemGrey4]. /// /// The [padding] parameter sets the padding of the content inside the tile. /// It defaults to a value that matches the iOS look, depending on a type of /// [CupertinoListTile]. For native look, it should not be provided. /// /// The [leadingSize] constrains the width and height of the leading widget. /// By default, it is set to a value that matches the iOS look, depending on a /// type of [CupertinoListTile]. For native look, it should not be provided. /// /// The [leadingToTitle] specifies the horizontal space between [leading] and /// [title] widgets. By default, it is set to a value that matched the iOS /// look, depending on a type of [CupertinoListTile]. For native look, it /// should not be provided. const CupertinoListTile({ super.key, required this.title, this.subtitle, this.additionalInfo, this.leading, this.trailing, this.onTap, this.backgroundColor, this.backgroundColorActivated, this.padding, this.leadingSize = _kLeadingSize, this.leadingToTitle = _kLeadingToTitle, }) : _type = _CupertinoListTileType.base; /// Creates a notched iOS-style list tile like the tiles in iOS Notes app or /// Reminders app. /// /// The [title] parameter is required. It is used to convey the most important /// information of list tile. It is typically a [Text]. /// /// The [subtitle] parameter is used to display additional information. It is /// placed below the [title]. /// /// The [additionalInfo] parameter is used to display additional information. /// It is placed at the end of the tile, before the [trailing] if supplied. /// /// The [leading] parameter is typically an [Icon] or an [Image] and it comes /// at the start of the tile. If omitted in all list tiles, a `hasLeading` of /// enclosing [CupertinoListSection] should be set to `false` to ensure /// correct margin of divider between tiles. For Notes-like tile appearance, /// the [leading] can be left `null`. /// /// The [trailing] parameter is typically a [CupertinoListTileChevron], an /// [Icon], or a [CupertinoButton]. It is placed at the very end of the tile. /// For Notes-like tile appearance, the [trailing] can be left `null`. /// /// The [onTap] parameter is used to provide an action that is called when the /// tile is tapped. It is mainly used for navigating to a new route. It should /// not be used to toggle a trailing [CupertinoSwitch] and similar use cases /// because when tile is tapped, it switches the background color and remains /// changed. This is according to iOS behavior. /// /// The [backgroundColor] provides a custom background color for the tile in /// a state before tapped. By default, it matches the theme's background color /// which is by default a [CupertinoColors.systemBackground]. /// /// The [backgroundColorActivated] provides a custom background color for the /// tile after it was tapped. By default, it matches the theme's background /// color which is by default a [CupertinoColors.systemGrey4]. /// /// The [padding] parameter sets the padding of the content inside the tile. /// It defaults to a value that matches the iOS look, depending on a type of /// [CupertinoListTile]. For native look, it should not be provided. /// /// The [leadingSize] constrains the width and height of the leading widget. /// By default, it is set to a value that matches the iOS look, depending on a /// type of [CupertinoListTile]. For native look, it should not be provided. /// /// The [leadingToTitle] specifies the horizontal space between [leading] and /// [title] widgets. By default, it is set to a value that matched the iOS /// look, depending on a type of [CupertinoListTile]. For native look, it /// should not be provided. const CupertinoListTile.notched({ super.key, required this.title, this.subtitle, this.additionalInfo, this.leading, this.trailing, this.onTap, this.backgroundColor, this.backgroundColorActivated, this.padding, this.leadingSize = _kNotchedLeadingSize, this.leadingToTitle = _kNotchedLeadingToTitle, }) : _type = _CupertinoListTileType.notched; final _CupertinoListTileType _type; /// A [title] is used to convey the central information. Usually a [Text]. final Widget title; /// A [subtitle] is used to display additional information. It is located /// below [title]. Usually a [Text] widget. final Widget? subtitle; /// Similar to [subtitle], an [additionalInfo] is used to display additional /// information. However, instead of being displayed below [title], it is /// displayed on the right, before [trailing]. Usually a [Text] widget. final Widget? additionalInfo; /// A widget displayed at the start of the [CupertinoListTile]. This is /// typically an `Icon` or an `Image`. final Widget? leading; /// A widget displayed at the end of the [CupertinoListTile]. This is usually /// a right chevron icon (e.g. `CupertinoListTileChevron`), or an `Icon`. final Widget? trailing; /// The [onTap] function is called when a user taps on [CupertinoListTile]. If /// left `null`, the [CupertinoListTile] will not react on taps. If this is a /// `Future<void> Function()`, then the [CupertinoListTile] remains activated /// until the returned future is awaited. This is according to iOS behavior. /// However, if this function is a `void Function()`, then the tile is active /// only for the duration of invocation. final FutureOr<void> Function()? onTap; /// The [backgroundColor] of the tile in normal state. Once the tile is /// tapped, the background color switches to [backgroundColorActivated]. It is /// set to match the iOS look by default. final Color? backgroundColor; /// The [backgroundColorActivated] is the background color of the tile after /// the tile was tapped. It is set to match the iOS look by default. final Color? backgroundColorActivated; /// Padding of the content inside [CupertinoListTile]. final EdgeInsetsGeometry? padding; /// The [leadingSize] is used to constrain the width and height of [leading] /// widget. final double leadingSize; /// The horizontal space between [leading] widget and [title]. final double leadingToTitle; @override State<CupertinoListTile> createState() => _CupertinoListTileState(); } class _CupertinoListTileState extends State<CupertinoListTile> { bool _tapped = false; @override Widget build(BuildContext context) { final TextStyle titleTextStyle = widget._type == _CupertinoListTileType.base || widget.subtitle == null ? CupertinoTheme.of(context).textTheme.textStyle : CupertinoTheme.of(context).textTheme.textStyle.merge( TextStyle( fontWeight: FontWeight.w600, fontSize: widget.leading == null ? _kNotchedTitleWithSubtitleFontSize : null, ), ); final TextStyle subtitleTextStyle = widget._type == _CupertinoListTileType.base ? CupertinoTheme.of(context).textTheme.textStyle.merge( TextStyle( fontSize: _kSubtitleFontSize, color: CupertinoColors.secondaryLabel.resolveFrom(context), ), ) : CupertinoTheme.of(context).textTheme.textStyle.merge( TextStyle( fontSize: _kNotchedSubtitleFontSize, color: CupertinoColors.secondaryLabel.resolveFrom(context), ), ); final TextStyle? additionalInfoTextStyle = widget.additionalInfo != null ? CupertinoTheme.of(context).textTheme.textStyle.merge( TextStyle(color: CupertinoColors.secondaryLabel.resolveFrom(context))) : null; final Widget title = DefaultTextStyle( style: titleTextStyle, maxLines: 1, overflow: TextOverflow.ellipsis, child: widget.title, ); EdgeInsetsGeometry? padding = widget.padding; if (padding == null) { switch (widget._type) { case _CupertinoListTileType.base: padding = widget.subtitle == null ? _kPadding : _kPaddingWithSubtitle; case _CupertinoListTileType.notched: padding = widget.leading == null ? _kNotchedPaddingWithoutLeading : _kNotchedPadding; } } Widget? subtitle; if (widget.subtitle != null) { subtitle = DefaultTextStyle( style: subtitleTextStyle, maxLines: 1, overflow: TextOverflow.ellipsis, child: widget.subtitle!, ); } Widget? additionalInfo; if (widget.additionalInfo != null) { additionalInfo = DefaultTextStyle( style: additionalInfoTextStyle!, maxLines: 1, child: widget.additionalInfo!, ); } // The color for default state tile is set to either what user provided or // null and it will resolve to the correct color provided by context. But if // the tile was tapped, it is set to what user provided or if null to the // default color that matched the iOS-style. Color? backgroundColor = widget.backgroundColor; if (_tapped) { backgroundColor = widget.backgroundColorActivated ?? CupertinoColors.systemGrey4.resolveFrom(context); } double minHeight; switch (widget._type) { case _CupertinoListTileType.base: minHeight = subtitle == null ? _kMinHeight : _kMinHeightWithSubtitle; case _CupertinoListTileType.notched: minHeight = widget.leading == null ? _kNotchedMinHeightWithoutLeading : _kNotchedMinHeight; } final Widget child = Container( constraints: BoxConstraints(minWidth: double.infinity, minHeight: minHeight), color: backgroundColor, child: Padding( padding: padding, child: Row( children: <Widget>[ if (widget.leading != null) ...<Widget>[ SizedBox( width: widget.leadingSize, height: widget.leadingSize, child: Center( child: widget.leading, ), ), SizedBox(width: widget.leadingToTitle), ] else SizedBox(height: widget.leadingSize), Expanded( child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ title, if (subtitle != null) ...<Widget>[ const SizedBox(height: _kNotchedTitleToSubtitle), subtitle, ], ], ), ), if (additionalInfo != null) ...<Widget>[ additionalInfo, if (widget.trailing != null) const SizedBox(width: _kAdditionalInfoToTrailing), ], if (widget.trailing != null) widget.trailing! ], ), ), ); if (widget.onTap == null) { return child; } return GestureDetector( onTapDown: (_) => setState(() { _tapped = true; }), onTapCancel: () => setState(() { _tapped = false; }), onTap: () async { await widget.onTap!(); if (mounted) { setState(() { _tapped = false; }); } }, behavior: HitTestBehavior.opaque, child: child, ); } } /// A typical iOS trailing widget used to denote that a `CupertinoListTile` is a /// button with an action. /// /// The [CupertinoListTileChevron] is meant as a convenience implementation of /// trailing right chevron. class CupertinoListTileChevron extends StatelessWidget { /// Creates a typical widget used to denote that a `CupertinoListTile` is a /// button with action. const CupertinoListTileChevron({super.key}); @override Widget build(BuildContext context) { return Icon( CupertinoIcons.right_chevron, size: CupertinoTheme.of(context).textTheme.textStyle.fontSize, color: CupertinoColors.systemGrey2.resolveFrom(context), ); } }