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

import 'package:flutter/widgets.dart';

import 'checkbox.dart';
8
import 'checkbox_theme.dart';
9
import 'list_tile.dart';
10
import 'list_tile_theme.dart';
11
import 'material_state.dart';
12
import 'theme.dart';
13
import 'theme_data.dart';
14

15
// Examples can assume:
16
// late bool? _throwShotAway;
17 18
// void setState(VoidCallback fn) { }

19 20
enum _CheckboxType { material, adaptive }

21 22 23 24 25
/// A [ListTile] with a [Checkbox]. In other words, a checkbox with a label.
///
/// The entire list tile is interactive: tapping anywhere in the tile toggles
/// the checkbox.
///
26 27
/// {@youtube 560 315 https://www.youtube.com/watch?v=RkSqPAn9szs}
///
28
/// The [value], [onChanged], [activeColor] and [checkColor] properties of this widget are
29 30
/// identical to the similarly-named properties on the [Checkbox] widget.
///
31
/// The [title], [subtitle], [isThreeLine], [dense], and [contentPadding] properties are like
32 33 34
/// those of the same name on [ListTile].
///
/// The [selected] property on this widget is similar to the [ListTile.selected]
35
/// property. This tile's [activeColor] is used for the selected item's text color, or
36
/// the theme's [CheckboxThemeData.overlayColor] if [activeColor] is null.
37 38
///
/// This widget does not coordinate the [selected] state and the [value] state; to have the list tile
39
/// appear selected when the checkbox is checked, pass the same value to both.
40 41 42 43 44 45
///
/// The checkbox is shown on the right by default in left-to-right languages
/// (i.e. the trailing edge). This can be changed using [controlAffinity]. The
/// [secondary] widget is placed on the opposite side. This maps to the
/// [ListTile.leading] and [ListTile.trailing] properties of [ListTile].
///
46 47 48 49 50 51 52 53 54
/// 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], and [selectedTileColor] are not painted by the
/// [CheckboxListTile] itself but by the [Material] widget ancestor.
/// In this case, one can wrap a [Material] widget around the [CheckboxListTile],
/// e.g.:
///
/// {@tool snippet}
/// ```dart
55
/// ColoredBox(
56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
///   color: Colors.green,
///   child: Material(
///     child: CheckboxListTile(
///       tileColor: Colors.red,
///       title: const Text('CheckboxListTile with red background'),
///       value: true,
///       onChanged:(bool? value) { },
///     ),
///   ),
/// )
/// ```
/// {@end-tool}
///
/// ## Performance considerations when wrapping [CheckboxListTile] with [Material]
///
/// Wrapping a large number of [CheckboxListTile]s individually with [Material]s
/// is expensive. Consider only wrapping the [CheckboxListTile]s that require it
/// or include a common [Material] ancestor where possible.
///
75 76 77
/// To show the [CheckboxListTile] as disabled, pass null as the [onChanged]
/// callback.
///
78
/// {@tool dartpad}
79
/// ![CheckboxListTile sample](https://flutter.github.io/assets-for-api-docs/assets/material/checkbox_list_tile.png)
80 81 82 83
///
/// This widget shows a checkbox that, when checked, slows down all animations
/// (including the animation of the checkbox itself getting checked!).
///
84 85 86
/// This sample requires that you also import 'package:flutter/scheduler.dart',
/// so that you can reference [timeDilation].
///
87
/// ** See code in examples/api/lib/material/checkbox_list_tile/checkbox_list_tile.0.dart **
88 89
/// {@end-tool}
///
90
/// {@tool dartpad}
91 92
/// This sample demonstrates how [CheckboxListTile] positions the checkbox widget
/// relative to the text in different configurations.
93 94 95 96
///
/// ** See code in examples/api/lib/material/checkbox_list_tile/checkbox_list_tile.1.dart **
/// {@end-tool}
///
97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113
/// ## Semantics in CheckboxListTile
///
/// Since the entirety of the CheckboxListTile is interactive, it should represent
/// itself as a single interactive entity.
///
/// To do so, a CheckboxListTile widget wraps its children with a [MergeSemantics]
/// widget. [MergeSemantics] will attempt to merge its descendant [Semantics]
/// nodes into one node in the semantics tree. Therefore, CheckboxListTile will
/// throw an error if any of its children requires its own [Semantics] node.
///
/// For example, you cannot nest a [RichText] widget as a descendant of
/// CheckboxListTile. [RichText] has an embedded gesture recognizer that
/// requires its own [Semantics] node, which directly conflicts with
/// CheckboxListTile's desire to merge all its descendants' semantic nodes
/// into one. Therefore, it may be necessary to create a custom radio tile
/// widget to accommodate similar use cases.
///
114
/// {@tool dartpad}
115 116 117 118 119 120
/// ![Checkbox list tile semantics sample](https://flutter.github.io/assets-for-api-docs/assets/material/checkbox_list_tile_semantics.png)
///
/// Here is an example of a custom labeled checkbox widget, called
/// LinkedLabelCheckbox, that includes an interactive [RichText] widget that
/// handles tap gestures.
///
121
/// ** See code in examples/api/lib/material/checkbox_list_tile/custom_labeled_checkbox.0.dart **
122 123 124 125 126 127 128 129 130
/// {@end-tool}
///
/// ## CheckboxListTile isn't exactly what I want
///
/// If the way CheckboxListTile pads and positions its elements isn't quite
/// what you're looking for, you can create custom labeled checkbox widgets by
/// combining [Checkbox] with other widgets, such as [Text], [Padding] and
/// [InkWell].
///
131
/// {@tool dartpad}
132 133 134 135 136
/// ![Custom checkbox list tile sample](https://flutter.github.io/assets-for-api-docs/assets/material/checkbox_list_tile_custom.png)
///
/// Here is an example of a custom LabeledCheckbox widget, but you can easily
/// make your own configurable widget.
///
137
/// ** See code in examples/api/lib/material/checkbox_list_tile/custom_labeled_checkbox.1.dart **
138
/// {@end-tool}
139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157
///
/// See also:
///
///  * [ListTileTheme], which can be used to affect the style of list tiles,
///    including checkbox list tiles.
///  * [RadioListTile], a similar widget for radio buttons.
///  * [SwitchListTile], a similar widget for switches.
///  * [ListTile] and [Checkbox], the widgets from which this widget is made.
class CheckboxListTile extends StatelessWidget {
  /// Creates a combination of a list tile and a checkbox.
  ///
  /// The checkbox tile itself does not maintain any state. Instead, when the
  /// state of the checkbox changes, the widget calls the [onChanged] callback.
  /// Most widgets that use a checkbox will listen for the [onChanged] callback
  /// and rebuild the checkbox tile with a new [value] to update the visual
  /// appearance of the checkbox.
  ///
  /// The following arguments are required:
  ///
158 159
  /// * [value], which determines whether the checkbox is checked. The [value]
  ///   can only be null if [tristate] is true.
160 161
  /// * [onChanged], which is called when the value of the checkbox should
  ///   change. It can be set to null to disable the checkbox.
162 163
  ///
  /// The value of [tristate] must not be null.
164
  const CheckboxListTile({
165
    super.key,
166 167
    required this.value,
    required this.onChanged,
168
    this.mouseCursor,
169
    this.activeColor,
170
    this.fillColor,
171
    this.checkColor,
172 173 174 175 176 177 178 179 180 181
    this.hoverColor,
    this.overlayColor,
    this.splashRadius,
    this.materialTapTargetSize,
    this.visualDensity,
    this.focusNode,
    this.autofocus = false,
    this.shape,
    this.side,
    this.isError = false,
182
    this.enabled,
183
    this.tileColor,
184 185
    this.title,
    this.subtitle,
186
    this.isThreeLine = false,
187 188
    this.dense,
    this.secondary,
189 190
    this.selected = false,
    this.controlAffinity = ListTileControlAffinity.platform,
191
    this.contentPadding,
192
    this.tristate = false,
193
    this.checkboxShape,
194
    this.selectedTileColor,
195
    this.onFocusChange,
196
    this.enableFeedback,
197
    this.checkboxSemanticLabel,
198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240
  }) : _checkboxType = _CheckboxType.material,
       assert(tristate || value != null),
       assert(!isThreeLine || subtitle != null);

  /// Creates a combination of a list tile and a platform adaptive checkbox.
  ///
  /// The checkbox uses [Checkbox.adaptive] to show a [CupertinoCheckbox] for
  /// iOS platforms, or [Checkbox] for all others.
  ///
  /// All other properties are the same as [CheckboxListTile].
  const CheckboxListTile.adaptive({
    super.key,
    required this.value,
    required this.onChanged,
    this.mouseCursor,
    this.activeColor,
    this.fillColor,
    this.checkColor,
    this.hoverColor,
    this.overlayColor,
    this.splashRadius,
    this.materialTapTargetSize,
    this.visualDensity,
    this.focusNode,
    this.autofocus = false,
    this.shape,
    this.side,
    this.isError = false,
    this.enabled,
    this.tileColor,
    this.title,
    this.subtitle,
    this.isThreeLine = false,
    this.dense,
    this.secondary,
    this.selected = false,
    this.controlAffinity = ListTileControlAffinity.platform,
    this.contentPadding,
    this.tristate = false,
    this.checkboxShape,
    this.selectedTileColor,
    this.onFocusChange,
    this.enableFeedback,
241
    this.checkboxSemanticLabel,
242 243
  }) : _checkboxType = _CheckboxType.adaptive,
       assert(tristate || value != null),
244
       assert(!isThreeLine || subtitle != null);
245 246

  /// Whether this checkbox is checked.
247
  final bool? value;
248 249 250 251 252 253 254 255 256

  /// Called when the value of the checkbox should change.
  ///
  /// The checkbox passes the new value to the callback but does not actually
  /// change state until the parent widget rebuilds the checkbox tile with the
  /// new value.
  ///
  /// If null, the checkbox will be displayed as disabled.
  ///
257 258
  /// {@tool snippet}
  ///
259 260 261 262 263
  /// The callback provided to [onChanged] should update the state of the parent
  /// [StatefulWidget] using the [State.setState] method, so that the parent
  /// gets rebuilt; for example:
  ///
  /// ```dart
264
  /// CheckboxListTile(
265
  ///   value: _throwShotAway,
266
  ///   onChanged: (bool? newValue) {
267 268 269 270
  ///     setState(() {
  ///       _throwShotAway = newValue;
  ///     });
  ///   },
271
  ///   title: const Text('Throw away your shot'),
272
  /// )
273
  /// ```
274
  /// {@end-tool}
275
  final ValueChanged<bool?>? onChanged;
276

277 278 279 280 281 282 283 284 285 286 287 288 289 290
  /// 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.hovered].
  ///  * [MaterialState.disabled].
  ///
  /// If null, then the value of [CheckboxThemeData.mouseCursor] is used. If
  /// that is also null, then [MaterialStateMouseCursor.clickable] is used.
  final MouseCursor? mouseCursor;

291 292
  /// The color to use when this checkbox is checked.
  ///
293
  /// Defaults to [ColorScheme.secondary] of the current [Theme].
294
  final Color? activeColor;
295

296 297 298 299 300 301 302 303 304 305 306 307
  /// The color that fills the checkbox.
  ///
  /// Resolves in the following states:
  ///  * [MaterialState.selected].
  ///  * [MaterialState.hovered].
  ///  * [MaterialState.disabled].
  ///
  /// If null, then the value of [activeColor] is used in the selected
  /// state. If that is also null, the value of [CheckboxThemeData.fillColor]
  /// is used. If that is also null, then the default value is used.
  final MaterialStateProperty<Color?>? fillColor;

308 309 310
  /// The color to use for the check icon when this checkbox is checked.
  ///
  /// Defaults to Color(0xFFFFFFFF).
311
  final Color? checkColor;
312

313 314 315 316 317 318 319 320 321 322 323 324 325
  /// {@macro flutter.material.checkbox.hoverColor}
  final Color? hoverColor;

  /// The color for the checkbox's [Material].
  ///
  /// Resolves in the following states:
  ///  * [MaterialState.pressed].
  ///  * [MaterialState.selected].
  ///  * [MaterialState.hovered].
  ///
  /// If null, then the value of [activeColor] with alpha [kRadialReactionAlpha]
  /// and [hoverColor] is used in the pressed and hovered state. If that is also null,
  /// the value of [CheckboxThemeData.overlayColor] is used. If that is also null,
326
  /// then the default value is used in the pressed and hovered state.
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 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368
  final MaterialStateProperty<Color?>? overlayColor;

  /// {@macro flutter.material.checkbox.splashRadius}
  ///
  /// If null, then the value of [CheckboxThemeData.splashRadius] is used. If
  /// that is also null, then [kRadialReactionRadius] is used.
  final double? splashRadius;

  /// {@macro flutter.material.checkbox.materialTapTargetSize}
  ///
  /// Defaults to [MaterialTapTargetSize.shrinkWrap].
  final MaterialTapTargetSize? materialTapTargetSize;

  /// Defines how compact the list tile's layout will be.
  ///
  /// {@macro flutter.material.themedata.visualDensity}
  final VisualDensity? visualDensity;


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

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

  /// {@macro flutter.material.ListTile.shape}
  final ShapeBorder? shape;

  /// {@macro flutter.material.checkbox.side}
  ///
  /// The given value is passed directly to [Checkbox.side].
  ///
  /// If this property is null, then [CheckboxThemeData.side] of
  /// [ThemeData.checkboxTheme] is used. If that is also null, then the side
  /// will be width 2.
  final BorderSide? side;

  /// {@macro flutter.material.checkbox.isError}
  ///
  /// Defaults to false.
  final bool isError;

369 370 371
  /// {@macro flutter.material.ListTile.tileColor}
  final Color? tileColor;

372 373 374
  /// The primary content of the list tile.
  ///
  /// Typically a [Text] widget.
375
  final Widget? title;
376 377 378 379

  /// Additional content displayed below the title.
  ///
  /// Typically a [Text] widget.
380
  final Widget? subtitle;
381 382 383 384

  /// A widget to display on the opposite side of the tile from the checkbox.
  ///
  /// Typically an [Icon] widget.
385
  final Widget? secondary;
386 387 388 389 390 391 392 393 394

  /// 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.
  ///
395
  /// If this property is null then its value is based on [ListTileThemeData.dense].
396
  final bool? dense;
397 398 399 400 401 402 403 404 405 406 407 408 409

  /// Whether to render icons and text in the [activeColor].
  ///
  /// No effort is made to automatically coordinate the [selected] state and the
  /// [value] state. To have the list tile appear selected when the checkbox is
  /// checked, pass the same value to both.
  ///
  /// Normally, this property is left to its default value, false.
  final bool selected;

  /// Where to place the control relative to the text.
  final ListTileControlAffinity controlAffinity;

410 411 412 413 414
  /// Defines insets surrounding the tile's contents.
  ///
  /// This value will surround the [Checkbox], [title], [subtitle], and [secondary]
  /// widgets in [CheckboxListTile].
  ///
415
  /// When the value is null, the [contentPadding] is `EdgeInsets.symmetric(horizontal: 16.0)`.
416
  final EdgeInsetsGeometry? contentPadding;
417

418 419 420 421 422 423 424 425 426 427 428 429
  /// If true the checkbox's [value] can be true, false, or null.
  ///
  /// Checkbox displays a dash when its value is null.
  ///
  /// When a tri-state checkbox ([tristate] is true) is tapped, its [onChanged]
  /// callback will be applied to true if the current value is false, to null if
  /// value is true, and to false if value is null (i.e. it cycles through false
  /// => true => null => false when tapped).
  ///
  /// If tristate is false (the default), [value] must not be null.
  final bool tristate;

430 431 432 433 434 435 436
  /// {@macro flutter.material.checkbox.shape}
  ///
  /// If this property is null then [CheckboxThemeData.shape] of [ThemeData.checkboxTheme]
  /// is used. If that's null then the shape will be a [RoundedRectangleBorder]
  /// with a circular corner radius of 1.0.
  final OutlinedBorder? checkboxShape;

437 438 439
  /// If non-null, defines the background color when [CheckboxListTile.selected] is true.
  final Color? selectedTileColor;

440 441 442
  /// {@macro flutter.material.inkwell.onFocusChange}
  final ValueChanged<bool>? onFocusChange;

443 444 445 446 447 448 449
  /// {@macro flutter.material.ListTile.enableFeedback}
  ///
  /// See also:
  ///
  ///  * [Feedback] for providing platform-specific feedback to certain actions.
  final bool? enableFeedback;

450 451 452 453 454 455 456
  /// Whether the CheckboxListTile is interactive.
  ///
  /// If false, this list tile is styled with the disabled color from the
  /// current [Theme] and the [ListTile.onTap] callback is
  /// inoperative.
  final bool? enabled;

457 458 459
  /// {@macro flutter.material.checkbox.semanticLabel}
  final String? checkboxSemanticLabel;

460 461
  final _CheckboxType _checkboxType;

462 463 464 465
  void _handleValueChange() {
    assert(onChanged != null);
    switch (value) {
      case false:
466
        onChanged!(true);
467
      case true:
468 469 470
        onChanged!(tristate ? null : false);
      case null:
        onChanged!(false);
471 472 473
    }
  }

474 475
  @override
  Widget build(BuildContext context) {
476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495
    final Widget control;

    switch (_checkboxType) {
      case _CheckboxType.material:
        control = Checkbox(
          value: value,
          onChanged: enabled ?? true ? onChanged : null,
          mouseCursor: mouseCursor,
          activeColor: activeColor,
          fillColor: fillColor,
          checkColor: checkColor,
          hoverColor: hoverColor,
          overlayColor: overlayColor,
          splashRadius: splashRadius,
          materialTapTargetSize: materialTapTargetSize ?? MaterialTapTargetSize.shrinkWrap,
          autofocus: autofocus,
          tristate: tristate,
          shape: checkboxShape,
          side: side,
          isError: isError,
496
          semanticLabel: checkboxSemanticLabel,
497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514
        );
      case _CheckboxType.adaptive:
        control = Checkbox.adaptive(
          value: value,
          onChanged: enabled ?? true ? onChanged : null,
          mouseCursor: mouseCursor,
          activeColor: activeColor,
          fillColor: fillColor,
          checkColor: checkColor,
          hoverColor: hoverColor,
          overlayColor: overlayColor,
          splashRadius: splashRadius,
          materialTapTargetSize: materialTapTargetSize ?? MaterialTapTargetSize.shrinkWrap,
          autofocus: autofocus,
          tristate: tristate,
          shape: checkboxShape,
          side: side,
          isError: isError,
515
          semanticLabel: checkboxSemanticLabel,
516 517 518
        );
    }

519
    Widget? leading, trailing;
520 521 522 523 524 525 526 527 528
    switch (controlAffinity) {
      case ListTileControlAffinity.leading:
        leading = control;
        trailing = secondary;
      case ListTileControlAffinity.trailing:
      case ListTileControlAffinity.platform:
        leading = secondary;
        trailing = control;
    }
529 530 531 532 533 534 535 536
    final ThemeData theme = Theme.of(context);
    final CheckboxThemeData checkboxTheme = CheckboxTheme.of(context);
    final Set<MaterialState> states = <MaterialState>{
      if (selected) MaterialState.selected,
    };
    final Color effectiveActiveColor = activeColor
      ?? checkboxTheme.fillColor?.resolve(states)
      ?? theme.colorScheme.secondary;
537
    return MergeSemantics(
538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555
      child: ListTile(
        selectedColor: effectiveActiveColor,
        leading: leading,
        title: title,
        subtitle: subtitle,
        trailing: trailing,
        isThreeLine: isThreeLine,
        dense: dense,
        enabled: enabled ?? onChanged != null,
        onTap: onChanged != null ? _handleValueChange : null,
        selected: selected,
        autofocus: autofocus,
        contentPadding: contentPadding,
        shape: shape,
        selectedTileColor: selectedTileColor,
        tileColor: tileColor,
        visualDensity: visualDensity,
        focusNode: focusNode,
556
        onFocusChange: onFocusChange,
557
        enableFeedback: enableFeedback,
558 559 560 561
      ),
    );
  }
}