checkbox_list_tile.dart 14.2 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 8 9
// 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';
import 'list_tile.dart';
import 'theme.dart';
10
import 'theme_data.dart';
11

12 13 14
// Examples can assume:
// void setState(VoidCallback fn) { }

15 16 17 18 19
/// 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.
///
20 21
/// {@youtube 560 315 https://www.youtube.com/watch?v=RkSqPAn9szs}
///
22
/// The [value], [onChanged], [activeColor] and [checkColor] properties of this widget are
23 24
/// identical to the similarly-named properties on the [Checkbox] widget.
///
25
/// The [title], [subtitle], [isThreeLine], [dense], and [contentPadding] properties are like
26 27 28 29 30 31 32 33 34 35 36 37 38
/// those of the same name on [ListTile].
///
/// The [selected] property on this widget is similar to the [ListTile.selected]
/// property, but the color used is that described by [activeColor], if any,
/// defaulting to the accent color of the current [Theme]. No effort is made to
/// 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.
///
/// 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].
///
39 40 41
/// To show the [CheckboxListTile] as disabled, pass null as the [onChanged]
/// callback.
///
42
/// {@tool dartpad --template=stateful_widget_scaffold_center}
43 44
///
/// ![CheckboxListTile sample](https://flutter.github.io/assets-for-api-docs/assets/material/checkbox_list_tile.png)
45 46 47 48
///
/// This widget shows a checkbox that, when checked, slows down all animations
/// (including the animation of the checkbox itself getting checked!).
///
49 50 51
/// This sample requires that you also import 'package:flutter/scheduler.dart',
/// so that you can reference [timeDilation].
///
52 53 54
/// ```dart imports
/// import 'package:flutter/scheduler.dart' show timeDilation;
/// ```
55
/// ```dart
56 57
/// @override
/// Widget build(BuildContext context) {
58 59 60 61 62 63 64
///   return CheckboxListTile(
///     title: const Text('Animate Slowly'),
///     value: timeDilation != 1.0,
///     onChanged: (bool value) {
///       setState(() { timeDilation = value ? 10.0 : 1.0; });
///     },
///     secondary: const Icon(Icons.hourglass_empty),
65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86
///   );
/// }
/// ```
/// {@end-tool}
///
/// ## 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.
///
87
/// {@tool sample --template=stateful_widget_scaffold_center}
88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149
///
/// ![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.
///
/// ```dart imports
/// import 'package:flutter/gestures.dart';
/// ```
/// ```dart preamble
/// class LinkedLabelCheckbox extends StatelessWidget {
///   const LinkedLabelCheckbox({
///     this.label,
///     this.padding,
///     this.value,
///     this.onChanged,
///   });
///
///   final String label;
///   final EdgeInsets padding;
///   final bool value;
///   final Function onChanged;
///
///   @override
///   Widget build(BuildContext context) {
///     return Padding(
///       padding: padding,
///       child: Row(
///         children: <Widget>[
///           Expanded(
///             child: RichText(
///               text: TextSpan(
///                 text: label,
///                 style: TextStyle(
///                   color: Colors.blueAccent,
///                   decoration: TextDecoration.underline,
///                 ),
///                 recognizer: TapGestureRecognizer()
///                   ..onTap = () {
///                   print('Label has been tapped.');
///                 },
///               ),
///             ),
///           ),
///           Checkbox(
///             value: value,
///             onChanged: (bool newValue) {
///               onChanged(newValue);
///             },
///           ),
///         ],
///       ),
///     );
///   }
/// }
/// ```
/// ```dart
/// bool _isSelected = false;
///
/// @override
/// Widget build(BuildContext context) {
150 151 152 153 154 155 156 157 158
///   return LinkedLabelCheckbox(
///     label: 'Linked, tappable label text',
///     padding: const EdgeInsets.symmetric(horizontal: 20.0),
///     value: _isSelected,
///     onChanged: (bool newValue) {
///       setState(() {
///         _isSelected = newValue;
///       });
///     },
159 160 161 162 163 164 165 166 167 168 169 170
///   );
/// }
/// ```
/// {@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].
///
171
/// {@tool dartpad --template=stateful_widget_scaffold_center}
172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220
///
/// ![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.
///
/// ```dart preamble
/// class LabeledCheckbox extends StatelessWidget {
///   const LabeledCheckbox({
///     this.label,
///     this.padding,
///     this.value,
///     this.onChanged,
///   });
///
///   final String label;
///   final EdgeInsets padding;
///   final bool value;
///   final Function onChanged;
///
///   @override
///   Widget build(BuildContext context) {
///     return InkWell(
///       onTap: () {
///         onChanged(!value);
///       },
///       child: Padding(
///         padding: padding,
///         child: Row(
///           children: <Widget>[
///             Expanded(child: Text(label)),
///             Checkbox(
///               value: value,
///               onChanged: (bool newValue) {
///                 onChanged(newValue);
///               },
///             ),
///           ],
///         ),
///       ),
///     );
///   }
/// }
/// ```
/// ```dart
/// bool _isSelected = false;
///
/// @override
/// Widget build(BuildContext context) {
221 222 223 224 225 226 227 228 229
///   return LabeledCheckbox(
///     label: 'This is the label text',
///     padding: const EdgeInsets.symmetric(horizontal: 20.0),
///     value: _isSelected,
///     onChanged: (bool newValue) {
///       setState(() {
///         _isSelected = newValue;
///       });
///     },
230 231
///   );
/// }
232
/// ```
233
/// {@end-tool}
234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252
///
/// 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:
  ///
253 254
  /// * [value], which determines whether the checkbox is checked. The [value]
  ///   can only be null if [tristate] is true.
255 256
  /// * [onChanged], which is called when the value of the checkbox should
  ///   change. It can be set to null to disable the checkbox.
257 258
  ///
  /// The value of [tristate] must not be null.
259
  const CheckboxListTile({
260 261 262
    Key? key,
    required this.value,
    required this.onChanged,
263
    this.activeColor,
264
    this.checkColor,
265 266
    this.title,
    this.subtitle,
267
    this.isThreeLine = false,
268 269
    this.dense,
    this.secondary,
270 271
    this.selected = false,
    this.controlAffinity = ListTileControlAffinity.platform,
272
    this.autofocus = false,
273
    this.contentPadding,
274
    this.tristate = false,
275
    this.shape,
276 277
  }) : assert(tristate != null),
       assert(tristate || value != null),
278 279 280 281
       assert(isThreeLine != null),
       assert(!isThreeLine || subtitle != null),
       assert(selected != null),
       assert(controlAffinity != null),
282
       assert(autofocus != null),
283 284 285
       super(key: key);

  /// Whether this checkbox is checked.
286
  final bool? value;
287 288 289 290 291 292 293 294 295 296 297 298 299 300

  /// 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.
  ///
  /// 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
301
  /// CheckboxListTile(
302 303 304 305 306 307
  ///   value: _throwShotAway,
  ///   onChanged: (bool newValue) {
  ///     setState(() {
  ///       _throwShotAway = newValue;
  ///     });
  ///   },
308
  ///   title: Text('Throw away your shot'),
309
  /// )
310
  /// ```
311
  final ValueChanged<bool?>? onChanged;
312 313 314 315

  /// The color to use when this checkbox is checked.
  ///
  /// Defaults to accent color of the current [Theme].
316
  final Color? activeColor;
317

318 319 320
  /// The color to use for the check icon when this checkbox is checked.
  ///
  /// Defaults to Color(0xFFFFFFFF).
321
  final Color? checkColor;
322

323 324 325
  /// The primary content of the list tile.
  ///
  /// Typically a [Text] widget.
326
  final Widget? title;
327 328 329 330

  /// Additional content displayed below the title.
  ///
  /// Typically a [Text] widget.
331
  final Widget? subtitle;
332 333 334 335

  /// A widget to display on the opposite side of the tile from the checkbox.
  ///
  /// Typically an [Icon] widget.
336
  final Widget? secondary;
337 338 339 340 341 342 343 344 345 346

  /// 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].
347
  final bool? dense;
348 349 350 351 352 353 354 355 356 357 358 359 360

  /// 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;

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

364 365 366 367 368 369
  /// Defines insets surrounding the tile's contents.
  ///
  /// This value will surround the [Checkbox], [title], [subtitle], and [secondary]
  /// widgets in [CheckboxListTile].
  ///
  /// When the value is null, the `contentPadding` is `EdgeInsets.symmetric(horizontal: 16.0)`.
370
  final EdgeInsetsGeometry? contentPadding;
371

372 373 374 375 376 377 378 379 380 381 382 383
  /// 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;

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

387 388 389 390
  void _handleValueChange() {
    assert(onChanged != null);
    switch (value) {
      case false:
391
        onChanged!(true);
392 393
        break;
      case true:
394
        onChanged!(tristate ? null : false);
395
        break;
396 397
      case null:
        onChanged!(false);
398 399 400 401
        break;
    }
  }

402 403
  @override
  Widget build(BuildContext context) {
404
    final Widget control = Checkbox(
405 406 407
      value: value,
      onChanged: onChanged,
      activeColor: activeColor,
408
      checkColor: checkColor,
409
      materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
410
      autofocus: autofocus,
411
      tristate: tristate,
412
    );
413
    Widget? leading, trailing;
414 415 416 417 418 419 420 421 422 423 424
    switch (controlAffinity) {
      case ListTileControlAffinity.leading:
        leading = control;
        trailing = secondary;
        break;
      case ListTileControlAffinity.trailing:
      case ListTileControlAffinity.platform:
        leading = secondary;
        trailing = control;
        break;
    }
425
    return MergeSemantics(
426
      child: ListTileTheme.merge(
427
        selectedColor: activeColor ?? Theme.of(context)!.accentColor,
428
        child: ListTile(
429 430 431 432 433 434 435
          leading: leading,
          title: title,
          subtitle: subtitle,
          trailing: trailing,
          isThreeLine: isThreeLine,
          dense: dense,
          enabled: onChanged != null,
436
          onTap: onChanged != null ? _handleValueChange : null,
437
          selected: selected,
438
          autofocus: autofocus,
439
          contentPadding: contentPadding,
440
          shape: shape,
441 442 443 444 445
        ),
      ),
    );
  }
}