list_tile.dart 15.1 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';
11
import 'divider.dart';
Adam Barth's avatar
Adam Barth committed
12
import 'ink_well.dart';
Hans Muller's avatar
Hans Muller committed
13
import 'theme.dart';
Adam Barth's avatar
Adam Barth committed
14

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

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

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

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 78
  /// 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,
        );
      },
    );
  }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

288
    final Iterator<Widget> iterator = tiles.iterator;
Hans Muller's avatar
Hans Muller committed
289 290
    final bool isNotEmpty = iterator.moveNext();

291 292 293 294 295 296
    final Decoration decoration = new BoxDecoration(
      border: new Border(
        bottom: Divider.createBorderSide(context, color: color),
      ),
    );

297
    Widget tile = iterator.current;
298
    while (iterator.moveNext()) {
Hans Muller's avatar
Hans Muller committed
299
      yield new DecoratedBox(
300
        position: DecorationPosition.foreground,
301
        decoration: decoration,
302
        child: tile,
Hans Muller's avatar
Hans Muller committed
303
      );
304
      tile = iterator.current;
Hans Muller's avatar
Hans Muller committed
305 306
    }
    if (isNotEmpty)
307
      yield tile;
Hans Muller's avatar
Hans Muller committed
308 309
  }

310 311 312 313 314 315 316 317 318 319 320 321 322 323 324
  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
325
    }
326 327
    assert(theme.brightness != null);
    return null;
Hans Muller's avatar
Hans Muller committed
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 354 355
  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) {
356 357 358 359 360 361 362 363 364 365 366 367 368
    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;
    }
369 370 371 372 373 374 375
    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) {
376
    final TextStyle style = theme.textTheme.body1;
377 378 379 380
    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
381 382
  }

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

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

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

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

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

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

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

453
    return new InkWell(
454 455
      onTap: enabled ? onTap : null,
      onLongPress: enabled ? onLongPress : null,
456 457
      child: new Semantics(
        selected: selected,
458
        enabled: enabled,
459 460 461 462 463 464 465 466 467 468 469
        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 SafeArea(
                top: false,
                bottom: false,
                child: new Row(children: children),
              ),
470
            ),
471 472
          )
        ),
473
      ),
Adam Barth's avatar
Adam Barth committed
474 475 476
    );
  }
}