list_tile.dart 57.8 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
Adam Barth's avatar
Adam Barth committed
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5 6 7
import 'dart:math' as math;

import 'package:flutter/rendering.dart';
8
import 'package:flutter/widgets.dart';
Adam Barth's avatar
Adam Barth committed
9

10
import 'color_scheme.dart';
11
import 'colors.dart';
12
import 'constants.dart';
13
import 'debug.dart';
14
import 'divider.dart';
15 16
import 'icon_button.dart';
import 'icon_button_theme.dart';
17
import 'ink_decoration.dart';
Adam Barth's avatar
Adam Barth committed
18
import 'ink_well.dart';
19
import 'list_tile_theme.dart';
20
import 'material_state.dart';
21
import 'text_theme.dart';
Hans Muller's avatar
Hans Muller committed
22
import 'theme.dart';
23
import 'theme_data.dart';
Adam Barth's avatar
Adam Barth committed
24

25 26 27
// Examples can assume:
// int _act = 1;

28 29
/// Defines the title font used for [ListTile] descendants of a [ListTileTheme].
///
30 31
/// 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]
32 33
/// text style, which is used by default.
enum ListTileStyle {
Adam Barth's avatar
Adam Barth committed
34
  /// Use a title font that's appropriate for a [ListTile] in a list.
35 36
  list,

Adam Barth's avatar
Adam Barth committed
37
  /// Use a title font that's appropriate for a [ListTile] that appears in a [Drawer].
38
  drawer,
39 40
}

41 42
/// Where to place the control in widgets that use [ListTile] to position a
/// control next to a label.
43
///
44
/// See also:
45
///
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
///  * [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,
}

66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108
/// 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,
}

109 110
/// A single fixed-height row that typically contains some text as well as
/// a leading or trailing icon.
111
///
112 113
/// {@youtube 560 315 https://www.youtube.com/watch?v=l8dj0yPBvgQ}
///
114
/// A list tile contains one to three lines of text optionally flanked by icons or
115
/// other widgets, such as check boxes. The icons (or other widgets) for the
116
/// tile are defined with the [leading] and [trailing] parameters. The first
117 118 119
/// 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]
120
/// is true then the overall height of this tile and the size of the
121 122
/// [DefaultTextStyle]s that wrap the [title] and [subtitle] widget are reduced.
///
123 124 125
/// 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).
126
///
127
/// The heights of the [leading] and [trailing] widgets are constrained
128 129
/// according to the
/// [Material spec](https://material.io/design/components/lists.html).
130 131 132 133
/// 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.
///
134
/// The [leading] and [trailing] widgets can expand as far as they wish
135 136
/// horizontally, so ensure that they are properly constrained.
///
137 138
/// List tiles are typically used in [ListView]s, or arranged in [Column]s in
/// [Drawer]s and [Card]s.
139
///
140 141 142 143 144 145
/// 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.:
146
///
147
/// {@tool snippet}
148
/// ```dart
149
/// const ColoredBox(
150
///   color: Colors.green,
151
///   child: Material(
152
///     child: ListTile(
153
///       title: Text('ListTile with red background'),
154 155 156 157 158
///       tileColor: Colors.red,
///     ),
///   ),
/// )
/// ```
159 160 161 162 163 164 165
/// {@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.
166
///
167 168 169 170 171 172 173 174 175 176
/// [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}
177
///
178
/// {@tool dartpad}
179 180 181 182
/// 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)
183
///
184
/// ** See code in examples/api/lib/material/list_tile/list_tile.1.dart **
185
/// {@end-tool}
186
///
187 188 189 190 191 192
/// {@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}
193
///
194 195 196 197 198 199 200 201
/// {@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}
///
202 203 204 205 206 207 208 209
/// {@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}
///
210
/// {@tool snippet}
Aayan's avatar
Aayan committed
211 212 213 214 215
/// 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
216 217
/// const Row(
///   children: <Widget>[
Aayan's avatar
Aayan committed
218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235
///     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}
///
236 237 238 239 240
/// 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
241
/// ListTile(
242
///   leading: const Icon(Icons.flight_land),
243
///   title: const Text("Trix's airplane"),
244 245 246 247 248
///   subtitle: _act != 2 ? const Text('The airplane is only in Act II.') : null,
///   enabled: _act == 2,
///   onTap: () { /* react to the tile being tapped */ }
/// )
/// ```
249
/// {@end-tool}
250
///
251 252 253 254 255 256 257 258 259 260 261 262
/// 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.
///
263
/// {@tool snippet}
264 265
///
/// Here is an example of a one-line, non-[dense] ListTile with a
266 267 268 269 270 271 272 273 274 275 276 277
/// 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,
278
///       padding: const EdgeInsets.symmetric(vertical: 4.0),
279
///       alignment: Alignment.center,
280
///       child: const CircleAvatar(),
281 282
///     ),
///   ),
283
///   title: const Text('title'),
284
///   dense: false,
285
/// )
286 287 288
/// ```
/// {@end-tool}
///
289 290 291 292 293 294
/// ## 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.
///
295
/// {@tool dartpad}
296
/// Here is an example of a custom list item that resembles a YouTube-related
297 298 299 300
/// 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)
///
301
/// ** See code in examples/api/lib/material/list_tile/custom_list_item.0.dart **
302 303
/// {@end-tool}
///
304
/// {@tool dartpad}
305
/// Here is an example of an article list item with multiline titles and
306 307 308 309 310
/// 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)
///
311
/// ** See code in examples/api/lib/material/list_tile/custom_list_item.1.dart **
312 313
/// {@end-tool}
///
314
/// See also:
315
///
316
///  * [ListTileTheme], which defines visual properties for [ListTile]s.
317 318 319 320
///  * [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.
321
///  * [Card], which can be used with [Column] to show a few [ListTile]s.
322
///  * [Divider], which can be used to separate [ListTile]s.
323
///  * [ListTile.divideTiles], a utility for inserting [Divider]s in between [ListTile]s.
324 325
///  * [CheckboxListTile], [RadioListTile], and [SwitchListTile], widgets
///    that combine [ListTile] with other controls.
326 327
///  * 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>
328 329
///  * Cookbook: [Use lists](https://flutter.dev/docs/cookbook/lists/basic-list)
///  * Cookbook: [Implement swipe to dismiss](https://flutter.dev/docs/cookbook/gestures/dismissible)
330 331
class ListTile extends StatelessWidget {
  /// Creates a list tile.
332 333 334 335
  ///
  /// If [isThreeLine] is true, then [subtitle] must not be null.
  ///
  /// Requires one of its ancestors to be a [Material] widget.
336
  const ListTile({
337
    super.key,
338 339 340 341
    this.leading,
    this.title,
    this.subtitle,
    this.trailing,
342
    this.isThreeLine = false,
343
    this.dense,
344
    this.visualDensity,
345
    this.shape,
346 347 348 349
    this.style,
    this.selectedColor,
    this.iconColor,
    this.textColor,
350 351 352
    this.titleTextStyle,
    this.subtitleTextStyle,
    this.leadingAndTrailingTextStyle,
353
    this.contentPadding,
354
    this.enabled = true,
Adam Barth's avatar
Adam Barth committed
355
    this.onTap,
356
    this.onLongPress,
357
    this.onFocusChange,
358
    this.mouseCursor,
359
    this.selected = false,
360 361
    this.focusColor,
    this.hoverColor,
362
    this.splashColor,
363
    this.focusNode,
364
    this.autofocus = false,
365 366
    this.tileColor,
    this.selectedTileColor,
367
    this.enableFeedback,
368 369 370
    this.horizontalTitleGap,
    this.minVerticalPadding,
    this.minLeadingWidth,
371
    this.titleAlignment,
372
  }) : assert(!isThreeLine || subtitle != null);
Adam Barth's avatar
Adam Barth committed
373

374 375
  /// A widget to display before the title.
  ///
376
  /// Typically an [Icon] or a [CircleAvatar] widget.
377
  final Widget? leading;
378

379
  /// The primary content of the list tile.
380 381
  ///
  /// Typically a [Text] widget.
382
  ///
383 384
  /// This should not wrap. To enforce the single line limit, use
  /// [Text.maxLines].
385
  final Widget? title;
386 387 388 389

  /// Additional content displayed below the title.
  ///
  /// Typically a [Text] widget.
390 391 392 393
  ///
  /// If [isThreeLine] is false, this should not wrap.
  ///
  /// If [isThreeLine] is true, this should be configured to take a maximum of
394 395
  /// two lines. For example, you can use [Text.maxLines] to enforce the number
  /// of lines.
396
  ///
397
  /// The subtitle's default [TextStyle] depends on [TextTheme.bodyMedium] except
398 399 400 401 402 403
  /// [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]
404
  /// if it's not null and to [TextTheme.bodySmall]'s color if [ListTileTheme.textColor]
405
  /// is null.
406
  final Widget? subtitle;
407 408 409 410

  /// A widget to display after the title.
  ///
  /// Typically an [Icon] widget.
411 412 413
  ///
  /// To show right-aligned metadata (assuming left-to-right reading order;
  /// left-aligned for right-to-left reading order), consider using a [Row] with
414
  /// [CrossAxisAlignment.baseline] alignment whose first item is [Expanded] and
415 416
  /// whose second child is the metadata text, instead of using the [trailing]
  /// property.
417
  final Widget? trailing;
418

419
  /// Whether this list tile is intended to display three lines of text.
420
  ///
421 422 423
  /// If true, then [subtitle] must be non-null (since it is expected to give
  /// the second and third lines of text).
  ///
424
  /// If false, the list tile is treated as having one line if the subtitle is
425
  /// null and treated as having two lines if the subtitle is non-null.
426 427 428
  ///
  /// When using a [Text] widget for [title] and [subtitle], you can enforce
  /// line limits using [Text.maxLines].
Hans Muller's avatar
Hans Muller committed
429
  final bool isThreeLine;
430

431
  /// Whether this list tile is part of a vertically dense list.
432 433
  ///
  /// If this property is null then its value is based on [ListTileTheme.dense].
434 435
  ///
  /// Dense list tiles default to a smaller height.
436 437
  ///
  /// It is not recommended to set [dense] to true when [ThemeData.useMaterial3] is true.
438
  final bool? dense;
439

440 441 442 443 444 445
  /// Defines how compact the list tile's layout will be.
  ///
  /// {@macro flutter.material.themedata.visualDensity}
  ///
  /// See also:
  ///
446 447
  ///  * [ThemeData.visualDensity], which specifies the [visualDensity] for all
  ///    widgets within a [Theme].
448
  final VisualDensity? visualDensity;
449

450
  /// {@template flutter.material.ListTile.shape}
451
  /// Defines the tile's [InkWell.customBorder] and [Ink.decoration] shape.
452 453 454 455 456 457
  /// {@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:
458
  ///
459 460
  /// * [ListTileTheme.of], which returns the nearest [ListTileTheme]'s
  ///   [ListTileThemeData].
461
  final ShapeBorder? shape;
462

463 464 465 466 467 468 469 470 471 472 473 474 475
  /// 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.
  ///
476
  /// If this property is null and [selected] is false then [ListTileThemeData.iconColor]
477
  /// is used. If that is also null and [ThemeData.useMaterial3] is true, [ColorScheme.onSurfaceVariant]
478 479 480 481 482 483 484 485
  /// 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.
486 487 488 489 490 491 492
  ///
  /// See also:
  ///
  /// * [ListTileTheme.of], which returns the nearest [ListTileTheme]'s
  ///   [ListTileThemeData].
  final Color? iconColor;

493 494 495 496 497 498 499 500 501
  /// 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.
502
  ///
503 504
  /// If this color is a [MaterialStateColor] it will be resolved against
  /// [MaterialState.selected] and [MaterialState.disabled] states.
505 506 507 508 509 510 511
  ///
  /// See also:
  ///
  /// * [ListTileTheme.of], which returns the nearest [ListTileTheme]'s
  ///   [ListTileThemeData].
  final Color? textColor;

512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533
  /// 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;

534 535 536 537 538 539 540 541 542 543 544
  /// 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;

545 546 547 548 549 550
  /// 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.
551
  final EdgeInsetsGeometry? contentPadding;
552

553
  /// Whether this list tile is interactive.
554
  ///
555
  /// If false, this list tile is styled with the disabled color from the
556 557
  /// current [Theme] and the [onTap] and [onLongPress] callbacks are
  /// inoperative.
558
  final bool enabled;
559

560
  /// Called when the user taps this list tile.
561 562
  ///
  /// Inoperative if [enabled] is false.
563
  final GestureTapCallback? onTap;
564

565
  /// Called when the user long-presses on this list tile.
566 567
  ///
  /// Inoperative if [enabled] is false.
568
  final GestureLongPressCallback? onLongPress;
Adam Barth's avatar
Adam Barth committed
569

570 571 572
  /// {@macro flutter.material.inkwell.onFocusChange}
  final ValueChanged<bool>? onFocusChange;

573
  /// {@template flutter.material.ListTile.mouseCursor}
574 575 576 577 578 579 580 581
  /// 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].
582
  /// {@endtemplate}
583
  ///
584 585 586 587 588 589 590
  /// 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>].
591
  final MouseCursor? mouseCursor;
592

593 594 595 596
  /// 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].
597
  ///
598
  /// {@tool dartpad}
599
  /// Here is an example of using a [StatefulWidget] to keep track of the
600
  /// selected index, and using that to set the [selected] property on the
601 602
  /// corresponding [ListTile].
  ///
603
  /// ** See code in examples/api/lib/material/list_tile/list_tile.selected.0.dart **
604
  /// {@end-tool}
605 606
  final bool selected;

607
  /// The color for the tile's [Material] when it has the input focus.
608
  final Color? focusColor;
609 610

  /// The color for the tile's [Material] when a pointer is hovering over it.
611
  final Color? hoverColor;
612

613 614 615
  /// The color of splash for the tile's [Material].
  final Color? splashColor;

616
  /// {@macro flutter.widgets.Focus.focusNode}
617
  final FocusNode? focusNode;
618

619 620 621
  /// {@macro flutter.widgets.Focus.autofocus}
  final bool autofocus;

622
  /// {@template flutter.material.ListTile.tileColor}
623
  /// Defines the background color of `ListTile` when [selected] is false.
624
  ///
625 626 627 628 629
  /// 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.
  ///
630
  /// {@endtemplate}
631
  final Color? tileColor;
632 633 634

  /// Defines the background color of `ListTile` when [selected] is true.
  ///
635
  /// When the value if null, the [selectedTileColor] is set to [ListTileTheme.selectedTileColor]
636
  /// if it's not null and to [Colors.transparent] if it's null.
637
  final Color? selectedTileColor;
638

639
  /// {@template flutter.material.ListTile.enableFeedback}
640 641 642 643 644
  /// 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.
  ///
645 646 647
  /// When null, the default value is true.
  /// {@endtemplate}
  ///
648 649 650 651 652
  /// See also:
  ///
  ///  * [Feedback] for providing platform-specific feedback to certain actions.
  final bool? enableFeedback;

653
  /// The horizontal gap between the titles and the leading/trailing widgets.
654 655 656 657
  ///
  /// 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;
658 659

  /// The minimum padding on the top and bottom of the title and subtitle widgets.
660 661 662 663
  ///
  /// 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;
664

665 666 667 668 669
  /// 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;
670

671 672 673 674 675 676 677 678 679 680 681 682 683 684
  /// 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;

685
  /// Add a one pixel border in between each tile. If color isn't specified the
686 687 688 689
  /// [ThemeData.dividerColor] of the context's [Theme] is used.
  ///
  /// See also:
  ///
690
  ///  * [Divider], which you can use to obtain this effect manually.
691
  static Iterable<Widget> divideTiles({ BuildContext? context, required Iterable<Widget> tiles, Color? color }) {
Hans Muller's avatar
Hans Muller committed
692
    assert(color != null || context != null);
693
    tiles = tiles.toList();
Hans Muller's avatar
Hans Muller committed
694

695 696 697
    if (tiles.isEmpty || tiles.length == 1) {
      return tiles;
    }
698

699 700
    Widget wrapTile(Widget tile) {
      return DecoratedBox(
701
        position: DecorationPosition.foreground,
702 703 704 705 706
        decoration: BoxDecoration(
          border: Border(
            bottom: Divider.createBorderSide(context, color: color),
          ),
        ),
707
        child: tile,
Hans Muller's avatar
Hans Muller committed
708 709
      );
    }
710 711 712 713 714

    return <Widget>[
      ...tiles.take(tiles.length - 1).map(wrapTile),
      tiles.last,
    ];
Hans Muller's avatar
Hans Muller committed
715 716
  }

717
  bool _isDenseLayout(ThemeData theme, ListTileThemeData tileTheme) {
718 719 720
    return dense ?? tileTheme.dense ?? theme.listTileTheme.dense ?? false;
  }

721
  Color _tileBackgroundColor(ThemeData theme, ListTileThemeData tileTheme, ListTileThemeData defaults) {
722 723 724
    final Color? color = selected
      ? selectedTileColor ?? tileTheme.selectedTileColor ?? theme.listTileTheme.selectedTileColor
      : tileColor ?? tileTheme.tileColor ?? theme.listTileTheme.tileColor;
725
    return color ?? defaults.tileColor!;
726 727
  }

728
  @override
Adam Barth's avatar
Adam Barth committed
729
  Widget build(BuildContext context) {
730
    assert(debugCheckHasMaterial(context));
731
    final ThemeData theme = Theme.of(context);
732
    final ListTileThemeData tileTheme = ListTileTheme.of(context);
733 734 735 736 737 738 739 740 741 742 743
    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,
    };
744

745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762
    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);
763 764 765
    final IconButtonThemeData iconButtonThemeData = IconButtonThemeData(
      style: IconButton.styleFrom(foregroundColor: effectiveIconColor),
    );
766 767

    TextStyle? leadingAndTrailingStyle;
768
    if (leading != null || trailing != null) {
769 770 771 772 773
      leadingAndTrailingStyle = leadingAndTrailingTextStyle
        ?? tileTheme.leadingAndTrailingTextStyle
        ?? defaults.leadingAndTrailingTextStyle!;
      final Color? leadingAndTrailingTextColor = effectiveColor;
      leadingAndTrailingStyle = leadingAndTrailingStyle.copyWith(color: leadingAndTrailingTextColor);
774
    }
775

776
    Widget? leadingIcon;
777
    if (leading != null) {
778
      leadingIcon = AnimatedDefaultTextStyle(
779
        style: leadingAndTrailingStyle!,
780
        duration: kThemeChangeDuration,
781
        child: leading!,
782
      );
Adam Barth's avatar
Adam Barth committed
783 784
    }

785 786 787 788 789 790 791 792
    TextStyle titleStyle = titleTextStyle
      ?? tileTheme.titleTextStyle
      ?? defaults.titleTextStyle!;
    final Color? titleColor = effectiveColor;
    titleStyle = titleStyle.copyWith(
      color: titleColor,
      fontSize: _isDenseLayout(theme, tileTheme) ? 13.0 : null,
    );
793
    final Widget titleText = AnimatedDefaultTextStyle(
794
      style: titleStyle,
795
      duration: kThemeChangeDuration,
796
      child: title ?? const SizedBox(),
Hans Muller's avatar
Hans Muller committed
797
    );
798

799 800
    Widget? subtitleText;
    TextStyle? subtitleStyle;
801
    if (subtitle != null) {
802 803 804 805 806 807 808 809
      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,
      );
810
      subtitleText = AnimatedDefaultTextStyle(
811
        style: subtitleStyle,
812
        duration: kThemeChangeDuration,
813
        child: subtitle!,
Hans Muller's avatar
Hans Muller committed
814 815
      );
    }
Adam Barth's avatar
Adam Barth committed
816

817
    Widget? trailingIcon;
818
    if (trailing != null) {
819
      trailingIcon = AnimatedDefaultTextStyle(
820
        style: leadingAndTrailingStyle!,
821
        duration: kThemeChangeDuration,
822
        child: trailing!,
823
      );
Adam Barth's avatar
Adam Barth committed
824 825
    }

826
    final TextDirection textDirection = Directionality.of(context);
827
    final EdgeInsets resolvedContentPadding = contentPadding?.resolve(textDirection)
828
      ?? tileTheme.contentPadding?.resolve(textDirection)
829
      ?? defaults.contentPadding!.resolve(textDirection);
830

831 832
    // Show basic cursor when ListTile isn't enabled or gesture callbacks are null.
    final Set<MaterialState> mouseStates = <MaterialState>{
833 834
      if (!enabled || (onTap == null && onLongPress == null)) MaterialState.disabled,
    };
835 836 837
    final MouseCursor effectiveMouseCursor = MaterialStateProperty.resolveAs<MouseCursor?>(mouseCursor, mouseStates)
      ?? tileTheme.mouseCursor?.resolve(mouseStates)
      ?? MaterialStateMouseCursor.clickable.resolve(mouseStates);
838

839 840 841 842
    final ListTileTitleAlignment effectiveTitleAlignment = titleAlignment
      ?? tileTheme.titleAlignment
      ?? (theme.useMaterial3 ? ListTileTitleAlignment.threeLine : ListTileTitleAlignment.titleHeight);

843
    return InkWell(
844
      customBorder: shape ?? tileTheme.shape,
845 846
      onTap: enabled ? onTap : null,
      onLongPress: enabled ? onLongPress : null,
847
      onFocusChange: onFocusChange,
848
      mouseCursor: effectiveMouseCursor,
849
      canRequestFocus: enabled,
850 851 852
      focusNode: focusNode,
      focusColor: focusColor,
      hoverColor: hoverColor,
853
      splashColor: splashColor,
854
      autofocus: autofocus,
855
      enableFeedback: enableFeedback ?? tileTheme.enableFeedback ?? true,
856
      child: Semantics(
857
        selected: selected,
858
        enabled: enabled,
859 860 861
        child: Ink(
          decoration: ShapeDecoration(
            shape: shape ?? tileTheme.shape ?? const Border(),
862
            color: _tileBackgroundColor(theme, tileTheme, defaults),
863
          ),
864 865 866 867
          child: SafeArea(
            top: false,
            bottom: false,
            minimum: resolvedContentPadding,
868 869
            child: IconTheme.merge(
              data: iconThemeData,
870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885
              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!,
886
                  titleAlignment: effectiveTitleAlignment,
887
                ),
888
              ),
889
            ),
890
          ),
891
       ),
892
      ),
Adam Barth's avatar
Adam Barth committed
893 894
    );
  }
895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910

  @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));
911 912 913
    properties.add(DiagnosticsProperty<TextStyle>('titleTextStyle', titleTextStyle, defaultValue: null));
    properties.add(DiagnosticsProperty<TextStyle>('subtitleTextStyle', subtitleTextStyle, defaultValue: null));
    properties.add(DiagnosticsProperty<TextStyle>('leadingAndTrailingTextStyle', leadingAndTrailingTextStyle, defaultValue: null));
914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929
    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));
930
    properties.add(DiagnosticsProperty<ListTileTitleAlignment>('titleAlignment', titleAlignment, defaultValue: null));
931
  }
Adam Barth's avatar
Adam Barth committed
932
}
933

934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961
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;
  }
}

962 963 964 965 966 967 968 969
// Identifies the children of a _ListTileElement.
enum _ListTileSlot {
  leading,
  title,
  subtitle,
  trailing,
}

970
class _ListTile extends RenderObjectWidget with SlottedMultiChildRenderObjectWidgetMixin<_ListTileSlot> {
971 972
  const _ListTile({
    this.leading,
973
    required this.title,
974 975
    this.subtitle,
    this.trailing,
976 977 978 979 980
    required this.isThreeLine,
    required this.isDense,
    required this.visualDensity,
    required this.textDirection,
    required this.titleBaselineType,
981 982 983
    required this.horizontalTitleGap,
    required this.minVerticalPadding,
    required this.minLeadingWidth,
984
    this.subtitleBaselineType,
985
    required this.titleAlignment,
986
  });
987

988
  final Widget? leading;
989
  final Widget title;
990 991
  final Widget? subtitle;
  final Widget? trailing;
992 993
  final bool isThreeLine;
  final bool isDense;
994
  final VisualDensity visualDensity;
995 996
  final TextDirection textDirection;
  final TextBaseline titleBaselineType;
997
  final TextBaseline? subtitleBaselineType;
998 999 1000
  final double horizontalTitleGap;
  final double minVerticalPadding;
  final double minLeadingWidth;
1001
  final ListTileTitleAlignment titleAlignment;
1002 1003

  @override
1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018
  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;
    }
  }
1019 1020 1021

  @override
  _RenderListTile createRenderObject(BuildContext context) {
1022
    return _RenderListTile(
1023 1024
      isThreeLine: isThreeLine,
      isDense: isDense,
1025
      visualDensity: visualDensity,
1026 1027 1028
      textDirection: textDirection,
      titleBaselineType: titleBaselineType,
      subtitleBaselineType: subtitleBaselineType,
1029 1030 1031
      horizontalTitleGap: horizontalTitleGap,
      minVerticalPadding: minVerticalPadding,
      minLeadingWidth: minLeadingWidth,
1032
      titleAlignment: titleAlignment,
1033 1034 1035 1036 1037 1038 1039 1040
    );
  }

  @override
  void updateRenderObject(BuildContext context, _RenderListTile renderObject) {
    renderObject
      ..isThreeLine = isThreeLine
      ..isDense = isDense
1041
      ..visualDensity = visualDensity
1042 1043
      ..textDirection = textDirection
      ..titleBaselineType = titleBaselineType
1044 1045 1046
      ..subtitleBaselineType = subtitleBaselineType
      ..horizontalTitleGap = horizontalTitleGap
      ..minLeadingWidth = minLeadingWidth
1047
      ..minVerticalPadding = minVerticalPadding
1048
      ..titleAlignment = titleAlignment;
1049 1050 1051
  }
}

1052
class _RenderListTile extends RenderBox with SlottedContainerRenderObjectMixin<_ListTileSlot> {
1053
  _RenderListTile({
1054 1055 1056 1057 1058 1059
    required bool isDense,
    required VisualDensity visualDensity,
    required bool isThreeLine,
    required TextDirection textDirection,
    required TextBaseline titleBaselineType,
    TextBaseline? subtitleBaselineType,
1060 1061 1062
    required double horizontalTitleGap,
    required double minVerticalPadding,
    required double minLeadingWidth,
1063
    required ListTileTitleAlignment titleAlignment,
1064
  }) : _isDense = isDense,
1065
       _visualDensity = visualDensity,
1066
       _isThreeLine = isThreeLine,
1067 1068
       _textDirection = textDirection,
       _titleBaselineType = titleBaselineType,
1069
       _subtitleBaselineType = subtitleBaselineType,
1070
       _horizontalTitleGap = horizontalTitleGap,
1071
       _minVerticalPadding = minVerticalPadding,
1072
       _minLeadingWidth = minLeadingWidth,
1073
       _titleAlignment = titleAlignment;
1074

1075 1076 1077 1078
  RenderBox? get leading => childForSlot(_ListTileSlot.leading);
  RenderBox? get title => childForSlot(_ListTileSlot.title);
  RenderBox? get subtitle => childForSlot(_ListTileSlot.subtitle);
  RenderBox? get trailing => childForSlot(_ListTileSlot.trailing);
1079 1080

  // The returned list is ordered for hit testing.
1081
  @override
1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092
  Iterable<RenderBox> get children {
    return <RenderBox>[
      if (leading != null)
        leading!,
      if (title != null)
        title!,
      if (subtitle != null)
        subtitle!,
      if (trailing != null)
        trailing!,
    ];
1093 1094 1095 1096 1097
  }

  bool get isDense => _isDense;
  bool _isDense;
  set isDense(bool value) {
1098
    if (_isDense == value) {
1099
      return;
1100
    }
1101 1102 1103 1104
    _isDense = value;
    markNeedsLayout();
  }

1105 1106 1107
  VisualDensity get visualDensity => _visualDensity;
  VisualDensity _visualDensity;
  set visualDensity(VisualDensity value) {
1108
    if (_visualDensity == value) {
1109
      return;
1110
    }
1111 1112 1113 1114
    _visualDensity = value;
    markNeedsLayout();
  }

1115 1116 1117
  bool get isThreeLine => _isThreeLine;
  bool _isThreeLine;
  set isThreeLine(bool value) {
1118
    if (_isThreeLine == value) {
1119
      return;
1120
    }
1121 1122 1123 1124 1125 1126 1127
    _isThreeLine = value;
    markNeedsLayout();
  }

  TextDirection get textDirection => _textDirection;
  TextDirection _textDirection;
  set textDirection(TextDirection value) {
1128
    if (_textDirection == value) {
1129
      return;
1130
    }
1131 1132 1133 1134
    _textDirection = value;
    markNeedsLayout();
  }

1135 1136 1137
  TextBaseline get titleBaselineType => _titleBaselineType;
  TextBaseline _titleBaselineType;
  set titleBaselineType(TextBaseline value) {
1138
    if (_titleBaselineType == value) {
1139
      return;
1140
    }
1141 1142 1143 1144
    _titleBaselineType = value;
    markNeedsLayout();
  }

1145 1146 1147
  TextBaseline? get subtitleBaselineType => _subtitleBaselineType;
  TextBaseline? _subtitleBaselineType;
  set subtitleBaselineType(TextBaseline? value) {
1148
    if (_subtitleBaselineType == value) {
1149
      return;
1150
    }
1151 1152 1153 1154
    _subtitleBaselineType = value;
    markNeedsLayout();
  }

1155 1156
  double get horizontalTitleGap => _horizontalTitleGap;
  double _horizontalTitleGap;
1157
  double get _effectiveHorizontalTitleGap => _horizontalTitleGap + visualDensity.horizontal * 2.0;
1158 1159

  set horizontalTitleGap(double value) {
1160
    if (_horizontalTitleGap == value) {
1161
      return;
1162
    }
1163 1164 1165 1166 1167 1168 1169 1170
    _horizontalTitleGap = value;
    markNeedsLayout();
  }

  double get minVerticalPadding => _minVerticalPadding;
  double _minVerticalPadding;

  set minVerticalPadding(double value) {
1171
    if (_minVerticalPadding == value) {
1172
      return;
1173
    }
1174 1175 1176 1177 1178 1179 1180 1181
    _minVerticalPadding = value;
    markNeedsLayout();
  }

  double get minLeadingWidth => _minLeadingWidth;
  double _minLeadingWidth;

  set minLeadingWidth(double value) {
1182
    if (_minLeadingWidth == value) {
1183
      return;
1184
    }
1185 1186 1187 1188
    _minLeadingWidth = value;
    markNeedsLayout();
  }

1189 1190 1191 1192
  ListTileTitleAlignment get titleAlignment => _titleAlignment;
  ListTileTitleAlignment _titleAlignment;
  set titleAlignment(ListTileTitleAlignment value) {
    if (_titleAlignment == value) {
1193 1194
      return;
    }
1195
    _titleAlignment = value;
1196 1197 1198
    markNeedsLayout();
  }

1199 1200 1201
  @override
  bool get sizedByParent => false;

1202
  static double _minWidth(RenderBox? box, double height) {
1203 1204 1205
    return box == null ? 0.0 : box.getMinIntrinsicWidth(height);
  }

1206
  static double _maxWidth(RenderBox? box, double height) {
1207 1208 1209 1210 1211 1212
    return box == null ? 0.0 : box.getMaxIntrinsicWidth(height);
  }

  @override
  double computeMinIntrinsicWidth(double height) {
    final double leadingWidth = leading != null
1213
      ? math.max(leading!.getMinIntrinsicWidth(height), _minLeadingWidth) + _effectiveHorizontalTitleGap
1214 1215 1216 1217 1218 1219 1220 1221 1222
      : 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
1223
      ? math.max(leading!.getMaxIntrinsicWidth(height), _minLeadingWidth) + _effectiveHorizontalTitleGap
1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234
      : 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;

1235
    final Offset baseDensity = visualDensity.baseSizeAdjustment;
1236
    if (isOneLine) {
1237
      return (isDense ? 48.0 : 56.0) + baseDensity.dy;
1238 1239
    }
    if (isTwoLine) {
1240
      return (isDense ? 64.0 : 72.0) + baseDensity.dy;
1241
    }
1242
    return (isDense ? 76.0 : 88.0) + baseDensity.dy;
1243 1244 1245 1246 1247 1248
  }

  @override
  double computeMinIntrinsicHeight(double width) {
    return math.max(
      _defaultTileHeight,
1249
      title!.getMinIntrinsicHeight(width) + (subtitle?.getMinIntrinsicHeight(width) ?? 0.0),
1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260
    );
  }

  @override
  double computeMaxIntrinsicHeight(double width) {
    return computeMinIntrinsicHeight(width);
  }

  @override
  double computeDistanceToActualBaseline(TextBaseline baseline) {
    assert(title != null);
1261 1262
    final BoxParentData parentData = title!.parentData! as BoxParentData;
    return parentData.offset.dy + title!.getDistanceToActualBaseline(baseline)!;
1263 1264
  }

1265
  static double? _boxBaseline(RenderBox box, TextBaseline baseline) {
1266
    return box.getDistanceToBaseline(baseline);
1267 1268
  }

1269
  static Size _layoutBox(RenderBox? box, BoxConstraints constraints) {
1270
    if (box == null) {
1271
      return Size.zero;
1272
    }
1273 1274 1275 1276 1277
    box.layout(constraints, parentUsesSize: true);
    return box.size;
  }

  static void _positionBox(RenderBox box, Offset offset) {
1278
    final BoxParentData parentData = box.parentData! as BoxParentData;
1279 1280 1281
    parentData.offset = offset;
  }

1282 1283 1284
  @override
  Size computeDryLayout(BoxConstraints constraints) {
    assert(debugCannotComputeDryLayout(
1285
      reason: 'Layout requires baseline metrics, which are only available after a full layout.',
1286
    ));
1287
    return Size.zero;
1288 1289
  }

1290 1291 1292 1293
  // All of the dimensions below were taken from the Material Design spec:
  // https://material.io/design/components/lists.html#specs
  @override
  void performLayout() {
1294
    final BoxConstraints constraints = this.constraints;
1295 1296 1297 1298 1299
    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;
1300
    final Offset densityAdjustment = visualDensity.baseSizeAdjustment;
1301 1302 1303 1304 1305 1306 1307

    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.
1308
      maxHeight: (isDense ? 48.0 : 56.0) + densityAdjustment.dy,
1309
    );
1310
    final BoxConstraints looseConstraints = constraints.loosen();
1311
    final BoxConstraints iconConstraints = looseConstraints.enforce(maxIconHeightConstraint);
1312 1313

    final double tileWidth = looseConstraints.maxWidth;
1314 1315
    final Size leadingSize = _layoutBox(leading, iconConstraints);
    final Size trailingSize = _layoutBox(trailing, iconConstraints);
1316
    assert(
1317
      tileWidth != leadingSize.width || tileWidth == 0.0,
1318 1319
      'Leading widget consumes entire tile width. Please use a sized widget, '
      'or consider replacing ListTile with a custom widget '
1320
      '(see https://api.flutter.dev/flutter/material/ListTile-class.html#material.ListTile.4)',
1321 1322
    );
    assert(
1323
      tileWidth != trailingSize.width || tileWidth == 0.0,
1324 1325
      'Trailing widget consumes entire tile width. Please use a sized widget, '
      'or consider replacing ListTile with a custom widget '
1326
      '(see https://api.flutter.dev/flutter/material/ListTile-class.html#material.ListTile.4)',
1327
    );
1328 1329

    final double titleStart = hasLeading
1330
      ? math.max(_minLeadingWidth, leadingSize.width) + _effectiveHorizontalTitleGap
1331
      : 0.0;
1332
    final double adjustedTrailingWidth = hasTrailing
1333
        ? math.max(trailingSize.width + _effectiveHorizontalTitleGap, 32.0)
1334
        : 0.0;
1335
    final BoxConstraints textConstraints = looseConstraints.tighten(
1336
      width: tileWidth - titleStart - adjustedTrailingWidth,
1337 1338 1339 1340
    );
    final Size titleSize = _layoutBox(title, textConstraints);
    final Size subtitleSize = _layoutBox(subtitle, textConstraints);

1341 1342
    double? titleBaseline;
    double? subtitleBaseline;
1343 1344 1345 1346 1347 1348 1349 1350 1351 1352
    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);
    }

1353 1354
    final double defaultTileHeight = _defaultTileHeight;

1355 1356
    double tileHeight;
    double titleY;
1357
    double? subtitleY;
1358
    if (!hasSubtitle) {
1359
      tileHeight = math.max(defaultTileHeight, titleSize.height + 2.0 * _minVerticalPadding);
1360 1361
      titleY = (tileHeight - titleSize.height) / 2.0;
    } else {
1362
      assert(subtitleBaselineType != null);
1363 1364
      titleY = titleBaseline! - _boxBaseline(title!, titleBaselineType)!;
      subtitleY = subtitleBaseline! - _boxBaseline(subtitle!, subtitleBaselineType!)! + visualDensity.vertical * 2.0;
1365
      tileHeight = defaultTileHeight;
1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386

      // 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;
      }
    }

1387 1388
    final double leadingY;
    final double trailingY;
1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420

    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: {
1421 1422
        leadingY = _minVerticalPadding;
        trailingY = _minVerticalPadding;
1423 1424 1425
        break;
      }
      case ListTileTitleAlignment.center: {
1426 1427
        leadingY = (tileHeight - leadingSize.height) / 2.0;
        trailingY = (tileHeight - trailingSize.height) / 2.0;
1428
        break;
1429
      }
1430 1431 1432 1433
      case ListTileTitleAlignment.bottom: {
        leadingY = tileHeight - leadingSize.height - _minVerticalPadding;
        trailingY = tileHeight - trailingSize.height - _minVerticalPadding;
        break;
1434
      }
1435
    }
1436 1437 1438

    switch (textDirection) {
      case TextDirection.rtl: {
1439
        if (hasLeading) {
1440
          _positionBox(leading!, Offset(tileWidth - leadingSize.width, leadingY));
1441
        }
1442
        _positionBox(title!, Offset(adjustedTrailingWidth, titleY));
1443
        if (hasSubtitle) {
1444
          _positionBox(subtitle!, Offset(adjustedTrailingWidth, subtitleY!));
1445 1446
        }
        if (hasTrailing) {
1447
          _positionBox(trailing!, Offset(0.0, trailingY));
1448
        }
1449 1450 1451
        break;
      }
      case TextDirection.ltr: {
1452
        if (hasLeading) {
1453
          _positionBox(leading!, Offset(0.0, leadingY));
1454
        }
1455
        _positionBox(title!, Offset(titleStart, titleY));
1456
        if (hasSubtitle) {
1457
          _positionBox(subtitle!, Offset(titleStart, subtitleY!));
1458 1459
        }
        if (hasTrailing) {
1460
          _positionBox(trailing!, Offset(tileWidth - trailingSize.width, trailingY));
1461
        }
1462 1463 1464 1465
        break;
      }
    }

1466
    size = constraints.constrain(Size(tileWidth, tileHeight));
1467 1468 1469 1470 1471 1472
    assert(size.width == constraints.constrainWidth(tileWidth));
    assert(size.height == constraints.constrainHeight(tileHeight));
  }

  @override
  void paint(PaintingContext context, Offset offset) {
1473
    void doPaint(RenderBox? child) {
1474
      if (child != null) {
1475
        final BoxParentData parentData = child.parentData! as BoxParentData;
1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488
        context.paintChild(child, parentData.offset + offset);
      }
    }
    doPaint(leading);
    doPaint(title);
    doPaint(subtitle);
    doPaint(trailing);
  }

  @override
  bool hitTestSelf(Offset position) => true;

  @override
1489
  bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
1490
    for (final RenderBox child in children) {
1491
      final BoxParentData parentData = child.parentData! as BoxParentData;
1492 1493 1494 1495 1496 1497 1498 1499
      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);
        },
      );
1500
      if (isHit) {
1501
        return true;
1502
      }
1503 1504 1505 1506
    }
    return false;
  }
}
1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525 1526 1527 1528 1529 1530 1531 1532 1533 1534 1535 1536 1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547 1548 1549 1550 1551 1552 1553 1554 1555 1556 1557 1558 1559 1560 1561 1562 1563

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.

1564
// Token database version: v0_162
1565 1566 1567 1568 1569 1570 1571 1572 1573 1574 1575 1576 1577 1578 1579 1580 1581 1582 1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594 1595

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
1596
  Color? get iconColor => _colors.onSurfaceVariant;
1597 1598 1599
}

// END GENERATED TOKEN PROPERTIES - LisTile