// Copyright 2015 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'colors.dart'; import 'constants.dart'; import 'debug.dart'; import 'ink_well.dart'; import 'theme.dart'; /// Defines the title font used for [ListTile] descendants of a [ListTileTheme]. /// /// List tiles that appear in a [Drawer] use the theme's [TextTheme.body2] /// text style, which is a little smaller than the theme's [TextTheme.subhead] /// 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, } /// An inherited widget that defines color and style parameters for [ListTile]s /// in this widget's subtree. /// /// Values specified here are used for [ListTile] properties that are not given /// an explicit non-null value. /// /// The [Drawer] widget specifies a tile theme for its children which sets /// [style] to [ListTileStyle.drawer]. class ListTileTheme extends InheritedWidget { /// Creates a list tile theme that controls the color and style parameters for /// [ListTile]s. const ListTileTheme({ Key key, this.dense: false, this.style: ListTileStyle.list, this.selectedColor, this.iconColor, this.textColor, Widget child, }) : super(key: key, child: child); /// Creates a list tile theme that controls the color and style parameters for /// [ListTile]s, and merges in the current list tile theme, if any. /// /// The [child] argument must not be null. static Widget merge({ Key key, bool dense, ListTileStyle style, Color selectedColor, Color iconColor, Color textColor, @required Widget child, }) { assert(child != null); return new Builder( builder: (BuildContext context) { final ListTileTheme parent = ListTileTheme.of(context); return new ListTileTheme( key: key, dense: dense ?? parent.dense, style: style ?? parent.style, selectedColor: selectedColor ?? parent.selectedColor, iconColor: iconColor ?? parent.iconColor, textColor: textColor ?? parent.textColor, child: child, ); }, ); } /// If true then [ListTile]s will have the vertically dense layout. final bool dense; /// If specified, [style] defines the font used for [ListTile] titles. final ListTileStyle style; /// If specified, the color used for icons and text when a [ListTile] is selected. final Color selectedColor; /// If specified, the icon color used for enabled [ListTile]s that are not selected. final Color iconColor; /// If specified, the text color used for enabled [ListTile]s that are not selected. final Color textColor; /// The closest instance of this class that encloses the given context. /// /// Typical usage is as follows: /// /// ```dart /// ListTileTheme theme = ListTileTheme.of(context); /// ``` static ListTileTheme of(BuildContext context) { final ListTileTheme result = context.inheritFromWidgetOfExactType(ListTileTheme); return result ?? const ListTileTheme(); } @override bool updateShouldNotify(ListTileTheme oldTheme) { return dense != oldTheme.dense || style != oldTheme.style || selectedColor != oldTheme.selectedColor || iconColor != oldTheme.iconColor || textColor != oldTheme.textColor; } } /// 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. 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, } /// A single fixed-height row that typically contains some text as well as /// a leading or trailing icon. /// /// 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. /// /// List tiles are always a fixed height (which height depends on how /// [isThreeLine], [dense], and [subtitle] are configured); they do not grow in /// height based on their contents. If you are looking for a widget that allows /// for arbitrary layout in a row, consider [Row]. /// /// List tiles are typically used in [ListView]s, or arranged in [Column]s in /// [Drawer]s and [Card]s. /// /// Requires one of its ancestors to be a [Material] widget. /// /// ## Sample code /// /// Here is a simple tile with an icon and some text. /// /// ```dart /// new ListTile( /// leading: const Icon(Icons.event_seat), /// title: const Text('The seat for the narrator'), /// ) /// ``` /// /// 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 /// int _act = 1; /// // ... /// new 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 */ } /// ) /// ``` /// /// 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. /// * <https://material.google.com/components/lists.html> 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({ Key key, this.leading, this.title, this.subtitle, this.trailing, this.isThreeLine: false, this.dense, this.enabled: true, this.onTap, this.onLongPress, this.selected: false, }) : assert(isThreeLine != null), assert(enabled != null), assert(selected != null), assert(!isThreeLine || subtitle != null), super(key: key); /// 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. final Widget title; /// Additional content displayed below the title. /// /// Typically a [Text] widget. final Widget subtitle; /// A widget to display after the title. /// /// Typically an [Icon] widget. final Widget trailing; /// Whether this list tile is intended to display three 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. 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]. final bool dense; /// 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; /// 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]. final bool selected; /// 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 }) sync* { assert(tiles != null); assert(color != null || context != null); final Color dividerColor = color ?? Theme.of(context).dividerColor; final Iterator<Widget> iterator = tiles.iterator; final bool isNotEmpty = iterator.moveNext(); Widget tile = iterator.current; while (iterator.moveNext()) { yield new DecoratedBox( position: DecorationPosition.foreground, decoration: new BoxDecoration( border: new Border( bottom: new BorderSide(color: dividerColor, width: 0.0), ), ), child: tile, ); tile = iterator.current; } if (isNotEmpty) yield tile; } Color _iconColor(ThemeData theme, ListTileTheme tileTheme) { if (!enabled) return theme.disabledColor; if (selected && tileTheme?.selectedColor != null) return tileTheme.selectedColor; if (!selected && tileTheme?.iconColor != null) return tileTheme.iconColor; switch (theme.brightness) { case Brightness.light: return selected ? theme.primaryColor : Colors.black45; case Brightness.dark: return selected ? theme.accentColor : null; // null - use current icon theme color } assert(theme.brightness != null); return null; } Color _textColor(ThemeData theme, ListTileTheme tileTheme, Color defaultColor) { if (!enabled) return theme.disabledColor; if (selected && tileTheme?.selectedColor != null) return tileTheme.selectedColor; if (!selected && tileTheme?.textColor != null) return tileTheme.textColor; if (selected) { switch (theme.brightness) { case Brightness.light: return theme.primaryColor; case Brightness.dark: return theme.accentColor; } } return defaultColor; } bool _denseLayout(ListTileTheme tileTheme) { return dense != null ? dense : (tileTheme?.dense ?? false); } TextStyle _titleTextStyle(ThemeData theme, ListTileTheme tileTheme) { final TextStyle style = tileTheme?.style == ListTileStyle.drawer ? theme.textTheme.body2 : theme.textTheme.subhead; final Color color = _textColor(theme, tileTheme, style.color); return _denseLayout(tileTheme) ? style.copyWith(fontSize: 13.0, color: color) : style.copyWith(color: color); } TextStyle _subtitleTextStyle(ThemeData theme, ListTileTheme tileTheme) { final TextStyle style = theme.textTheme.body1; final Color color = _textColor(theme, tileTheme, theme.textTheme.caption.color); return _denseLayout(tileTheme) ? style.copyWith(color: color, fontSize: 12.0) : style.copyWith(color: color); } @override Widget build(BuildContext context) { assert(debugCheckHasMaterial(context)); final ThemeData theme = Theme.of(context); final ListTileTheme tileTheme = ListTileTheme.of(context); final bool isTwoLine = !isThreeLine && subtitle != null; final bool isOneLine = !isThreeLine && !isTwoLine; double tileHeight; if (isOneLine) tileHeight = _denseLayout(tileTheme) ? 48.0 : 56.0; else if (isTwoLine) tileHeight = _denseLayout(tileTheme) ? 60.0 : 72.0; else tileHeight = _denseLayout(tileTheme) ? 76.0 : 88.0; // Overall, the list tile is a Row() with these children. final List<Widget> children = <Widget>[]; IconThemeData iconThemeData; if (leading != null || trailing != null) iconThemeData = new IconThemeData(color: _iconColor(theme, tileTheme)); if (leading != null) { children.add(IconTheme.merge( data: iconThemeData, child: new Container( margin: const EdgeInsetsDirectional.only(end: 16.0), width: 40.0, alignment: AlignmentDirectional.centerStart, child: leading, ), )); } final Widget primaryLine = new AnimatedDefaultTextStyle( style: _titleTextStyle(theme, tileTheme), duration: kThemeChangeDuration, child: title ?? new Container() ); Widget center = primaryLine; if (subtitle != null && (isTwoLine || isThreeLine)) { center = new Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ primaryLine, new AnimatedDefaultTextStyle( style: _subtitleTextStyle(theme, tileTheme), duration: kThemeChangeDuration, child: subtitle, ), ], ); } children.add(new Expanded( child: center, )); if (trailing != null) { children.add(IconTheme.merge( data: iconThemeData, child: new Container( margin: const EdgeInsetsDirectional.only(start: 16.0), alignment: AlignmentDirectional.centerEnd, child: trailing, ), )); } return new InkWell( onTap: enabled ? onTap : null, onLongPress: enabled ? onLongPress : null, child: new Container( height: tileHeight, padding: const EdgeInsets.symmetric(horizontal: 16.0), child: new Row(children: children), ) ); } }