| // 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:math' as math; |
| |
| import 'package:flutter/rendering.dart'; |
| import 'package:flutter/widgets.dart'; |
| |
| import 'color_scheme.dart'; |
| import 'colors.dart'; |
| import 'constants.dart'; |
| import 'debug.dart'; |
| import 'divider.dart'; |
| import 'icon_button.dart'; |
| import 'icon_button_theme.dart'; |
| import 'ink_decoration.dart'; |
| import 'ink_well.dart'; |
| import 'list_tile_theme.dart'; |
| import 'material_state.dart'; |
| import 'text_theme.dart'; |
| import 'theme.dart'; |
| import 'theme_data.dart'; |
| |
| // Examples can assume: |
| // int _act = 1; |
| |
| /// Defines the title font used for [ListTile] descendants of a [ListTileTheme]. |
| /// |
| /// List tiles that appear in a [Drawer] use the theme's [TextTheme.bodyLarge] |
| /// text style, which is a little smaller than the theme's [TextTheme.titleMedium] |
| /// text style, which is used by default. |
| enum ListTileStyle { |
| /// Use a title font that's appropriate for a [ListTile] in a list. |
| list, |
| |
| /// Use a title font that's appropriate for a [ListTile] that appears in a [Drawer]. |
| drawer, |
| } |
| |
| /// Where to place the control in widgets that use [ListTile] to position a |
| /// control next to a label. |
| /// |
| /// See also: |
| /// |
| /// * [CheckboxListTile], which combines a [ListTile] with a [Checkbox]. |
| /// * [RadioListTile], which combines a [ListTile] with a [Radio] button. |
| /// * [SwitchListTile], which combines a [ListTile] with a [Switch]. |
| /// * [ExpansionTile], which combines a [ListTile] with a button that expands |
| /// or collapses the tile to reveal or hide the children. |
| enum ListTileControlAffinity { |
| /// Position the control on the leading edge, and the secondary widget, if |
| /// any, on the trailing edge. |
| leading, |
| |
| /// Position the control on the trailing edge, and the secondary widget, if |
| /// any, on the leading edge. |
| trailing, |
| |
| /// Position the control relative to the text in the fashion that is typical |
| /// for the current platform, and place the secondary widget on the opposite |
| /// side. |
| platform, |
| } |
| |
| /// 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. |
| enum ListTileTitleAlignment { |
| /// The top of the [ListTile.leading] and [ListTile.trailing] widgets are |
| /// placed [ListTile.minVerticalPadding] below the top of the [ListTile.title] |
| /// if [ListTile.isThreeLine] is true, otherwise they're centered relative |
| /// to the [ListTile.title] and [ListTile.subtitle] widgets. |
| /// |
| /// This is the default when [ThemeData.useMaterial3] is true. |
| threeLine, |
| |
| /// The tops of the [ListTile.leading] and [ListTile.trailing] widgets are |
| /// placed 16 units below the top of the [ListTile.title] |
| /// if the titles' overall height is greater than 72, otherwise they're |
| /// centered relative to the [ListTile.title] and [ListTile.subtitle] widgets. |
| /// |
| /// This is the default when [ThemeData.useMaterial3] is false. |
| titleHeight, |
| |
| /// The tops of the [ListTile.leading] and [ListTile.trailing] widgets are |
| /// placed [ListTile.minVerticalPadding] below the top of the [ListTile.title]. |
| top, |
| |
| /// The [ListTile.leading] and [ListTile.trailing] widgets are |
| /// centered relative to the [ListTile]'s titles. |
| center, |
| |
| /// The bottoms of the [ListTile.leading] and [ListTile.trailing] widgets are |
| /// placed [ListTile.minVerticalPadding] above the bottom of the [ListTile]'s |
| /// titles. |
| bottom, |
| } |
| |
| /// 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 |
| /// [MaterialStateColor] 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. |
| /// |
| /// ![Custom list item a](https://flutter.github.io/assets-for-api-docs/assets/widgets/custom_list_item_a.png) |
| /// |
| /// ** 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. |
| /// |
| /// ![Custom list item b](https://flutter.github.io/assets-for-api-docs/assets/widgets/custom_list_item_b.png) |
| /// |
| /// ** 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://flutter.dev/docs/cookbook/lists/basic-list) |
| /// * Cookbook: [Implement swipe to dismiss](https://flutter.dev/docs/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 = false, |
| 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.titleAlignment, |
| }) : assert(!isThreeLine || 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]. |
| final bool isThreeLine; |
| |
| /// 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. |
| 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 [MaterialStateColor] it will be resolved against |
| /// [MaterialState.selected] and [MaterialState.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 [MaterialStateColor] it will be resolved against |
| /// [MaterialState.selected] and [MaterialState.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] |
| /// 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, [TextTheme.bodyMedium] 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] |
| /// 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 null, `EdgeInsets.symmetric(horizontal: 16.0)` is 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 [MaterialStateProperty<MouseCursor>], |
| /// [MaterialStateProperty.resolve] is used for the following [MaterialState]s: |
| /// |
| /// * [MaterialState.selected]. |
| /// * [MaterialState.disabled]. |
| /// {@endtemplate} |
| /// |
| /// If null, then the value of [ListTileThemeData.mouseCursor] is used. If |
| /// that is also null, then [MaterialStateMouseCursor.clickable] is used. |
| /// |
| /// See also: |
| /// |
| /// * [MaterialStateMouseCursor], which can be used to create a [MouseCursor] |
| /// that is also a [MaterialStateProperty<MouseCursor>]. |
| 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; |
| |
| /// 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; |
| |
| /// 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 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<MaterialState> states = <MaterialState>{ |
| if (!enabled) MaterialState.disabled, |
| if (selected) MaterialState.selected, |
| }; |
| |
| Color? resolveColor(Color? explicitColor, Color? selectedColor, Color? enabledColor, [Color? disabledColor]) { |
| return _IndividualOverrides( |
| explicitColor: explicitColor, |
| selectedColor: selectedColor, |
| enabledColor: enabledColor, |
| disabledColor: disabledColor, |
| ).resolve(states); |
| } |
| |
| final Color? effectiveIconColor = resolveColor(iconColor, selectedColor, iconColor) |
| ?? resolveColor(tileTheme.iconColor, tileTheme.selectedColor, tileTheme.iconColor) |
| ?? resolveColor(theme.listTileTheme.iconColor, theme.listTileTheme.selectedColor, theme.listTileTheme.iconColor) |
| ?? resolveColor(defaults.iconColor, defaults.selectedColor, defaults.iconColor, theme.disabledColor); |
| 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: IconButton.styleFrom(foregroundColor: effectiveIconColor), |
| ); |
| |
| 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 ?? theme.textTheme.bodySmall!.color; |
| 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<MaterialState> mouseStates = <MaterialState>{ |
| if (!enabled || (onTap == null && onLongPress == null)) MaterialState.disabled, |
| }; |
| final MouseCursor effectiveMouseCursor = MaterialStateProperty.resolveAs<MouseCursor?>(mouseCursor, mouseStates) |
| ?? tileTheme.mouseCursor?.resolve(mouseStates) |
| ?? MaterialStateMouseCursor.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, |
| child: Semantics( |
| selected: selected, |
| enabled: enabled, |
| child: Ink( |
| decoration: ShapeDecoration( |
| shape: shape ?? tileTheme.shape ?? const Border(), |
| color: _tileBackgroundColor(theme, tileTheme, defaults), |
| ), |
| child: SafeArea( |
| top: false, |
| bottom: false, |
| minimum: 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, |
| 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!, |
| titleAlignment: effectiveTitleAlignment, |
| ), |
| ), |
| ), |
| ), |
| ), |
| ), |
| ); |
| } |
| |
| @override |
| void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
| super.debugFillProperties(properties); |
| properties.add(DiagnosticsProperty<Widget>('leading', leading, defaultValue: null)); |
| properties.add(DiagnosticsProperty<Widget>('title', title, defaultValue: null)); |
| properties.add(DiagnosticsProperty<Widget>('subtitle', subtitle, defaultValue: null)); |
| properties.add(DiagnosticsProperty<Widget>('trailing', trailing, defaultValue: null)); |
| properties.add(FlagProperty('isThreeLine', value: isThreeLine, ifTrue:'THREE_LINE', ifFalse: 'TWO_LINE', showName: true, defaultValue: false)); |
| properties.add(FlagProperty('dense', value: dense, ifTrue: 'true', ifFalse: 'false', showName: true)); |
| properties.add(DiagnosticsProperty<VisualDensity>('visualDensity', visualDensity, defaultValue: null)); |
| properties.add(DiagnosticsProperty<ShapeBorder>('shape', shape, defaultValue: null)); |
| properties.add(DiagnosticsProperty<ListTileStyle>('style', style, defaultValue: null)); |
| properties.add(ColorProperty('selectedColor', selectedColor, defaultValue: null)); |
| properties.add(ColorProperty('iconColor', iconColor, defaultValue: null)); |
| properties.add(ColorProperty('textColor', textColor, defaultValue: null)); |
| properties.add(DiagnosticsProperty<TextStyle>('titleTextStyle', titleTextStyle, defaultValue: null)); |
| properties.add(DiagnosticsProperty<TextStyle>('subtitleTextStyle', subtitleTextStyle, defaultValue: null)); |
| properties.add(DiagnosticsProperty<TextStyle>('leadingAndTrailingTextStyle', leadingAndTrailingTextStyle, defaultValue: null)); |
| properties.add(DiagnosticsProperty<EdgeInsetsGeometry>('contentPadding', contentPadding, defaultValue: null)); |
| properties.add(FlagProperty('enabled', value: enabled, ifTrue: 'true', ifFalse: 'false', showName: true, defaultValue: true)); |
| properties.add(DiagnosticsProperty<Function>('onTap', onTap, defaultValue: null)); |
| properties.add(DiagnosticsProperty<Function>('onLongPress', onLongPress, defaultValue: null)); |
| properties.add(DiagnosticsProperty<MouseCursor>('mouseCursor', mouseCursor, defaultValue: null)); |
| properties.add(FlagProperty('selected', value: selected, ifTrue: 'true', ifFalse: 'false', showName: true, defaultValue: false)); |
| properties.add(ColorProperty('focusColor', focusColor, defaultValue: null)); |
| properties.add(ColorProperty('hoverColor', hoverColor, defaultValue: null)); |
| properties.add(DiagnosticsProperty<FocusNode>('focusNode', focusNode, defaultValue: null)); |
| properties.add(FlagProperty('autofocus', value: autofocus, ifTrue: 'true', ifFalse: 'false', showName: true, defaultValue: false)); |
| properties.add(ColorProperty('tileColor', tileColor, defaultValue: null)); |
| properties.add(ColorProperty('selectedTileColor', selectedTileColor, defaultValue: null)); |
| properties.add(FlagProperty('enableFeedback', value: enableFeedback, ifTrue: 'true', ifFalse: 'false', showName: true)); |
| properties.add(DoubleProperty('horizontalTitleGap', horizontalTitleGap, defaultValue: null)); |
| properties.add(DoubleProperty('minVerticalPadding', minVerticalPadding, defaultValue: null)); |
| properties.add(DoubleProperty('minLeadingWidth', minLeadingWidth, defaultValue: null)); |
| properties.add(DiagnosticsProperty<ListTileTitleAlignment>('titleAlignment', titleAlignment, defaultValue: null)); |
| } |
| } |
| |
| class _IndividualOverrides extends MaterialStateProperty<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<MaterialState> states) { |
| if (explicitColor is MaterialStateColor) { |
| return MaterialStateProperty.resolveAs<Color?>(explicitColor, states); |
| } |
| if (states.contains(MaterialState.disabled)) { |
| return disabledColor; |
| } |
| if (states.contains(MaterialState.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.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 ListTileTitleAlignment titleAlignment; |
| |
| @override |
| Iterable<_ListTileSlot> get slots => _ListTileSlot.values; |
| |
| @override |
| Widget? childForSlot(_ListTileSlot slot) { |
| switch (slot) { |
| case _ListTileSlot.leading: |
| return leading; |
| case _ListTileSlot.title: |
| return title; |
| case _ListTileSlot.subtitle: |
| return subtitle; |
| case _ListTileSlot.trailing: |
| return 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, |
| 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 |
| ..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, |
| required ListTileTitleAlignment titleAlignment, |
| }) : _isDense = isDense, |
| _visualDensity = visualDensity, |
| _isThreeLine = isThreeLine, |
| _textDirection = textDirection, |
| _titleBaselineType = titleBaselineType, |
| _subtitleBaselineType = subtitleBaselineType, |
| _horizontalTitleGap = horizontalTitleGap, |
| _minVerticalPadding = minVerticalPadding, |
| _minLeadingWidth = minLeadingWidth, |
| _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 { |
| 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(); |
| } |
| |
| 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); |
| } |
| |
| double get _defaultTileHeight { |
| final bool hasSubtitle = subtitle != null; |
| final bool isTwoLine = !isThreeLine && hasSubtitle; |
| final bool isOneLine = !isThreeLine && !hasSubtitle; |
| |
| final Offset baseDensity = visualDensity.baseSizeAdjustment; |
| if (isOneLine) { |
| return (isDense ? 48.0 : 56.0) + baseDensity.dy; |
| } |
| if (isTwoLine) { |
| return (isDense ? 64.0 : 72.0) + baseDensity.dy; |
| } |
| return (isDense ? 76.0 : 88.0) + baseDensity.dy; |
| } |
| |
| @override |
| double computeMinIntrinsicHeight(double width) { |
| return math.max( |
| _defaultTileHeight, |
| title!.getMinIntrinsicHeight(width) + (subtitle?.getMinIntrinsicHeight(width) ?? 0.0), |
| ); |
| } |
| |
| @override |
| double computeMaxIntrinsicHeight(double width) { |
| return computeMinIntrinsicHeight(width); |
| } |
| |
| @override |
| double computeDistanceToActualBaseline(TextBaseline baseline) { |
| assert(title != null); |
| final BoxParentData parentData = title!.parentData! as BoxParentData; |
| return parentData.offset.dy + title!.getDistanceToActualBaseline(baseline)!; |
| } |
| |
| static double? _boxBaseline(RenderBox box, TextBaseline baseline) { |
| return box.getDistanceToBaseline(baseline); |
| } |
| |
| static Size _layoutBox(RenderBox? box, BoxConstraints constraints) { |
| if (box == null) { |
| return Size.zero; |
| } |
| box.layout(constraints, parentUsesSize: true); |
| return box.size; |
| } |
| |
| static void _positionBox(RenderBox box, Offset offset) { |
| final BoxParentData parentData = box.parentData! as BoxParentData; |
| parentData.offset = offset; |
| } |
| |
| @override |
| Size computeDryLayout(BoxConstraints constraints) { |
| assert(debugCannotComputeDryLayout( |
| reason: 'Layout requires baseline metrics, which are only available after a full layout.', |
| )); |
| return Size.zero; |
| } |
| |
| // All of the dimensions below were taken from the Material Design spec: |
| // https://material.io/design/components/lists.html#specs |
| @override |
| void performLayout() { |
| final BoxConstraints constraints = this.constraints; |
| final bool hasLeading = leading != null; |
| final bool hasSubtitle = subtitle != null; |
| final bool hasTrailing = trailing != null; |
| final bool isTwoLine = !isThreeLine && hasSubtitle; |
| final bool isOneLine = !isThreeLine && !hasSubtitle; |
| final Offset densityAdjustment = visualDensity.baseSizeAdjustment; |
| |
| final BoxConstraints 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) + densityAdjustment.dy, |
| ); |
| final BoxConstraints looseConstraints = constraints.loosen(); |
| final BoxConstraints iconConstraints = looseConstraints.enforce(maxIconHeightConstraint); |
| |
| final double tileWidth = looseConstraints.maxWidth; |
| final Size leadingSize = _layoutBox(leading, iconConstraints); |
| final Size trailingSize = _layoutBox(trailing, iconConstraints); |
| assert( |
| tileWidth != leadingSize.width || tileWidth == 0.0, |
| 'Leading widget consumes entire tile width. Please use a sized widget, ' |
| 'or consider replacing ListTile with a custom widget ' |
| '(see https://api.flutter.dev/flutter/material/ListTile-class.html#material.ListTile.4)', |
| ); |
| assert( |
| tileWidth != trailingSize.width || tileWidth == 0.0, |
| 'Trailing widget consumes entire tile width. Please use a sized widget, ' |
| 'or consider replacing ListTile with a custom widget ' |
| '(see https://api.flutter.dev/flutter/material/ListTile-class.html#material.ListTile.4)', |
| ); |
| |
| final double titleStart = hasLeading |
| ? math.max(_minLeadingWidth, leadingSize.width) + _effectiveHorizontalTitleGap |
| : 0.0; |
| final double adjustedTrailingWidth = hasTrailing |
| ? math.max(trailingSize.width + _effectiveHorizontalTitleGap, 32.0) |
| : 0.0; |
| final BoxConstraints textConstraints = looseConstraints.tighten( |
| width: tileWidth - titleStart - adjustedTrailingWidth, |
| ); |
| final Size titleSize = _layoutBox(title, textConstraints); |
| final Size subtitleSize = _layoutBox(subtitle, textConstraints); |
| |
| double? titleBaseline; |
| double? subtitleBaseline; |
| if (isTwoLine) { |
| titleBaseline = isDense ? 28.0 : 32.0; |
| subtitleBaseline = isDense ? 48.0 : 52.0; |
| } else if (isThreeLine) { |
| titleBaseline = isDense ? 22.0 : 28.0; |
| subtitleBaseline = isDense ? 42.0 : 48.0; |
| } else { |
| assert(isOneLine); |
| } |
| |
| final double defaultTileHeight = _defaultTileHeight; |
| |
| double tileHeight; |
| double titleY; |
| double? subtitleY; |
| if (!hasSubtitle) { |
| tileHeight = math.max(defaultTileHeight, titleSize.height + 2.0 * _minVerticalPadding); |
| titleY = (tileHeight - titleSize.height) / 2.0; |
| } else { |
| assert(subtitleBaselineType != null); |
| titleY = titleBaseline! - _boxBaseline(title!, titleBaselineType)!; |
| subtitleY = subtitleBaseline! - _boxBaseline(subtitle!, subtitleBaselineType!)! + visualDensity.vertical * 2.0; |
| tileHeight = defaultTileHeight; |
| |
| // If the title and subtitle overlap, move the title upwards by half |
| // the overlap and the subtitle down by the same amount, and adjust |
| // tileHeight so that both titles fit. |
| final double titleOverlap = titleY + titleSize.height - subtitleY; |
| if (titleOverlap > 0.0) { |
| titleY -= titleOverlap / 2.0; |
| subtitleY += titleOverlap / 2.0; |
| } |
| |
| // If the title or subtitle overflow tileHeight then punt: title |
| // and subtitle are arranged in a column, tileHeight = column height plus |
| // _minVerticalPadding on top and bottom. |
| if (titleY < _minVerticalPadding || |
| (subtitleY + subtitleSize.height + _minVerticalPadding) > tileHeight) { |
| tileHeight = titleSize.height + subtitleSize.height + 2.0 * _minVerticalPadding; |
| titleY = _minVerticalPadding; |
| subtitleY = titleSize.height + _minVerticalPadding; |
| } |
| } |
| |
| final double leadingY; |
| final double trailingY; |
| |
| switch (titleAlignment) { |
| case ListTileTitleAlignment.threeLine: { |
| if (isThreeLine) { |
| leadingY = _minVerticalPadding; |
| trailingY = _minVerticalPadding; |
| } else { |
| leadingY = (tileHeight - leadingSize.height) / 2.0; |
| trailingY = (tileHeight - trailingSize.height) / 2.0; |
| } |
| break; |
| } |
| case ListTileTitleAlignment.titleHeight: { |
| // 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 |
| // The interpretation for these redlines is as follows: |
| // - For large tiles (> 72dp), both leading and trailing controls should be |
| // a fixed distance from top. As per guidelines this is set to 16dp. |
| // - 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. |
| if (tileHeight > 72.0) { |
| leadingY = 16.0; |
| trailingY = 16.0; |
| } else { |
| leadingY = math.min((tileHeight - leadingSize.height) / 2.0, 16.0); |
| trailingY = (tileHeight - trailingSize.height) / 2.0; |
| } |
| break; |
| } |
| case ListTileTitleAlignment.top: { |
| leadingY = _minVerticalPadding; |
| trailingY = _minVerticalPadding; |
| break; |
| } |
| case ListTileTitleAlignment.center: { |
| leadingY = (tileHeight - leadingSize.height) / 2.0; |
| trailingY = (tileHeight - trailingSize.height) / 2.0; |
| break; |
| } |
| case ListTileTitleAlignment.bottom: { |
| leadingY = tileHeight - leadingSize.height - _minVerticalPadding; |
| trailingY = tileHeight - trailingSize.height - _minVerticalPadding; |
| break; |
| } |
| } |
| |
| switch (textDirection) { |
| case TextDirection.rtl: { |
| if (hasLeading) { |
| _positionBox(leading!, Offset(tileWidth - leadingSize.width, leadingY)); |
| } |
| _positionBox(title!, Offset(adjustedTrailingWidth, titleY)); |
| if (hasSubtitle) { |
| _positionBox(subtitle!, Offset(adjustedTrailingWidth, subtitleY!)); |
| } |
| if (hasTrailing) { |
| _positionBox(trailing!, Offset(0.0, trailingY)); |
| } |
| break; |
| } |
| case TextDirection.ltr: { |
| if (hasLeading) { |
| _positionBox(leading!, Offset(0.0, leadingY)); |
| } |
| _positionBox(title!, Offset(titleStart, titleY)); |
| if (hasSubtitle) { |
| _positionBox(subtitle!, Offset(titleStart, subtitleY!)); |
| } |
| if (hasTrailing) { |
| _positionBox(trailing!, Offset(tileWidth - trailingSize.width, trailingY)); |
| } |
| break; |
| } |
| } |
| |
| size = constraints.constrain(Size(tileWidth, tileHeight)); |
| assert(size.width == constraints.constrainWidth(tileWidth)); |
| assert(size.height == constraints.constrainHeight(tileHeight)); |
| } |
| |
| @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!) { |
| case ListTileStyle.drawer: |
| return _textTheme.bodyLarge; |
| case ListTileStyle.list: |
| return _textTheme.titleMedium; |
| } |
| } |
| |
| @override |
| TextStyle? get subtitleTextStyle => _textTheme.bodyMedium; |
| |
| @override |
| TextStyle? get leadingAndTrailingTextStyle => _textTheme.bodyMedium; |
| |
| @override |
| Color? get selectedColor => _theme.colorScheme.primary; |
| |
| @override |
| Color? get iconColor { |
| switch (_theme.brightness) { |
| case Brightness.light: |
| // For the sake of backwards compatibility, the default for unselected |
| // tiles is Colors.black45 rather than colorScheme.onSurface.withAlpha(0x73). |
| return Colors.black45; |
| case Brightness.dark: |
| return null; // null, Use current icon theme color |
| } |
| } |
| } |
| |
| // 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. |
| |
| // Token database version: v0_162 |
| |
| 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; |
| |
| @override |
| TextStyle? get subtitleTextStyle => _textTheme.bodyMedium; |
| |
| @override |
| TextStyle? get leadingAndTrailingTextStyle => _textTheme.labelSmall; |
| |
| @override |
| Color? get selectedColor => _colors.primary; |
| |
| @override |
| Color? get iconColor => _colors.onSurfaceVariant; |
| } |
| |
| // END GENERATED TOKEN PROPERTIES - LisTile |