| // 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 'dart:math' as math; |
| |
| import 'package:flutter/widgets.dart'; |
| import 'package:flutter/foundation.dart'; |
| import 'package:flutter/rendering.dart'; |
| |
| import 'colors.dart'; |
| import 'constants.dart'; |
| import 'debug.dart'; |
| import 'divider.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, |
| this.contentPadding, |
| 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, |
| EdgeInsetsGeometry contentPadding, |
| @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, |
| contentPadding: contentPadding ?? parent.contentPadding, |
| 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 tile's internal padding. |
| /// |
| /// Insets a [ListTile]'s contents: its [leading], [title], [subtitle], |
| /// and [trailing] widgets. |
| final EdgeInsetsGeometry contentPadding; |
| |
| /// 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 oldWidget) { |
| return dense != oldWidget.dense |
| || style != oldWidget.style |
| || selectedColor != oldWidget.selectedColor |
| || iconColor != oldWidget.iconColor |
| || textColor != oldWidget.textColor |
| || contentPadding != oldWidget.contentPadding; |
| } |
| } |
| |
| /// 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.contentPadding, |
| 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; |
| |
| /// 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; |
| |
| /// 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 Iterator<Widget> iterator = tiles.iterator; |
| final bool isNotEmpty = iterator.moveNext(); |
| |
| final Decoration decoration = new BoxDecoration( |
| border: new Border( |
| bottom: Divider.createBorderSide(context, color: color), |
| ), |
| ); |
| |
| Widget tile = iterator.current; |
| while (iterator.moveNext()) { |
| yield new DecoratedBox( |
| position: DecorationPosition.foreground, |
| decoration: decoration, |
| 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 _isDenseLayout(ListTileTheme tileTheme) { |
| return dense != null ? dense : (tileTheme?.dense ?? false); |
| } |
| |
| TextStyle _titleTextStyle(ThemeData theme, ListTileTheme tileTheme) { |
| TextStyle style; |
| if (tileTheme != null) { |
| switch (tileTheme.style) { |
| case ListTileStyle.drawer: |
| style = theme.textTheme.body2; |
| break; |
| case ListTileStyle.list: |
| style = theme.textTheme.subhead; |
| break; |
| } |
| } else { |
| style = theme.textTheme.subhead; |
| } |
| final Color color = _textColor(theme, tileTheme, style.color); |
| return _isDenseLayout(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 _isDenseLayout(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); |
| |
| IconThemeData iconThemeData; |
| if (leading != null || trailing != null) |
| iconThemeData = new IconThemeData(color: _iconColor(theme, tileTheme)); |
| |
| Widget leadingIcon; |
| if (leading != null) { |
| leadingIcon = IconTheme.merge( |
| data: iconThemeData, |
| child: leading, |
| ); |
| } |
| |
| final TextStyle titleStyle = _titleTextStyle(theme, tileTheme); |
| final Widget titleText = new AnimatedDefaultTextStyle( |
| style: titleStyle, |
| duration: kThemeChangeDuration, |
| child: title ?? const SizedBox() |
| ); |
| |
| Widget subtitleText; |
| TextStyle subtitleStyle; |
| if (subtitle != null) { |
| subtitleStyle = _subtitleTextStyle(theme, tileTheme); |
| subtitleText = new AnimatedDefaultTextStyle( |
| style: subtitleStyle, |
| duration: kThemeChangeDuration, |
| child: subtitle, |
| ); |
| } |
| |
| Widget trailingIcon; |
| if (trailing != null) { |
| trailingIcon = IconTheme.merge( |
| data: iconThemeData, |
| child: trailing, |
| ); |
| } |
| |
| const EdgeInsets _defaultContentPadding = EdgeInsets.symmetric(horizontal: 16.0); |
| final TextDirection textDirection = Directionality.of(context); |
| final EdgeInsets resolvedContentPadding = contentPadding?.resolve(textDirection) |
| ?? tileTheme?.contentPadding?.resolve(textDirection) |
| ?? _defaultContentPadding; |
| |
| return new InkWell( |
| onTap: enabled ? onTap : null, |
| onLongPress: enabled ? onLongPress : null, |
| child: new Semantics( |
| selected: selected, |
| enabled: enabled, |
| child: new SafeArea( |
| top: false, |
| bottom: false, |
| minimum: resolvedContentPadding, |
| child: new _ListTile( |
| leading: leadingIcon, |
| title: titleText, |
| subtitle: subtitleText, |
| trailing: trailingIcon, |
| isDense: _isDenseLayout(tileTheme), |
| isThreeLine: isThreeLine, |
| textDirection: textDirection, |
| titleBaselineType: titleStyle.textBaseline, |
| subtitleBaselineType: subtitleStyle?.textBaseline, |
| ), |
| ), |
| ), |
| ); |
| } |
| } |
| |
| // Identifies the children of a _ListTileElement. |
| enum _ListTileSlot { |
| leading, |
| title, |
| subtitle, |
| trailing, |
| } |
| |
| class _ListTile extends RenderObjectWidget { |
| const _ListTile({ |
| Key key, |
| this.leading, |
| this.title, |
| this.subtitle, |
| this.trailing, |
| @required this.isThreeLine, |
| @required this.isDense, |
| @required this.textDirection, |
| @required this.titleBaselineType, |
| this.subtitleBaselineType, |
| }) : assert(isThreeLine != null), |
| assert(isDense != null), |
| assert(textDirection != null), |
| assert(titleBaselineType != null), |
| super(key: key); |
| |
| final Widget leading; |
| final Widget title; |
| final Widget subtitle; |
| final Widget trailing; |
| final bool isThreeLine; |
| final bool isDense; |
| final TextDirection textDirection; |
| final TextBaseline titleBaselineType; |
| final TextBaseline subtitleBaselineType; |
| |
| @override |
| _ListTileElement createElement() => new _ListTileElement(this); |
| |
| @override |
| _RenderListTile createRenderObject(BuildContext context) { |
| return new _RenderListTile( |
| isThreeLine: isThreeLine, |
| isDense: isDense, |
| textDirection: textDirection, |
| titleBaselineType: titleBaselineType, |
| subtitleBaselineType: subtitleBaselineType, |
| ); |
| } |
| |
| @override |
| void updateRenderObject(BuildContext context, _RenderListTile renderObject) { |
| renderObject |
| ..isThreeLine = isThreeLine |
| ..isDense = isDense |
| ..textDirection = textDirection |
| ..titleBaselineType = titleBaselineType |
| ..subtitleBaselineType = subtitleBaselineType; |
| } |
| } |
| |
| class _ListTileElement extends RenderObjectElement { |
| _ListTileElement(_ListTile widget) : super(widget); |
| |
| final Map<_ListTileSlot, Element> slotToChild = <_ListTileSlot, Element>{}; |
| final Map<Element, _ListTileSlot> childToSlot = <Element, _ListTileSlot>{}; |
| |
| @override |
| _ListTile get widget => super.widget; |
| |
| @override |
| _RenderListTile get renderObject => super.renderObject; |
| |
| @override |
| void visitChildren(ElementVisitor visitor) { |
| slotToChild.values.forEach(visitor); |
| } |
| |
| @override |
| void forgetChild(Element child) { |
| assert(slotToChild.values.contains(child)); |
| assert(childToSlot.keys.contains(child)); |
| final _ListTileSlot slot = childToSlot[child]; |
| childToSlot.remove(child); |
| slotToChild.remove(slot); |
| } |
| |
| void _mountChild(Widget widget, _ListTileSlot slot) { |
| final Element oldChild = slotToChild[slot]; |
| final Element newChild = updateChild(oldChild, widget, slot); |
| if (oldChild != null) { |
| slotToChild.remove(slot); |
| childToSlot.remove(oldChild); |
| } |
| if (newChild != null) { |
| slotToChild[slot] = newChild; |
| childToSlot[newChild] = slot; |
| } |
| } |
| |
| @override |
| void mount(Element parent, dynamic newSlot) { |
| super.mount(parent, newSlot); |
| _mountChild(widget.leading, _ListTileSlot.leading); |
| _mountChild(widget.title, _ListTileSlot.title); |
| _mountChild(widget.subtitle, _ListTileSlot.subtitle); |
| _mountChild(widget.trailing, _ListTileSlot.trailing); |
| } |
| |
| void _updateChild(Widget widget, _ListTileSlot slot) { |
| final Element oldChild = slotToChild[slot]; |
| final Element newChild = updateChild(oldChild, widget, slot); |
| if (oldChild != null) { |
| childToSlot.remove(oldChild); |
| slotToChild.remove(slot); |
| } |
| if (newChild != null) { |
| slotToChild[slot] = newChild; |
| childToSlot[newChild] = slot; |
| } |
| } |
| |
| @override |
| void update(_ListTile newWidget) { |
| super.update(newWidget); |
| assert(widget == newWidget); |
| _updateChild(widget.leading, _ListTileSlot.leading); |
| _updateChild(widget.title, _ListTileSlot.title); |
| _updateChild(widget.subtitle, _ListTileSlot.subtitle); |
| _updateChild(widget.trailing, _ListTileSlot.trailing); |
| } |
| |
| void _updateRenderObject(RenderObject child, _ListTileSlot slot) { |
| switch (slot) { |
| case _ListTileSlot.leading: |
| renderObject.leading = child; |
| break; |
| case _ListTileSlot.title: |
| renderObject.title = child; |
| break; |
| case _ListTileSlot.subtitle: |
| renderObject.subtitle = child; |
| break; |
| case _ListTileSlot.trailing: |
| renderObject.trailing = child; |
| break; |
| } |
| } |
| |
| @override |
| void insertChildRenderObject(RenderObject child, dynamic slotValue) { |
| assert(child is RenderBox); |
| assert(slotValue is _ListTileSlot); |
| final _ListTileSlot slot = slotValue; |
| _updateRenderObject(child, slot); |
| assert(renderObject.childToSlot.keys.contains(child)); |
| assert(renderObject.slotToChild.keys.contains(slot)); |
| } |
| |
| @override |
| void removeChildRenderObject(RenderObject child) { |
| assert(child is RenderBox); |
| assert(renderObject.childToSlot.keys.contains(child)); |
| _updateRenderObject(null, renderObject.childToSlot[child]); |
| assert(!renderObject.childToSlot.keys.contains(child)); |
| assert(!renderObject.slotToChild.keys.contains(slot)); |
| } |
| |
| @override |
| void moveChildRenderObject(RenderObject child, dynamic slotValue) { |
| assert(false, 'not reachable'); |
| } |
| } |
| |
| class _RenderListTile extends RenderBox { |
| _RenderListTile({ |
| @required bool isDense, |
| @required bool isThreeLine, |
| @required TextDirection textDirection, |
| @required TextBaseline titleBaselineType, |
| TextBaseline subtitleBaselineType, |
| }) : assert(isDense != null), |
| assert(isThreeLine != null), |
| assert(textDirection != null), |
| assert(titleBaselineType != null), |
| _isDense = isDense, |
| _isThreeLine = isThreeLine, |
| _textDirection = textDirection, |
| _titleBaselineType = titleBaselineType, |
| _subtitleBaselineType = subtitleBaselineType; |
| |
| static const double _minLeadingWidth = 40.0; |
| // The horizontal gap between the titles and the leading/trailing widgets |
| static const double _horizontalTitleGap = 16.0; |
| // The minimum padding on the top and bottom of the title and subtitle widgets. |
| static const double _minVerticalPadding = 4.0; |
| |
| final Map<_ListTileSlot, RenderBox> slotToChild = <_ListTileSlot, RenderBox>{}; |
| final Map<RenderBox, _ListTileSlot> childToSlot = <RenderBox, _ListTileSlot>{}; |
| |
| RenderBox _updateChild(RenderBox oldChild, RenderBox newChild, _ListTileSlot slot) { |
| if (oldChild != null) { |
| dropChild(oldChild); |
| childToSlot.remove(oldChild); |
| slotToChild.remove(slot); |
| } |
| if (newChild != null) { |
| childToSlot[newChild] = slot; |
| slotToChild[slot] = newChild; |
| adoptChild(newChild); |
| } |
| return newChild; |
| } |
| |
| RenderBox _leading; |
| RenderBox get leading => _leading; |
| set leading(RenderBox value) { |
| _leading = _updateChild(_leading, value, _ListTileSlot.leading); |
| } |
| |
| RenderBox _title; |
| RenderBox get title => _title; |
| set title(RenderBox value) { |
| _title = _updateChild(_title, value, _ListTileSlot.title); |
| } |
| |
| RenderBox _subtitle; |
| RenderBox get subtitle => _subtitle; |
| set subtitle(RenderBox value) { |
| _subtitle = _updateChild(_subtitle, value, _ListTileSlot.subtitle); |
| } |
| |
| RenderBox _trailing; |
| RenderBox get trailing => _trailing; |
| set trailing(RenderBox value) { |
| _trailing = _updateChild(_trailing, value, _ListTileSlot.trailing); |
| } |
| |
| // The returned list is ordered for hit testing. |
| Iterable<RenderBox> get _children sync *{ |
| if (leading != null) |
| yield leading; |
| if (title != null) |
| yield title; |
| if (subtitle != null) |
| yield subtitle; |
| if (trailing != null) |
| yield trailing; |
| } |
| |
| bool get isDense => _isDense; |
| bool _isDense; |
| set isDense(bool value) { |
| assert(value != null); |
| if (_isDense == value) |
| return; |
| _isDense = value; |
| markNeedsLayout(); |
| } |
| |
| bool get isThreeLine => _isThreeLine; |
| bool _isThreeLine; |
| set isThreeLine(bool value) { |
| assert(value != null); |
| if (_isThreeLine == value) |
| return; |
| _isThreeLine = value; |
| markNeedsLayout(); |
| } |
| |
| TextDirection get textDirection => _textDirection; |
| TextDirection _textDirection; |
| set textDirection(TextDirection value) { |
| assert(value != null); |
| if (_textDirection == value) |
| return; |
| _textDirection = value; |
| markNeedsLayout(); |
| } |
| |
| TextBaseline get titleBaselineType => _titleBaselineType; |
| TextBaseline _titleBaselineType; |
| set titleBaselineType(TextBaseline value) { |
| assert(value != null); |
| if (_titleBaselineType == value) |
| return; |
| _titleBaselineType = value; |
| markNeedsLayout(); |
| } |
| |
| TextBaseline get subtitleBaselineType => _subtitleBaselineType; |
| TextBaseline _subtitleBaselineType; |
| set subtitleBaselineType(TextBaseline value) { |
| if (_subtitleBaselineType == value) |
| return; |
| _subtitleBaselineType = value; |
| markNeedsLayout(); |
| } |
| |
| @override |
| void attach(PipelineOwner owner) { |
| super.attach(owner); |
| for (RenderBox child in _children) |
| child.attach(owner); |
| } |
| |
| @override |
| void detach() { |
| super.detach(); |
| for (RenderBox child in _children) |
| child.detach(); |
| } |
| |
| @override |
| void redepthChildren() { |
| _children.forEach(redepthChild); |
| } |
| |
| @override |
| void visitChildren(RenderObjectVisitor visitor) { |
| _children.forEach(visitor); |
| } |
| |
| @override |
| List<DiagnosticsNode> debugDescribeChildren() { |
| final List<DiagnosticsNode> value = <DiagnosticsNode>[]; |
| void add(RenderBox child, String name) { |
| if (child != null) |
| value.add(child.toDiagnosticsNode(name: name)); |
| } |
| add(leading, 'leading'); |
| add(title, 'title'); |
| add(subtitle, 'subtitle'); |
| add(trailing, 'trailing'); |
| return value; |
| } |
| |
| @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) + _horizontalTitleGap |
| : 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) + _horizontalTitleGap |
| : 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; |
| |
| if (isOneLine) |
| return isDense ? 48.0 : 56.0; |
| if (isTwoLine) |
| return isDense ? 64.0 : 72.0; |
| return isDense ? 76.0 : 88.0; |
| } |
| |
| @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; |
| 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; |
| parentData.offset = offset; |
| } |
| |
| // All of the dimensions below were taken from the Material Design spec: |
| // https://material.io/design/components/lists.html#specs |
| @override |
| void performLayout() { |
| 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 BoxConstraints looseConstraints = constraints.loosen(); |
| |
| final double tileWidth = looseConstraints.maxWidth; |
| final Size leadingSize = _layoutBox(leading, looseConstraints); |
| final Size trailingSize = _layoutBox(trailing, looseConstraints); |
| |
| final double titleStart = hasLeading |
| ? math.max(_minLeadingWidth, leadingSize.width) + _horizontalTitleGap |
| : 0.0; |
| final BoxConstraints textConstraints = looseConstraints.tighten( |
| width: tileWidth - titleStart - (hasTrailing ? trailingSize.width + _horizontalTitleGap : 0.0), |
| ); |
| 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); |
| } |
| |
| 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); |
| 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 = (tileHeight - leadingSize.height) / 2.0; |
| final double trailingY = (tileHeight - trailingSize.height) / 2.0; |
| |
| switch (textDirection) { |
| case TextDirection.rtl: { |
| if (hasLeading) |
| _positionBox(leading, new Offset(tileWidth - leadingSize.width, leadingY)); |
| final double titleX = hasTrailing ? trailingSize.width + _horizontalTitleGap : 0.0; |
| _positionBox(title, new Offset(titleX, titleY)); |
| if (hasSubtitle) |
| _positionBox(subtitle, new Offset(titleX, subtitleY)); |
| if (hasTrailing) |
| _positionBox(trailing, new Offset(0.0, trailingY)); |
| break; |
| } |
| case TextDirection.ltr: { |
| if (hasLeading) |
| _positionBox(leading, new Offset(0.0, leadingY)); |
| _positionBox(title, new Offset(titleStart, titleY)); |
| if (hasSubtitle) |
| _positionBox(subtitle, new Offset(titleStart, subtitleY)); |
| if (hasTrailing) |
| _positionBox(trailing, new Offset(tileWidth - trailingSize.width, trailingY)); |
| break; |
| } |
| } |
| |
| size = constraints.constrain(new 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; |
| context.paintChild(child, parentData.offset + offset); |
| } |
| } |
| doPaint(leading); |
| doPaint(title); |
| doPaint(subtitle); |
| doPaint(trailing); |
| } |
| |
| @override |
| bool hitTestSelf(Offset position) => true; |
| |
| @override |
| bool hitTestChildren(HitTestResult result, { @required Offset position }) { |
| assert(position != null); |
| for (RenderBox child in _children) { |
| final BoxParentData parentData = child.parentData; |
| if (child.hitTest(result, position: position - parentData.offset)) |
| return true; |
| } |
| return false; |
| } |
| } |