Files
PiliPlus/lib/common/widgets/list_tile.dart
bggRGjQaUbCoE 8d94c0405f opt ui
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-03 13:09:08 +08:00

1858 lines
60 KiB
Dart

// 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.
// ignore_for_file: uri_does_not_exist_in_doc_import
/// @docImport 'card.dart';
/// @docImport 'checkbox.dart';
/// @docImport 'checkbox_list_tile.dart';
/// @docImport 'circle_avatar.dart';
/// @docImport 'drawer.dart';
/// @docImport 'expansion_tile.dart';
/// @docImport 'material.dart';
/// @docImport 'radio.dart';
/// @docImport 'radio_list_tile.dart';
/// @docImport 'scaffold.dart';
/// @docImport 'switch.dart';
/// @docImport 'switch_list_tile.dart';
library;
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
// Examples can assume:
// int _act = 1;
typedef _Sizes = ({
double titleY,
BoxConstraints textConstraints,
Size tileSize,
});
typedef _PositionChild = void Function(RenderBox child, Offset offset);
/// Defines how [ListTile.leading] and [ListTile.trailing] are
/// vertically aligned relative to the [ListTile]'s titles
/// ([ListTile.title] and [ListTile.subtitle]).
///
/// See also:
///
/// * [ListTile.titleAlignment], to configure the title alignment for an
/// individual [ListTile].
/// * [ListTileThemeData.titleAlignment], to configure the title alignment
/// for all of the [ListTile]s under a [ListTileTheme].
/// * [ThemeData.listTileTheme], to configure the [ListTileTheme]
/// for an entire app.
extension on ListTileTitleAlignment {
// If isLeading is true the y offset is for the leading widget, otherwise it's
// for the trailing child.
double _yOffsetFor(
double childHeight,
double tileHeight,
_RenderListTile listTile,
bool isLeading,
) {
return switch (this) {
ListTileTitleAlignment.threeLine =>
listTile.isThreeLine
? ListTileTitleAlignment.top._yOffsetFor(
childHeight,
tileHeight,
listTile,
isLeading,
)
: ListTileTitleAlignment.center._yOffsetFor(
childHeight,
tileHeight,
listTile,
isLeading,
),
// This attempts to implement the redlines for the vertical position of the
// leading and trailing icons on the spec page:
// https://m2.material.io/components/lists#specs
//
// For large tiles (> 72dp), both leading and trailing controls should be
// a fixed distance from top. As per guidelines this is set to 16dp.
ListTileTitleAlignment.titleHeight when tileHeight > 72.0 => 16.0,
// For smaller tiles, trailing should always be centered. Leading can be
// centered or closer to the top. It should never be further than 16dp
// to the top.
ListTileTitleAlignment.titleHeight =>
isLeading
? math.min((tileHeight - childHeight) / 2.0, 16.0)
: (tileHeight - childHeight) / 2.0,
ListTileTitleAlignment.top => listTile.minVerticalPadding,
ListTileTitleAlignment.center => (tileHeight - childHeight) / 2.0,
ListTileTitleAlignment.bottom =>
tileHeight - childHeight - listTile.minVerticalPadding,
};
}
}
/// A single fixed-height row that typically contains some text as well as
/// a leading or trailing icon.
///
/// {@youtube 560 315 https://www.youtube.com/watch?v=l8dj0yPBvgQ}
///
/// A list tile contains one to three lines of text optionally flanked by icons or
/// other widgets, such as check boxes. The icons (or other widgets) for the
/// tile are defined with the [leading] and [trailing] parameters. The first
/// line of text is not optional and is specified with [title]. The value of
/// [subtitle], which _is_ optional, will occupy the space allocated for an
/// additional line of text, or two lines if [isThreeLine] is true. If [dense]
/// is true then the overall height of this tile and the size of the
/// [DefaultTextStyle]s that wrap the [title] and [subtitle] widget are reduced.
///
/// It is the responsibility of the caller to ensure that [title] does not wrap,
/// and to ensure that [subtitle] doesn't wrap (if [isThreeLine] is false) or
/// wraps to two lines (if it is true).
///
/// The heights of the [leading] and [trailing] widgets are constrained
/// according to the
/// [Material spec](https://material.io/design/components/lists.html).
/// An exception is made for one-line ListTiles for accessibility. Please
/// see the example below to see how to adhere to both Material spec and
/// accessibility requirements.
///
/// The [leading] and [trailing] widgets can expand as far as they wish
/// horizontally, so ensure that they are properly constrained.
///
/// List tiles are typically used in [ListView]s, or arranged in [Column]s in
/// [Drawer]s and [Card]s.
///
/// This widget requires a [Material] widget ancestor in the tree to paint
/// itself on, which is typically provided by the app's [Scaffold].
/// The [tileColor], [selectedTileColor], [focusColor], and [hoverColor]
/// are not painted by the [ListTile] itself but by the [Material] widget
/// ancestor. In this case, one can wrap a [Material] widget around the
/// [ListTile], e.g.:
///
/// {@tool snippet}
/// ```dart
/// const ColoredBox(
/// color: Colors.green,
/// child: Material(
/// child: ListTile(
/// title: Text('ListTile with red background'),
/// tileColor: Colors.red,
/// ),
/// ),
/// )
/// ```
/// {@end-tool}
///
/// ## Performance considerations when wrapping [ListTile] with [Material]
///
/// Wrapping a large number of [ListTile]s individually with [Material]s
/// is expensive. Consider only wrapping the [ListTile]s that require it
/// or include a common [Material] ancestor where possible.
///
/// [ListTile] must be wrapped in a [Material] widget to animate [tileColor],
/// [selectedTileColor], [focusColor], and [hoverColor] as these colors
/// are not drawn by the list tile itself but by the material widget ancestor.
///
/// {@tool dartpad}
/// This example showcases how [ListTile] needs to be wrapped in a [Material]
/// widget to animate colors.
///
/// ** See code in examples/api/lib/material/list_tile/list_tile.0.dart **
/// {@end-tool}
///
/// {@tool dartpad}
/// This example uses a [ListView] to demonstrate different configurations of
/// [ListTile]s in [Card]s.
///
/// ![Different variations of ListTile](https://flutter.github.io/assets-for-api-docs/assets/material/list_tile.png)
///
/// ** See code in examples/api/lib/material/list_tile/list_tile.1.dart **
/// {@end-tool}
///
/// {@tool dartpad}
/// This sample shows the creation of a [ListTile] using [ThemeData.useMaterial3] flag,
/// as described in: https://m3.material.io/components/lists/overview.
///
/// ** See code in examples/api/lib/material/list_tile/list_tile.2.dart **
/// {@end-tool}
///
/// {@tool dartpad}
/// This sample shows [ListTile]'s [textColor] and [iconColor] can use
/// [WidgetStateColor] color to change the color of the text and icon
/// when the [ListTile] is enabled, selected, or disabled.
///
/// ** See code in examples/api/lib/material/list_tile/list_tile.3.dart **
/// {@end-tool}
///
/// {@tool dartpad}
/// This sample shows [ListTile.titleAlignment] can be used to configure the
/// [leading] and [trailing] widgets alignment relative to the [title] and
/// [subtitle] widgets.
///
/// ** See code in examples/api/lib/material/list_tile/list_tile.4.dart **
/// {@end-tool}
///
/// {@tool snippet}
/// To use a [ListTile] within a [Row], it needs to be wrapped in an
/// [Expanded] widget. [ListTile] requires fixed width constraints,
/// whereas a [Row] does not constrain its children.
///
/// ```dart
/// const Row(
/// children: <Widget>[
/// Expanded(
/// child: ListTile(
/// leading: FlutterLogo(),
/// title: Text('These ListTiles are expanded '),
/// ),
/// ),
/// Expanded(
/// child: ListTile(
/// trailing: FlutterLogo(),
/// title: Text('to fill the available space.'),
/// ),
/// ),
/// ],
/// )
/// ```
/// {@end-tool}
/// {@tool snippet}
///
/// Tiles can be much more elaborate. Here is a tile which can be tapped, but
/// which is disabled when the `_act` variable is not 2. When the tile is
/// tapped, the whole row has an ink splash effect (see [InkWell]).
///
/// ```dart
/// ListTile(
/// leading: const Icon(Icons.flight_land),
/// title: const Text("Trix's airplane"),
/// subtitle: _act != 2 ? const Text('The airplane is only in Act II.') : null,
/// enabled: _act == 2,
/// onTap: () { /* react to the tile being tapped */ }
/// )
/// ```
/// {@end-tool}
///
/// To be accessible, tappable [leading] and [trailing] widgets have to
/// be at least 48x48 in size. However, to adhere to the Material spec,
/// [trailing] and [leading] widgets in one-line ListTiles should visually be
/// at most 32 ([dense]: true) or 40 ([dense]: false) in height, which may
/// conflict with the accessibility requirement.
///
/// For this reason, a one-line ListTile allows the height of [leading]
/// and [trailing] widgets to be constrained by the height of the ListTile.
/// This allows for the creation of tappable [leading] and [trailing] widgets
/// that are large enough, but it is up to the developer to ensure that
/// their widgets follow the Material spec.
///
/// {@tool snippet}
///
/// Here is an example of a one-line, non-[dense] ListTile with a
/// tappable leading widget that adheres to accessibility requirements and
/// the Material spec. To adjust the use case below for a one-line, [dense]
/// ListTile, adjust the vertical padding to 8.0.
///
/// ```dart
/// ListTile(
/// leading: GestureDetector(
/// behavior: HitTestBehavior.translucent,
/// onTap: () {},
/// child: Container(
/// width: 48,
/// height: 48,
/// padding: const EdgeInsets.symmetric(vertical: 4.0),
/// alignment: Alignment.center,
/// child: const CircleAvatar(),
/// ),
/// ),
/// title: const Text('title'),
/// dense: false,
/// )
/// ```
/// {@end-tool}
///
/// ## The ListTile layout isn't exactly what I want
///
/// If the way ListTile pads and positions its elements isn't quite what
/// you're looking for, it's easy to create custom list items with a
/// combination of other widgets, such as [Row]s and [Column]s.
///
/// {@tool dartpad}
/// Here is an example of a custom list item that resembles a YouTube-related
/// video list item created with [Expanded] and [Container] widgets.
///
/// ** See code in examples/api/lib/material/list_tile/custom_list_item.0.dart **
/// {@end-tool}
///
/// {@tool dartpad}
/// Here is an example of an article list item with multiline titles and
/// subtitles. It utilizes [Row]s and [Column]s, as well as [Expanded] and
/// [AspectRatio] widgets to organize its layout.
///
/// ** See code in examples/api/lib/material/list_tile/custom_list_item.1.dart **
/// {@end-tool}
///
/// See also:
///
/// * [ListTileTheme], which defines visual properties for [ListTile]s.
/// * [ListView], which can display an arbitrary number of [ListTile]s
/// in a scrolling list.
/// * [CircleAvatar], which shows an icon representing a person and is often
/// used as the [leading] element of a ListTile.
/// * [Card], which can be used with [Column] to show a few [ListTile]s.
/// * [Divider], which can be used to separate [ListTile]s.
/// * [ListTile.divideTiles], a utility for inserting [Divider]s in between [ListTile]s.
/// * [CheckboxListTile], [RadioListTile], and [SwitchListTile], widgets
/// that combine [ListTile] with other controls.
/// * Material 3 [ListTile] specifications are referenced from <https://m3.material.io/components/lists/specs>
/// and Material 2 [ListTile] specifications are referenced from <https://material.io/design/components/lists.html>
/// * Cookbook: [Use lists](https://docs.flutter.dev/cookbook/lists/basic-list)
/// * Cookbook: [Implement swipe to dismiss](https://docs.flutter.dev/cookbook/gestures/dismissible)
class ListTile extends StatelessWidget {
/// Creates a list tile.
///
/// If [isThreeLine] is true, then [subtitle] must not be null.
///
/// Requires one of its ancestors to be a [Material] widget.
const ListTile({
super.key,
this.leading,
this.title,
this.subtitle,
this.trailing,
this.isThreeLine,
this.dense,
this.visualDensity,
this.shape,
this.style,
this.selectedColor,
this.iconColor,
this.textColor,
this.titleTextStyle,
this.subtitleTextStyle,
this.leadingAndTrailingTextStyle,
this.contentPadding,
this.enabled = true,
this.onTap,
this.onLongPress,
this.onFocusChange,
this.mouseCursor,
this.selected = false,
this.focusColor,
this.hoverColor,
this.splashColor,
this.focusNode,
this.autofocus = false,
this.tileColor,
this.selectedTileColor,
this.enableFeedback,
this.horizontalTitleGap,
this.minVerticalPadding,
this.minLeadingWidth,
this.minTileHeight,
this.titleAlignment,
this.internalAddSemanticForOnTap = true,
this.statesController,
}) : assert(isThreeLine != true || subtitle != null);
/// A widget to display before the title.
///
/// Typically an [Icon] or a [CircleAvatar] widget.
final Widget? leading;
/// The primary content of the list tile.
///
/// Typically a [Text] widget.
///
/// This should not wrap. To enforce the single line limit, use
/// [Text.maxLines].
final Widget? title;
/// Additional content displayed below the title.
///
/// Typically a [Text] widget.
///
/// If [isThreeLine] is false, this should not wrap.
///
/// If [isThreeLine] is true, this should be configured to take a maximum of
/// two lines. For example, you can use [Text.maxLines] to enforce the number
/// of lines.
///
/// The subtitle's default [TextStyle] depends on [TextTheme.bodyMedium] except
/// [TextStyle.color]. The [TextStyle.color] depends on the value of [enabled]
/// and [selected].
///
/// When [enabled] is false, the text color is set to [ThemeData.disabledColor].
///
/// When [selected] is false, the text color is set to [ListTileTheme.textColor]
/// if it's not null and to [TextTheme.bodySmall]'s color if [ListTileTheme.textColor]
/// is null.
final Widget? subtitle;
/// A widget to display after the title.
///
/// Typically an [Icon] widget.
///
/// To show right-aligned metadata (assuming left-to-right reading order;
/// left-aligned for right-to-left reading order), consider using a [Row] with
/// [CrossAxisAlignment.baseline] alignment whose first item is [Expanded] and
/// whose second child is the metadata text, instead of using the [trailing]
/// property.
final Widget? trailing;
/// Whether this list tile is intended to display three lines of text.
///
/// If true, then [subtitle] must be non-null (since it is expected to give
/// the second and third lines of text).
///
/// If false, the list tile is treated as having one line if the subtitle is
/// null and treated as having two lines if the subtitle is non-null.
///
/// When using a [Text] widget for [title] and [subtitle], you can enforce
/// line limits using [Text.maxLines].
///
/// See also:
///
/// * [ListTileTheme.of], which returns the nearest [ListTileTheme]'s
/// [ListTileThemeData].
final bool? isThreeLine;
/// {@template flutter.material.ListTile.dense}
/// Whether this list tile is part of a vertically dense list.
///
/// If this property is null then its value is based on [ListTileTheme.dense].
///
/// Dense list tiles default to a smaller height.
///
/// It is not recommended to set [dense] to true when [ThemeData.useMaterial3] is true.
/// {@endtemplate}
final bool? dense;
/// Defines how compact the list tile's layout will be.
///
/// {@macro flutter.material.themedata.visualDensity}
///
/// See also:
///
/// * [ThemeData.visualDensity], which specifies the [visualDensity] for all
/// widgets within a [Theme].
final VisualDensity? visualDensity;
/// {@template flutter.material.ListTile.shape}
/// Defines the tile's [InkWell.customBorder] and [Ink.decoration] shape.
/// {@endtemplate}
///
/// If this property is null then [ListTileThemeData.shape] is used. If that
/// is also null then a rectangular [Border] will be used.
///
/// See also:
///
/// * [ListTileTheme.of], which returns the nearest [ListTileTheme]'s
/// [ListTileThemeData].
final ShapeBorder? shape;
/// Defines the color used for icons and text when the list tile is selected.
///
/// If this property is null then [ListTileThemeData.selectedColor]
/// is used. If that is also null then [ColorScheme.primary] is used.
///
/// See also:
///
/// * [ListTileTheme.of], which returns the nearest [ListTileTheme]'s
/// [ListTileThemeData].
final Color? selectedColor;
/// Defines the default color for [leading] and [trailing] icons.
///
/// If this property is null and [selected] is false then [ListTileThemeData.iconColor]
/// is used. If that is also null and [ThemeData.useMaterial3] is true, [ColorScheme.onSurfaceVariant]
/// is used, otherwise if [ThemeData.brightness] is [Brightness.light], [Colors.black54] is used,
/// and if [ThemeData.brightness] is [Brightness.dark], the value is null.
///
/// If this property is null and [selected] is true then [ListTileThemeData.selectedColor]
/// is used. If that is also null then [ColorScheme.primary] is used.
///
/// If this color is a [WidgetStateColor] it will be resolved against
/// [WidgetState.selected] and [WidgetState.disabled] states.
///
/// See also:
///
/// * [ListTileTheme.of], which returns the nearest [ListTileTheme]'s
/// [ListTileThemeData].
final Color? iconColor;
/// Defines the text color for the [title], [subtitle], [leading], and [trailing].
///
/// If this property is null and [selected] is false then [ListTileThemeData.textColor]
/// is used. If that is also null then default text color is used for the [title], [subtitle]
/// [leading], and [trailing]. Except for [subtitle], if [ThemeData.useMaterial3] is false,
/// [TextTheme.bodySmall] is used.
///
/// If this property is null and [selected] is true then [ListTileThemeData.selectedColor]
/// is used. If that is also null then [ColorScheme.primary] is used.
///
/// If this color is a [WidgetStateColor] it will be resolved against
/// [WidgetState.selected] and [WidgetState.disabled] states.
///
/// See also:
///
/// * [ListTileTheme.of], which returns the nearest [ListTileTheme]'s
/// [ListTileThemeData].
final Color? textColor;
/// The text style for ListTile's [title].
///
/// If this property is null, then [ListTileThemeData.titleTextStyle] is used.
/// If that is also null and [ThemeData.useMaterial3] is true, [TextTheme.bodyLarge]
/// with [ColorScheme.onSurface] will be used. Otherwise, If ListTile style is
/// [ListTileStyle.list], [TextTheme.titleMedium] will be used and if ListTile style
/// is [ListTileStyle.drawer], [TextTheme.bodyLarge] will be used.
final TextStyle? titleTextStyle;
/// The text style for ListTile's [subtitle].
///
/// If this property is null, then [ListTileThemeData.subtitleTextStyle] is used.
/// If that is also null and [ThemeData.useMaterial3] is true, [TextTheme.bodyMedium]
/// with [ColorScheme.onSurfaceVariant] will be used, otherwise [TextTheme.bodyMedium]
/// with [TextTheme.bodySmall] color will be used.
final TextStyle? subtitleTextStyle;
/// The text style for ListTile's [leading] and [trailing].
///
/// If this property is null, then [ListTileThemeData.leadingAndTrailingTextStyle] is used.
/// If that is also null and [ThemeData.useMaterial3] is true, [TextTheme.labelSmall]
/// with [ColorScheme.onSurfaceVariant] will be used, otherwise [TextTheme.bodyMedium]
/// will be used.
final TextStyle? leadingAndTrailingTextStyle;
/// Defines the font used for the [title].
///
/// If this property is null then [ListTileThemeData.style] is used. If that
/// is also null then [ListTileStyle.list] is used.
///
/// See also:
///
/// * [ListTileTheme.of], which returns the nearest [ListTileTheme]'s
/// [ListTileThemeData].
final ListTileStyle? style;
/// The tile's internal padding.
///
/// Insets a [ListTile]'s contents: its [leading], [title], [subtitle], and [trailing] widgets.
///
/// If this property is null, then [ListTileThemeData.contentPadding] is used. If that is also
/// null and [ThemeData.useMaterial3] is true, then a default value of
/// `EdgeInsetsDirectional.only(start: 16.0, end: 24.0)` will be used. Otherwise, a default value
/// of `EdgeInsets.symmetric(horizontal: 16.0)` will be used.
final EdgeInsetsGeometry? contentPadding;
/// Whether this list tile is interactive.
///
/// If false, this list tile is styled with the disabled color from the
/// current [Theme] and the [onTap] and [onLongPress] callbacks are
/// inoperative.
final bool enabled;
/// Called when the user taps this list tile.
///
/// Inoperative if [enabled] is false.
final GestureTapCallback? onTap;
/// Called when the user long-presses on this list tile.
///
/// Inoperative if [enabled] is false.
final GestureLongPressCallback? onLongPress;
/// {@macro flutter.material.inkwell.onFocusChange}
final ValueChanged<bool>? onFocusChange;
/// {@template flutter.material.ListTile.mouseCursor}
/// The cursor for a mouse pointer when it enters or is hovering over the
/// widget.
///
/// If [mouseCursor] is a [WidgetStateMouseCursor],
/// [WidgetStateProperty.resolve] is used for the following [WidgetState]s:
///
/// * [WidgetState.selected].
/// * [WidgetState.disabled].
/// {@endtemplate}
///
/// If null, then the value of [ListTileThemeData.mouseCursor] is used. If
/// that is also null, then [WidgetStateMouseCursor.clickable] is used.
final MouseCursor? mouseCursor;
/// If this tile is also [enabled] then icons and text are rendered with the same color.
///
/// By default the selected color is the theme's primary color. The selected color
/// can be overridden with a [ListTileTheme].
///
/// {@tool dartpad}
/// Here is an example of using a [StatefulWidget] to keep track of the
/// selected index, and using that to set the [selected] property on the
/// corresponding [ListTile].
///
/// ** See code in examples/api/lib/material/list_tile/list_tile.selected.0.dart **
/// {@end-tool}
final bool selected;
/// The color for the tile's [Material] when it has the input focus.
final Color? focusColor;
/// The color for the tile's [Material] when a pointer is hovering over it.
final Color? hoverColor;
/// The color of splash for the tile's [Material].
final Color? splashColor;
/// {@macro flutter.widgets.Focus.focusNode}
final FocusNode? focusNode;
/// {@macro flutter.widgets.Focus.autofocus}
final bool autofocus;
/// {@template flutter.material.ListTile.tileColor}
/// Defines the background color of `ListTile` when [selected] is false.
///
/// If this property is null and [selected] is false then [ListTileThemeData.tileColor]
/// is used. If that is also null and [selected] is true, [selectedTileColor] is used.
/// When that is also null, the [ListTileTheme.selectedTileColor] is used, otherwise
/// [Colors.transparent] is used.
///
/// {@endtemplate}
final Color? tileColor;
/// Defines the background color of `ListTile` when [selected] is true.
///
/// When the value if null, the [selectedTileColor] is set to [ListTileTheme.selectedTileColor]
/// if it's not null and to [Colors.transparent] if it's null.
final Color? selectedTileColor;
/// {@template flutter.material.ListTile.enableFeedback}
/// Whether detected gestures should provide acoustic and/or haptic feedback.
///
/// For example, on Android a tap will produce a clicking sound and a
/// long-press will produce a short vibration, when feedback is enabled.
///
/// When null, the default value is true.
/// {@endtemplate}
///
/// See also:
///
/// * [Feedback] for providing platform-specific feedback to certain actions.
final bool? enableFeedback;
/// The horizontal gap between the titles and the leading/trailing widgets.
///
/// If null, then the value of [ListTileTheme.horizontalTitleGap] is used. If
/// that is also null, then a default value of 16 is used.
final double? horizontalTitleGap;
/// The minimum padding on the top and bottom of the title and subtitle widgets.
///
/// If null, then the value of [ListTileTheme.minVerticalPadding] is used. If
/// that is also null, then a default value of 4 is used.
final double? minVerticalPadding;
/// The minimum width allocated for the [ListTile.leading] widget.
///
/// If null, then the value of [ListTileTheme.minLeadingWidth] is used. If
/// that is also null, then a default value of 40 is used.
final double? minLeadingWidth;
/// {@template flutter.material.ListTile.minTileHeight}
/// The minimum height allocated for the [ListTile] widget.
///
/// If this is null, default tile heights are 56.0, 72.0, and 88.0 for one,
/// two, and three lines of text respectively. If `isDense` is true, these
/// defaults are changed to 48.0, 64.0, and 76.0. A visual density value or
/// a large title will also adjust the default tile heights.
/// {@endtemplate}
final double? minTileHeight;
/// Defines how [ListTile.leading] and [ListTile.trailing] are
/// vertically aligned relative to the [ListTile]'s titles
/// ([ListTile.title] and [ListTile.subtitle]).
///
/// If this property is null then [ListTileThemeData.titleAlignment]
/// is used. If that is also null then [ListTileTitleAlignment.threeLine]
/// is used.
///
/// See also:
///
/// * [ListTileTheme.of], which returns the nearest [ListTileTheme]'s
/// [ListTileThemeData].
final ListTileTitleAlignment? titleAlignment;
/// Whether to add button:true to the semantics if onTap is provided.
/// This is a temporary flag to help changing the behavior of ListTile onTap semantics.
///
// TODO(hangyujin): Remove this flag after fixing related g3 tests and flipping
// the default value to true.
final bool internalAddSemanticForOnTap;
/// {@macro flutter.material.inkwell.statesController}
final WidgetStatesController? statesController;
/// Add a one pixel border in between each tile. If color isn't specified the
/// [ThemeData.dividerColor] of the context's [Theme] is used.
///
/// See also:
///
/// * [Divider], which you can use to obtain this effect manually.
static Iterable<Widget> divideTiles({
BuildContext? context,
required Iterable<Widget> tiles,
Color? color,
}) {
assert(color != null || context != null);
tiles = tiles.toList();
if (tiles.isEmpty || tiles.length == 1) {
return tiles;
}
Widget wrapTile(Widget tile) {
return DecoratedBox(
position: DecorationPosition.foreground,
decoration: BoxDecoration(
border: Border(
bottom: Divider.createBorderSide(context, color: color),
),
),
child: tile,
);
}
return <Widget>[...tiles.take(tiles.length - 1).map(wrapTile), tiles.last];
}
bool _isDenseLayout(ThemeData theme, ListTileThemeData tileTheme) {
return dense ?? tileTheme.dense ?? theme.listTileTheme.dense ?? false;
}
Color _tileBackgroundColor(
ThemeData theme,
ListTileThemeData tileTheme,
ListTileThemeData defaults,
) {
final Color? color = selected
? selectedTileColor ??
tileTheme.selectedTileColor ??
theme.listTileTheme.selectedTileColor
: tileColor ?? tileTheme.tileColor ?? theme.listTileTheme.tileColor;
return color ?? defaults.tileColor!;
}
@override
Widget build(BuildContext context) {
assert(debugCheckHasMaterial(context));
final ThemeData theme = Theme.of(context);
final IconButtonThemeData iconButtonTheme = IconButtonTheme.of(context);
final ListTileThemeData tileTheme = ListTileTheme.of(context);
final ListTileStyle listTileStyle =
style ??
tileTheme.style ??
theme.listTileTheme.style ??
ListTileStyle.list;
final ListTileThemeData defaults = theme.useMaterial3
? _LisTileDefaultsM3(context)
: _LisTileDefaultsM2(context, listTileStyle);
final Set<WidgetState> states = <WidgetState>{
if (!enabled) WidgetState.disabled,
if (selected) WidgetState.selected,
};
Color? resolveColor(
Color? explicitColor,
Color? selectedColor,
Color? enabledColor, [
Color? disabledColor,
]) {
return _IndividualOverrides(
explicitColor: explicitColor,
selectedColor: selectedColor,
enabledColor: enabledColor,
disabledColor: disabledColor,
).resolve(states);
}
Color? effectiveIconColor =
resolveColor(iconColor, selectedColor, iconColor) ??
resolveColor(
tileTheme.iconColor,
tileTheme.selectedColor,
tileTheme.iconColor,
) ??
resolveColor(
theme.listTileTheme.iconColor,
theme.listTileTheme.selectedColor,
theme.listTileTheme.iconColor,
);
final Color? defaultEffectiveIconColor = resolveColor(
defaults.iconColor,
defaults.selectedColor,
defaults.iconColor,
theme.disabledColor,
);
final Color? effectiveIconButtonColor =
effectiveIconColor ??
iconButtonTheme.style?.foregroundColor?.resolve(states) ??
defaultEffectiveIconColor;
effectiveIconColor ??= defaultEffectiveIconColor;
final Color? effectiveColor =
resolveColor(textColor, selectedColor, textColor) ??
resolveColor(
tileTheme.textColor,
tileTheme.selectedColor,
tileTheme.textColor,
) ??
resolveColor(
theme.listTileTheme.textColor,
theme.listTileTheme.selectedColor,
theme.listTileTheme.textColor,
) ??
resolveColor(
defaults.textColor,
defaults.selectedColor,
defaults.textColor,
theme.disabledColor,
);
final IconThemeData iconThemeData = IconThemeData(
color: effectiveIconColor,
);
final IconButtonThemeData iconButtonThemeData = IconButtonThemeData(
style:
IconButtonTheme.of(context).style?.copyWith(
foregroundColor: WidgetStatePropertyAll<Color?>(
effectiveIconButtonColor,
),
) ??
IconButton.styleFrom(foregroundColor: effectiveIconButtonColor),
);
TextStyle? leadingAndTrailingStyle;
if (leading != null || trailing != null) {
leadingAndTrailingStyle =
leadingAndTrailingTextStyle ??
tileTheme.leadingAndTrailingTextStyle ??
defaults.leadingAndTrailingTextStyle!;
final Color? leadingAndTrailingTextColor = effectiveColor;
leadingAndTrailingStyle = leadingAndTrailingStyle.copyWith(
color: leadingAndTrailingTextColor,
);
}
Widget? leadingIcon;
if (leading != null) {
leadingIcon = AnimatedDefaultTextStyle(
style: leadingAndTrailingStyle!,
duration: kThemeChangeDuration,
child: leading!,
);
}
TextStyle titleStyle =
titleTextStyle ?? tileTheme.titleTextStyle ?? defaults.titleTextStyle!;
final Color? titleColor = effectiveColor;
titleStyle = titleStyle.copyWith(
color: titleColor,
fontSize: _isDenseLayout(theme, tileTheme) ? 13.0 : null,
);
final Widget titleText = AnimatedDefaultTextStyle(
style: titleStyle,
duration: kThemeChangeDuration,
child: title ?? const SizedBox(),
);
Widget? subtitleText;
TextStyle? subtitleStyle;
if (subtitle != null) {
subtitleStyle =
subtitleTextStyle ??
tileTheme.subtitleTextStyle ??
defaults.subtitleTextStyle!;
final Color? subtitleColor = effectiveColor;
subtitleStyle = subtitleStyle.copyWith(
color: subtitleColor,
fontSize: _isDenseLayout(theme, tileTheme) ? 12.0 : null,
);
subtitleText = AnimatedDefaultTextStyle(
style: subtitleStyle,
duration: kThemeChangeDuration,
child: subtitle!,
);
}
Widget? trailingIcon;
if (trailing != null) {
trailingIcon = AnimatedDefaultTextStyle(
style: leadingAndTrailingStyle!,
duration: kThemeChangeDuration,
child: trailing!,
);
}
final TextDirection textDirection = Directionality.of(context);
final EdgeInsets resolvedContentPadding =
contentPadding?.resolve(textDirection) ??
tileTheme.contentPadding?.resolve(textDirection) ??
defaults.contentPadding!.resolve(textDirection);
// Show basic cursor when ListTile isn't enabled or gesture callbacks are null.
final Set<WidgetState> mouseStates = <WidgetState>{
if (!enabled || (onTap == null && onLongPress == null))
WidgetState.disabled,
};
final MouseCursor effectiveMouseCursor =
WidgetStateProperty.resolveAs<MouseCursor?>(
mouseCursor,
mouseStates,
) ??
tileTheme.mouseCursor?.resolve(mouseStates) ??
WidgetStateMouseCursor.clickable.resolve(mouseStates);
final ListTileTitleAlignment effectiveTitleAlignment =
titleAlignment ??
tileTheme.titleAlignment ??
(theme.useMaterial3
? ListTileTitleAlignment.threeLine
: ListTileTitleAlignment.titleHeight);
return InkWell(
customBorder: shape ?? tileTheme.shape,
onTap: enabled ? onTap : null,
onLongPress: enabled ? onLongPress : null,
onFocusChange: onFocusChange,
mouseCursor: effectiveMouseCursor,
canRequestFocus: enabled,
focusNode: focusNode,
focusColor: focusColor,
hoverColor: hoverColor,
splashColor: splashColor,
autofocus: autofocus,
enableFeedback: enableFeedback ?? tileTheme.enableFeedback ?? true,
statesController: statesController,
child: Semantics(
button:
internalAddSemanticForOnTap &&
(onTap != null || onLongPress != null),
selected: selected,
enabled: enabled,
child: Ink(
decoration: ShapeDecoration(
shape: shape ?? tileTheme.shape ?? const Border(),
color: _tileBackgroundColor(theme, tileTheme, defaults),
),
child: Padding(
padding: resolvedContentPadding,
child: IconTheme.merge(
data: iconThemeData,
child: IconButtonTheme(
data: iconButtonThemeData,
child: _ListTile(
leading: leadingIcon,
title: titleText,
subtitle: subtitleText,
trailing: trailingIcon,
isDense: _isDenseLayout(theme, tileTheme),
visualDensity:
visualDensity ??
tileTheme.visualDensity ??
theme.visualDensity,
isThreeLine:
isThreeLine ??
tileTheme.isThreeLine ??
theme.listTileTheme.isThreeLine ??
false,
textDirection: textDirection,
titleBaselineType:
titleStyle.textBaseline ??
defaults.titleTextStyle!.textBaseline!,
subtitleBaselineType:
subtitleStyle?.textBaseline ??
defaults.subtitleTextStyle!.textBaseline!,
horizontalTitleGap:
horizontalTitleGap ?? tileTheme.horizontalTitleGap ?? 16,
minVerticalPadding:
minVerticalPadding ??
tileTheme.minVerticalPadding ??
defaults.minVerticalPadding!,
minLeadingWidth:
minLeadingWidth ??
tileTheme.minLeadingWidth ??
defaults.minLeadingWidth!,
minTileHeight: minTileHeight ?? tileTheme.minTileHeight,
titleAlignment: effectiveTitleAlignment,
),
),
),
),
),
),
);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties
..add(
FlagProperty(
'isThreeLine',
value: isThreeLine,
ifTrue: 'THREE_LINE',
ifFalse: 'TWO_LINE',
showName: true,
),
)
..add(
FlagProperty(
'dense',
value: dense,
ifTrue: 'true',
ifFalse: 'false',
showName: true,
),
)
..add(
DiagnosticsProperty<VisualDensity>(
'visualDensity',
visualDensity,
defaultValue: null,
),
)
..add(
DiagnosticsProperty<ShapeBorder>('shape', shape, defaultValue: null),
)
..add(
DiagnosticsProperty<ListTileStyle>('style', style, defaultValue: null),
)
..add(
ColorProperty('selectedColor', selectedColor, defaultValue: null),
)
..add(ColorProperty('iconColor', iconColor, defaultValue: null))
..add(ColorProperty('textColor', textColor, defaultValue: null))
..add(
DiagnosticsProperty<TextStyle>(
'titleTextStyle',
titleTextStyle,
defaultValue: null,
),
)
..add(
DiagnosticsProperty<TextStyle>(
'subtitleTextStyle',
subtitleTextStyle,
defaultValue: null,
),
)
..add(
DiagnosticsProperty<TextStyle>(
'leadingAndTrailingTextStyle',
leadingAndTrailingTextStyle,
defaultValue: null,
),
)
..add(
DiagnosticsProperty<EdgeInsetsGeometry>(
'contentPadding',
contentPadding,
defaultValue: null,
),
)
..add(
FlagProperty(
'enabled',
value: enabled,
ifTrue: 'true',
ifFalse: 'false',
showName: true,
defaultValue: true,
),
)
..add(
DiagnosticsProperty<Function>('onTap', onTap, defaultValue: null),
)
..add(
DiagnosticsProperty<Function>(
'onLongPress',
onLongPress,
defaultValue: null,
),
)
..add(
DiagnosticsProperty<MouseCursor>(
'mouseCursor',
mouseCursor,
defaultValue: null,
),
)
..add(
FlagProperty(
'selected',
value: selected,
ifTrue: 'true',
ifFalse: 'false',
showName: true,
defaultValue: false,
),
)
..add(ColorProperty('focusColor', focusColor, defaultValue: null))
..add(ColorProperty('hoverColor', hoverColor, defaultValue: null))
..add(
DiagnosticsProperty<FocusNode>(
'focusNode',
focusNode,
defaultValue: null,
),
)
..add(
FlagProperty(
'autofocus',
value: autofocus,
ifTrue: 'true',
ifFalse: 'false',
showName: true,
defaultValue: false,
),
)
..add(ColorProperty('tileColor', tileColor, defaultValue: null))
..add(
ColorProperty(
'selectedTileColor',
selectedTileColor,
defaultValue: null,
),
)
..add(
FlagProperty(
'enableFeedback',
value: enableFeedback,
ifTrue: 'true',
ifFalse: 'false',
showName: true,
),
)
..add(
DoubleProperty(
'horizontalTitleGap',
horizontalTitleGap,
defaultValue: null,
),
)
..add(
DoubleProperty(
'minVerticalPadding',
minVerticalPadding,
defaultValue: null,
),
)
..add(
DoubleProperty('minLeadingWidth', minLeadingWidth, defaultValue: null),
)
..add(
DiagnosticsProperty<ListTileTitleAlignment>(
'titleAlignment',
titleAlignment,
defaultValue: null,
),
);
}
}
class _IndividualOverrides extends WidgetStateProperty<Color?> {
_IndividualOverrides({
this.explicitColor,
this.enabledColor,
this.selectedColor,
this.disabledColor,
});
final Color? explicitColor;
final Color? enabledColor;
final Color? selectedColor;
final Color? disabledColor;
@override
Color? resolve(Set<WidgetState> states) {
if (explicitColor is WidgetStateColor) {
return WidgetStateProperty.resolveAs<Color?>(explicitColor, states);
}
if (states.contains(WidgetState.disabled)) {
return disabledColor;
}
if (states.contains(WidgetState.selected)) {
return selectedColor;
}
return enabledColor;
}
}
// Identifies the children of a _ListTileElement.
enum _ListTileSlot { leading, title, subtitle, trailing }
class _ListTile
extends SlottedMultiChildRenderObjectWidget<_ListTileSlot, RenderBox> {
const _ListTile({
this.leading,
required this.title,
this.subtitle,
this.trailing,
required this.isThreeLine,
required this.isDense,
required this.visualDensity,
required this.textDirection,
required this.titleBaselineType,
required this.horizontalTitleGap,
required this.minVerticalPadding,
required this.minLeadingWidth,
this.minTileHeight,
this.subtitleBaselineType,
required this.titleAlignment,
});
final Widget? leading;
final Widget title;
final Widget? subtitle;
final Widget? trailing;
final bool isThreeLine;
final bool isDense;
final VisualDensity visualDensity;
final TextDirection textDirection;
final TextBaseline titleBaselineType;
final TextBaseline? subtitleBaselineType;
final double horizontalTitleGap;
final double minVerticalPadding;
final double minLeadingWidth;
final double? minTileHeight;
final ListTileTitleAlignment titleAlignment;
@override
Iterable<_ListTileSlot> get slots => _ListTileSlot.values;
@override
Widget? childForSlot(_ListTileSlot slot) {
return switch (slot) {
_ListTileSlot.leading => leading,
_ListTileSlot.title => title,
_ListTileSlot.subtitle => subtitle,
_ListTileSlot.trailing => trailing,
};
}
@override
_RenderListTile createRenderObject(BuildContext context) {
return _RenderListTile(
isThreeLine: isThreeLine,
isDense: isDense,
visualDensity: visualDensity,
textDirection: textDirection,
titleBaselineType: titleBaselineType,
subtitleBaselineType: subtitleBaselineType,
horizontalTitleGap: horizontalTitleGap,
minVerticalPadding: minVerticalPadding,
minLeadingWidth: minLeadingWidth,
minTileHeight: minTileHeight,
titleAlignment: titleAlignment,
);
}
@override
void updateRenderObject(BuildContext context, _RenderListTile renderObject) {
renderObject
..isThreeLine = isThreeLine
..isDense = isDense
..visualDensity = visualDensity
..textDirection = textDirection
..titleBaselineType = titleBaselineType
..subtitleBaselineType = subtitleBaselineType
..horizontalTitleGap = horizontalTitleGap
..minLeadingWidth = minLeadingWidth
..minTileHeight = minTileHeight
..minVerticalPadding = minVerticalPadding
..titleAlignment = titleAlignment;
}
}
class _RenderListTile extends RenderBox
with SlottedContainerRenderObjectMixin<_ListTileSlot, RenderBox> {
_RenderListTile({
required bool isDense,
required VisualDensity visualDensity,
required bool isThreeLine,
required TextDirection textDirection,
required TextBaseline titleBaselineType,
TextBaseline? subtitleBaselineType,
required double horizontalTitleGap,
required double minVerticalPadding,
required double minLeadingWidth,
double? minTileHeight,
required ListTileTitleAlignment titleAlignment,
}) : _isDense = isDense,
_visualDensity = visualDensity,
_isThreeLine = isThreeLine,
_textDirection = textDirection,
_titleBaselineType = titleBaselineType,
_subtitleBaselineType = subtitleBaselineType,
_horizontalTitleGap = horizontalTitleGap,
_minVerticalPadding = minVerticalPadding,
_minLeadingWidth = minLeadingWidth,
_minTileHeight = minTileHeight,
_titleAlignment = titleAlignment;
RenderBox? get leading => childForSlot(_ListTileSlot.leading);
RenderBox get title => childForSlot(_ListTileSlot.title)!;
RenderBox? get subtitle => childForSlot(_ListTileSlot.subtitle);
RenderBox? get trailing => childForSlot(_ListTileSlot.trailing);
// The returned list is ordered for hit testing.
@override
Iterable<RenderBox> get children {
final RenderBox? title = childForSlot(_ListTileSlot.title);
return <RenderBox>[
if (leading != null) leading!,
if (title != null) title,
if (subtitle != null) subtitle!,
if (trailing != null) trailing!,
];
}
bool get isDense => _isDense;
bool _isDense;
set isDense(bool value) {
if (_isDense == value) {
return;
}
_isDense = value;
markNeedsLayout();
}
VisualDensity get visualDensity => _visualDensity;
VisualDensity _visualDensity;
set visualDensity(VisualDensity value) {
if (_visualDensity == value) {
return;
}
_visualDensity = value;
markNeedsLayout();
}
bool get isThreeLine => _isThreeLine;
bool _isThreeLine;
set isThreeLine(bool value) {
if (_isThreeLine == value) {
return;
}
_isThreeLine = value;
markNeedsLayout();
}
TextDirection get textDirection => _textDirection;
TextDirection _textDirection;
set textDirection(TextDirection value) {
if (_textDirection == value) {
return;
}
_textDirection = value;
markNeedsLayout();
}
TextBaseline get titleBaselineType => _titleBaselineType;
TextBaseline _titleBaselineType;
set titleBaselineType(TextBaseline value) {
if (_titleBaselineType == value) {
return;
}
_titleBaselineType = value;
markNeedsLayout();
}
TextBaseline? get subtitleBaselineType => _subtitleBaselineType;
TextBaseline? _subtitleBaselineType;
set subtitleBaselineType(TextBaseline? value) {
if (_subtitleBaselineType == value) {
return;
}
_subtitleBaselineType = value;
markNeedsLayout();
}
double get horizontalTitleGap => _horizontalTitleGap;
double _horizontalTitleGap;
double get _effectiveHorizontalTitleGap =>
_horizontalTitleGap + visualDensity.horizontal * 2.0;
set horizontalTitleGap(double value) {
if (_horizontalTitleGap == value) {
return;
}
_horizontalTitleGap = value;
markNeedsLayout();
}
double get minVerticalPadding => _minVerticalPadding;
double _minVerticalPadding;
set minVerticalPadding(double value) {
if (_minVerticalPadding == value) {
return;
}
_minVerticalPadding = value;
markNeedsLayout();
}
double get minLeadingWidth => _minLeadingWidth;
double _minLeadingWidth;
set minLeadingWidth(double value) {
if (_minLeadingWidth == value) {
return;
}
_minLeadingWidth = value;
markNeedsLayout();
}
double? _minTileHeight;
double? get minTileHeight => _minTileHeight;
set minTileHeight(double? value) {
if (_minTileHeight == value) {
return;
}
_minTileHeight = value;
markNeedsLayout();
}
ListTileTitleAlignment get titleAlignment => _titleAlignment;
ListTileTitleAlignment _titleAlignment;
set titleAlignment(ListTileTitleAlignment value) {
if (_titleAlignment == value) {
return;
}
_titleAlignment = value;
markNeedsLayout();
}
@override
bool get sizedByParent => false;
static double _minWidth(RenderBox? box, double height) {
return box == null ? 0.0 : box.getMinIntrinsicWidth(height);
}
static double _maxWidth(RenderBox? box, double height) {
return box == null ? 0.0 : box.getMaxIntrinsicWidth(height);
}
@override
double computeMinIntrinsicWidth(double height) {
final double leadingWidth = leading != null
? math.max(leading!.getMinIntrinsicWidth(height), _minLeadingWidth) +
_effectiveHorizontalTitleGap
: 0.0;
return leadingWidth +
math.max(_minWidth(title, height), _minWidth(subtitle, height)) +
_maxWidth(trailing, height);
}
@override
double computeMaxIntrinsicWidth(double height) {
final double leadingWidth = leading != null
? math.max(leading!.getMaxIntrinsicWidth(height), _minLeadingWidth) +
_effectiveHorizontalTitleGap
: 0.0;
return leadingWidth +
math.max(_maxWidth(title, height), _maxWidth(subtitle, height)) +
_maxWidth(trailing, height);
}
// The target tile height to use if _minTileHeight is not specified.
double get _defaultTileHeight {
final Offset baseDensity = visualDensity.baseSizeAdjustment;
return baseDensity.dy +
switch ((isThreeLine, subtitle != null)) {
(true, _) => isDense ? 76.0 : 88.0, // 3 lines,
(false, true) => isDense ? 64.0 : 72.0, // 2 lines
(false, false) => isDense ? 48.0 : 56.0, // 1 line,
};
}
double get _targetTileHeight => _minTileHeight ?? _defaultTileHeight;
@override
double computeMinIntrinsicHeight(double width) {
return math.max(
_targetTileHeight,
title.getMinIntrinsicHeight(width) +
(subtitle?.getMinIntrinsicHeight(width) ?? 0.0),
);
}
@override
double computeMaxIntrinsicHeight(double width) {
return getMinIntrinsicHeight(width);
}
@override
double? computeDistanceToActualBaseline(TextBaseline baseline) {
final BoxParentData parentData = title.parentData! as BoxParentData;
final BaselineOffset offset =
BaselineOffset(title.getDistanceToActualBaseline(baseline)) +
parentData.offset.dy;
return offset.offset;
}
BoxConstraints get maxIconHeightConstraint => BoxConstraints(
// One-line trailing and leading widget heights do not follow
// Material specifications, but this sizing is required to adhere
// to accessibility requirements for smallest tappable widget.
// Two- and three-line trailing widget heights are constrained
// properly according to the Material spec.
maxHeight: (isDense ? 48.0 : 56.0) + visualDensity.baseSizeAdjustment.dy,
);
static void _positionBox(RenderBox box, Offset offset) {
final BoxParentData parentData = box.parentData! as BoxParentData;
parentData.offset = offset;
}
// Implements _RenderListTile's layout algorithm. If `positionChild` is not null,
// it will be called on each child with that child's layout offset.
//
// All of the dimensions below were taken from the Material Design spec:
// https://material.io/design/components/lists.html#specs
_Sizes _computeSizes(
ChildBaselineGetter getBaseline,
ChildLayouter getSize,
BoxConstraints constraints, {
_PositionChild? positionChild,
}) {
final BoxConstraints looseConstraints = constraints.loosen();
final double tileWidth = looseConstraints.maxWidth;
final BoxConstraints iconConstraints = looseConstraints.enforce(
maxIconHeightConstraint,
);
final RenderBox? leading = this.leading;
final RenderBox? trailing = this.trailing;
final Size? leadingSize = leading == null
? null
: getSize(leading, iconConstraints);
final Size? trailingSize = trailing == null
? null
: getSize(trailing, iconConstraints);
assert(() {
if (tileWidth == 0.0) {
return true;
}
String? overflowedWidget;
if (tileWidth == leadingSize?.width) {
overflowedWidget = 'Leading';
} else if (tileWidth == trailingSize?.width) {
overflowedWidget = 'Trailing';
}
if (overflowedWidget == null) {
return true;
}
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary(
'$overflowedWidget widget consumes the entire tile width (including ListTile.contentPadding).',
),
ErrorDescription(
'Either resize the tile width so that the ${overflowedWidget.toLowerCase()} widget plus any content padding '
'do not exceed the tile width, or use a sized widget, or consider replacing '
'ListTile with a custom widget.',
),
ErrorHint(
'See also: https://api.flutter.dev/flutter/material/ListTile-class.html#material.ListTile.4',
),
]);
}());
final double titleStart = leadingSize == null
? 0.0
: math.max(_minLeadingWidth, leadingSize.width) +
_effectiveHorizontalTitleGap;
final double adjustedTrailingWidth = trailingSize == null
? 0.0
: math.max(trailingSize.width + _effectiveHorizontalTitleGap, 32.0);
final BoxConstraints textConstraints = looseConstraints.tighten(
width: tileWidth - titleStart - adjustedTrailingWidth,
);
final RenderBox? subtitle = this.subtitle;
final double titleHeight = getSize(title, textConstraints).height;
final bool isLTR = switch (textDirection) {
TextDirection.ltr => true,
TextDirection.rtl => false,
};
final double titleY;
final double tileHeight;
if (subtitle == null) {
tileHeight = math.max(
_targetTileHeight,
titleHeight + 2.0 * _minVerticalPadding,
);
titleY = (tileHeight - titleHeight) / 2.0;
} else {
final double subtitleHeight = getSize(subtitle, textConstraints).height;
final double titleBaseline =
getBaseline(title, textConstraints, titleBaselineType) ?? titleHeight;
final double subtitleBaseline =
getBaseline(subtitle, textConstraints, subtitleBaselineType!) ??
subtitleHeight;
final double targetTitleY =
(isThreeLine ? (isDense ? 22.0 : 28.0) : (isDense ? 28.0 : 32.0)) -
titleBaseline;
final double targetSubtitleY =
(isThreeLine ? (isDense ? 42.0 : 48.0) : (isDense ? 48.0 : 52.0)) +
visualDensity.vertical * 2.0 -
subtitleBaseline;
// Prevent the title and the subtitle from overlapping by moving them away from
// each other by the same distance.
final double halfOverlap =
math.max(targetTitleY + titleHeight - targetSubtitleY, 0) / 2;
final double idealTitleY = targetTitleY - halfOverlap;
final double idealSubtitleY = targetSubtitleY + halfOverlap;
// However if either component can't maintain the minimal padding from the top/bottom edges, the ListTile enters "compat mode".
final bool compact =
idealTitleY < minVerticalPadding ||
idealSubtitleY + subtitleHeight + minVerticalPadding >
_targetTileHeight;
// Position subtitle.
positionChild?.call(
subtitle,
Offset(
isLTR ? titleStart : adjustedTrailingWidth,
compact ? minVerticalPadding + titleHeight : idealSubtitleY,
),
);
tileHeight = compact
? 2 * _minVerticalPadding + titleHeight + subtitleHeight
: _targetTileHeight;
titleY = compact ? minVerticalPadding : idealTitleY;
}
if (positionChild != null) {
positionChild(
title,
Offset(isLTR ? titleStart : adjustedTrailingWidth, titleY),
);
if (leading != null && leadingSize != null) {
positionChild(
leading,
Offset(
isLTR ? 0.0 : tileWidth - leadingSize.width,
titleAlignment._yOffsetFor(
leadingSize.height,
tileHeight,
this,
true,
),
),
);
}
if (trailing != null && trailingSize != null) {
positionChild(
trailing,
Offset(
isLTR ? tileWidth - trailingSize.width : 0.0,
titleAlignment._yOffsetFor(
trailingSize.height,
tileHeight,
this,
false,
),
),
);
}
}
return (
titleY: titleY,
textConstraints: textConstraints,
tileSize: Size(tileWidth, tileHeight),
);
}
@override
double? computeDryBaseline(
covariant BoxConstraints constraints,
TextBaseline baseline,
) {
final _Sizes sizes = _computeSizes(
ChildLayoutHelper.getDryBaseline,
ChildLayoutHelper.dryLayoutChild,
constraints,
);
final BaselineOffset titleBaseline =
BaselineOffset(title.getDryBaseline(sizes.textConstraints, baseline)) +
sizes.titleY;
return titleBaseline.offset;
}
@override
Size computeDryLayout(BoxConstraints constraints) {
return constraints.constrain(
_computeSizes(
ChildLayoutHelper.getDryBaseline,
ChildLayoutHelper.dryLayoutChild,
constraints,
).tileSize,
);
}
@override
void performLayout() {
final Size tileSize = _computeSizes(
ChildLayoutHelper.getBaseline,
ChildLayoutHelper.layoutChild,
constraints,
positionChild: _positionBox,
).tileSize;
size = constraints.constrain(tileSize);
assert(size.width == constraints.constrainWidth(tileSize.width));
assert(size.height == constraints.constrainHeight(tileSize.height));
}
@override
void paint(PaintingContext context, Offset offset) {
void doPaint(RenderBox? child) {
if (child != null) {
final BoxParentData parentData = child.parentData! as BoxParentData;
context.paintChild(child, parentData.offset + offset);
}
}
doPaint(leading);
doPaint(title);
doPaint(subtitle);
doPaint(trailing);
}
@override
bool hitTestSelf(Offset position) => true;
@override
bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
for (final RenderBox child in children) {
final BoxParentData parentData = child.parentData! as BoxParentData;
final bool isHit = result.addWithPaintOffset(
offset: parentData.offset,
position: position,
hitTest: (BoxHitTestResult result, Offset transformed) {
assert(transformed == position - parentData.offset);
return child.hitTest(result, position: transformed);
},
);
if (isHit) {
return true;
}
}
return false;
}
}
class _LisTileDefaultsM2 extends ListTileThemeData {
_LisTileDefaultsM2(this.context, ListTileStyle style)
: super(
contentPadding: const EdgeInsets.symmetric(horizontal: 16.0),
minLeadingWidth: 40,
minVerticalPadding: 4,
shape: const Border(),
style: style,
);
final BuildContext context;
late final ThemeData _theme = Theme.of(context);
late final TextTheme _textTheme = _theme.textTheme;
@override
Color? get tileColor => Colors.transparent;
@override
TextStyle? get titleTextStyle => switch (style!) {
ListTileStyle.drawer => _textTheme.bodyLarge,
ListTileStyle.list => _textTheme.titleMedium,
};
@override
TextStyle? get subtitleTextStyle =>
_textTheme.bodyMedium!.copyWith(color: _textTheme.bodySmall!.color);
@override
TextStyle? get leadingAndTrailingTextStyle => _textTheme.bodyMedium;
@override
Color? get selectedColor => _theme.colorScheme.primary;
@override
Color? get iconColor => switch (_theme.brightness) {
// For the sake of backwards compatibility, the default for unselected
// tiles is Colors.black45 rather than colorScheme.onSurface.withAlpha(0x73).
Brightness.light => Colors.black45,
// null -> use current icon theme color
Brightness.dark => null,
};
}
// BEGIN GENERATED TOKEN PROPERTIES - LisTile
// Do not edit by hand. The code between the "BEGIN GENERATED" and
// "END GENERATED" comments are generated from data in the Material
// Design token database by the script:
// dev/tools/gen_defaults/bin/gen_defaults.dart.
// dart format off
class _LisTileDefaultsM3 extends ListTileThemeData {
_LisTileDefaultsM3(this.context)
: super(
contentPadding: const EdgeInsetsDirectional.only(start: 16.0, end: 24.0),
minLeadingWidth: 24,
minVerticalPadding: 8,
shape: const RoundedRectangleBorder(),
);
final BuildContext context;
late final ThemeData _theme = Theme.of(context);
late final ColorScheme _colors = _theme.colorScheme;
late final TextTheme _textTheme = _theme.textTheme;
@override
Color? get tileColor => Colors.transparent;
@override
TextStyle? get titleTextStyle => _textTheme.bodyLarge!.copyWith(color: _colors.onSurface);
@override
TextStyle? get subtitleTextStyle => _textTheme.bodyMedium!.copyWith(color: _colors.onSurfaceVariant);
@override
TextStyle? get leadingAndTrailingTextStyle => _textTheme.labelSmall!.copyWith(color: _colors.onSurfaceVariant);
@override
Color? get selectedColor => _colors.primary;
@override
Color? get iconColor => _colors.onSurfaceVariant;
}
// dart format on
// END GENERATED TOKEN PROPERTIES - LisTile