list_tile.dart 14.9 KB
Newer Older
Adam Barth's avatar
Adam Barth committed
1 2 3 4
// 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.

xster's avatar
xster committed
5
import 'package:flutter/foundation.dart';
Adam Barth's avatar
Adam Barth committed
6 7
import 'package:flutter/widgets.dart';

8
import 'colors.dart';
9
import 'constants.dart';
10
import 'debug.dart';
Adam Barth's avatar
Adam Barth committed
11
import 'ink_well.dart';
Hans Muller's avatar
Hans Muller committed
12
import 'theme.dart';
Adam Barth's avatar
Adam Barth committed
13

14 15 16 17 18 19
/// 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 {
Adam Barth's avatar
Adam Barth committed
20
  /// Use a title font that's appropriate for a [ListTile] in a list.
21 22
  list,

Adam Barth's avatar
Adam Barth committed
23
  /// Use a title font that's appropriate for a [ListTile] that appears in a [Drawer].
24
  drawer,
25 26
}

27 28 29 30 31 32 33 34 35
/// 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 {
36 37
  /// Creates a list tile theme that controls the color and style parameters for
  /// [ListTile]s.
38 39 40 41 42 43 44 45 46 47
  const ListTileTheme({
    Key key,
    this.dense: false,
    this.style: ListTileStyle.list,
    this.selectedColor,
    this.iconColor,
    this.textColor,
    Widget child,
  }) : super(key: key, child: child);

48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77
  /// 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,
        );
      },
    );
  }

78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101
  /// 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);
102
    return result ?? const ListTileTheme();
103 104 105 106 107 108 109 110 111 112 113 114
  }

  @override
  bool updateShouldNotify(ListTileTheme oldTheme) {
    return dense != oldTheme.dense
        || style != oldTheme.style
        || selectedColor != oldTheme.selectedColor
        || iconColor != oldTheme.iconColor
        || textColor != oldTheme.textColor;
  }
}

115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136
/// 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,
}

137 138
/// A single fixed-height row that typically contains some text as well as
/// a leading or trailing icon.
139
///
140
/// A list tile contains one to three lines of text optionally flanked by icons or
141
/// other widgets, such as check boxes. The icons (or other widgets) for the
142
/// tile are defined with the [leading] and [trailing] parameters. The first
143 144 145
/// 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]
146
/// is true then the overall height of this tile and the size of the
147 148
/// [DefaultTextStyle]s that wrap the [title] and [subtitle] widget are reduced.
///
149
/// List tiles are always a fixed height (which height depends on how
150 151 152 153
/// [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].
///
154 155
/// List tiles are typically used in [ListView]s, or arranged in [Column]s in
/// [Drawer]s and [Card]s.
156 157 158
///
/// Requires one of its ancestors to be a [Material] widget.
///
159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185
/// ## 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 */ }
/// )
/// ```
///
186
/// See also:
187
///
188
///  * [ListTileTheme], which defines visual properties for [ListTile]s.
189 190 191 192
///  * [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.
193
///  * [Card], which can be used with [Column] to show a few [ListTile]s.
194
///  * [Divider], which can be used to separate [ListTile]s.
195
///  * [ListTile.divideTiles], a utility for inserting [Divider]s in between [ListTile]s.
196 197
///  * [CheckboxListTile], [RadioListTile], and [SwitchListTile], widgets
///    that combine [ListTile] with other controls.
198
///  * <https://material.google.com/components/lists.html>
199 200
class ListTile extends StatelessWidget {
  /// Creates a list tile.
201 202 203 204
  ///
  /// If [isThreeLine] is true, then [subtitle] must not be null.
  ///
  /// Requires one of its ancestors to be a [Material] widget.
205
  const ListTile({
Adam Barth's avatar
Adam Barth committed
206
    Key key,
207 208 209 210
    this.leading,
    this.title,
    this.subtitle,
    this.trailing,
Hans Muller's avatar
Hans Muller committed
211
    this.isThreeLine: false,
212
    this.dense,
213
    this.enabled: true,
Adam Barth's avatar
Adam Barth committed
214
    this.onTap,
215 216
    this.onLongPress,
    this.selected: false,
217 218 219
  }) : assert(isThreeLine != null),
       assert(enabled != null),
       assert(selected != null),
220
       assert(!isThreeLine || subtitle != null),
221
       super(key: key);
Adam Barth's avatar
Adam Barth committed
222

223 224
  /// A widget to display before the title.
  ///
225
  /// Typically an [Icon] or a [CircleAvatar] widget.
226
  final Widget leading;
227

228
  /// The primary content of the list tile.
229 230
  ///
  /// Typically a [Text] widget.
231
  final Widget title;
232 233 234 235

  /// Additional content displayed below the title.
  ///
  /// Typically a [Text] widget.
236
  final Widget subtitle;
237 238 239 240

  /// A widget to display after the title.
  ///
  /// Typically an [Icon] widget.
241
  final Widget trailing;
242

243
  /// Whether this list tile is intended to display three lines of text.
244
  ///
245
  /// If false, the list tile is treated as having one line if the subtitle is
246
  /// null and treated as having two lines if the subtitle is non-null.
Hans Muller's avatar
Hans Muller committed
247
  final bool isThreeLine;
248

249
  /// Whether this list tile is part of a vertically dense list.
250 251
  ///
  /// If this property is null then its value is based on [ListTileTheme.dense].
252
  final bool dense;
253

254
  /// Whether this list tile is interactive.
255
  ///
256
  /// If false, this list tile is styled with the disabled color from the
257 258
  /// current [Theme] and the [onTap] and [onLongPress] callbacks are
  /// inoperative.
259
  final bool enabled;
260

261
  /// Called when the user taps this list tile.
262 263
  ///
  /// Inoperative if [enabled] is false.
Adam Barth's avatar
Adam Barth committed
264
  final GestureTapCallback onTap;
265

266
  /// Called when the user long-presses on this list tile.
267 268
  ///
  /// Inoperative if [enabled] is false.
Adam Barth's avatar
Adam Barth committed
269 270
  final GestureLongPressCallback onLongPress;

271 272 273 274 275 276
  /// 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;

277
  /// Add a one pixel border in between each tile. If color isn't specified the
278 279 280 281 282
  /// [ThemeData.dividerColor] of the context's [Theme] is used.
  ///
  /// See also:
  ///
  /// * [Divider], which you can use to obtain this effect manually.
283 284
  static Iterable<Widget> divideTiles({ BuildContext context, @required Iterable<Widget> tiles, Color color }) sync* {
    assert(tiles != null);
Hans Muller's avatar
Hans Muller committed
285 286 287
    assert(color != null || context != null);

    final Color dividerColor = color ?? Theme.of(context).dividerColor;
288
    final Iterator<Widget> iterator = tiles.iterator;
Hans Muller's avatar
Hans Muller committed
289 290
    final bool isNotEmpty = iterator.moveNext();

291
    Widget tile = iterator.current;
292
    while (iterator.moveNext()) {
Hans Muller's avatar
Hans Muller committed
293
      yield new DecoratedBox(
294
        position: DecorationPosition.foreground,
Hans Muller's avatar
Hans Muller committed
295 296
        decoration: new BoxDecoration(
          border: new Border(
297
            bottom: new BorderSide(color: dividerColor, width: 0.0),
298
          ),
Hans Muller's avatar
Hans Muller committed
299
        ),
300
        child: tile,
Hans Muller's avatar
Hans Muller committed
301
      );
302
      tile = iterator.current;
Hans Muller's avatar
Hans Muller committed
303 304
    }
    if (isNotEmpty)
305
      yield tile;
Hans Muller's avatar
Hans Muller committed
306 307
  }

308 309 310 311 312 313 314 315 316 317 318 319 320 321 322
  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
323
    }
324 325
    assert(theme.brightness != null);
    return null;
Hans Muller's avatar
Hans Muller committed
326 327
  }

328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353
  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) {
354 355 356 357 358 359 360 361 362 363 364 365 366
    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;
    }
367 368 369 370 371 372 373
    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) {
374
    final TextStyle style = theme.textTheme.body1;
375 376 377 378
    final Color color = _textColor(theme, tileTheme, theme.textTheme.caption.color);
    return _denseLayout(tileTheme)
      ? style.copyWith(color: color, fontSize: 12.0)
      : style.copyWith(color: color);
Hans Muller's avatar
Hans Muller committed
379 380
  }

381
  @override
Adam Barth's avatar
Adam Barth committed
382
  Widget build(BuildContext context) {
383
    assert(debugCheckHasMaterial(context));
384 385 386
    final ThemeData theme = Theme.of(context);
    final ListTileTheme tileTheme = ListTileTheme.of(context);

387
    final bool isTwoLine = !isThreeLine && subtitle != null;
Hans Muller's avatar
Hans Muller committed
388
    final bool isOneLine = !isThreeLine && !isTwoLine;
389
    double tileHeight;
Hans Muller's avatar
Hans Muller committed
390
    if (isOneLine)
391
      tileHeight = _denseLayout(tileTheme) ? 48.0 : 56.0;
Hans Muller's avatar
Hans Muller committed
392
    else if (isTwoLine)
393
      tileHeight = _denseLayout(tileTheme) ? 60.0 : 72.0;
Hans Muller's avatar
Hans Muller committed
394
    else
395
      tileHeight = _denseLayout(tileTheme) ? 76.0 : 88.0;
Hans Muller's avatar
Hans Muller committed
396

397
    // Overall, the list tile is a Row() with these children.
Hans Muller's avatar
Hans Muller committed
398
    final List<Widget> children = <Widget>[];
Adam Barth's avatar
Adam Barth committed
399

400 401 402 403
    IconThemeData iconThemeData;
    if (leading != null || trailing != null)
      iconThemeData = new IconThemeData(color: _iconColor(theme, tileTheme));

404
    if (leading != null) {
405
      children.add(IconTheme.merge(
406
        data: iconThemeData,
407
        child: new Container(
408
          margin: const EdgeInsetsDirectional.only(end: 16.0),
409
          width: 40.0,
410
          alignment: AlignmentDirectional.centerStart,
411
          child: leading,
412
        ),
Adam Barth's avatar
Adam Barth committed
413 414 415
      ));
    }

416
    final Widget primaryLine = new AnimatedDefaultTextStyle(
417
      style: _titleTextStyle(theme, tileTheme),
418
      duration: kThemeChangeDuration,
419
      child: title ?? new Container()
Hans Muller's avatar
Hans Muller committed
420 421
    );
    Widget center = primaryLine;
422
    if (subtitle != null && (isTwoLine || isThreeLine)) {
Hans Muller's avatar
Hans Muller committed
423
      center = new Column(
424
        mainAxisSize: MainAxisSize.min,
425
        crossAxisAlignment: CrossAxisAlignment.start,
Hans Muller's avatar
Hans Muller committed
426 427
        children: <Widget>[
          primaryLine,
428
          new AnimatedDefaultTextStyle(
429
            style: _subtitleTextStyle(theme, tileTheme),
430
            duration: kThemeChangeDuration,
431
            child: subtitle,
432 433
          ),
        ],
Hans Muller's avatar
Hans Muller committed
434 435
      );
    }
436
    children.add(new Expanded(
437
      child: center,
Adam Barth's avatar
Adam Barth committed
438 439
    ));

440
    if (trailing != null) {
441 442 443 444
      children.add(IconTheme.merge(
        data: iconThemeData,
        child: new Container(
          margin: const EdgeInsetsDirectional.only(start: 16.0),
445
          alignment: AlignmentDirectional.centerEnd,
446 447
          child: trailing,
        ),
Adam Barth's avatar
Adam Barth committed
448 449 450
      ));
    }

451
    return new InkWell(
452 453
      onTap: enabled ? onTap : null,
      onLongPress: enabled ? onLongPress : null,
454 455 456 457 458 459 460 461 462 463
      child: new ConstrainedBox(
        constraints: new BoxConstraints(minHeight: tileHeight),
        child: new Padding(
          padding: const EdgeInsets.symmetric(horizontal: 16.0),
          child: new UnconstrainedBox(
            constrainedAxis: Axis.horizontal,
            child: new Row(children: children),
          ),
        )
      ),
Adam Barth's avatar
Adam Barth committed
464 465 466
    );
  }
}