blob: e684cfc1820165b40ee109f8e8a3e0c53f3188b5 [file] [log] [blame]
// Copyright 2014 The Flutter 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 'package:flutter/widgets.dart';
import 'checkbox.dart';
import 'list_tile.dart';
import 'theme.dart';
import 'theme_data.dart';
// Examples can assume:
// void setState(VoidCallback fn) { }
/// 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.
///
/// The [value], [onChanged], [activeColor] and [checkColor] properties of this widget are
/// identical to the similarly-named properties on the [Checkbox] widget.
///
/// The [title], [subtitle], [isThreeLine], and [dense] properties are like
/// 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].
///
/// To show the [CheckboxListTile] as disabled, pass null as the [onChanged]
/// callback.
///
/// {@tool dartpad --template=stateful_widget_scaffold_center}
///
/// ![CheckboxListTile sample](https://flutter.github.io/assets-for-api-docs/assets/material/checkbox_list_tile.png)
///
/// This widget shows a checkbox that, when checked, slows down all animations
/// (including the animation of the checkbox itself getting checked!).
///
/// This sample requires that you also import 'package:flutter/scheduler.dart',
/// so that you can reference [timeDilation].
///
/// ```dart imports
/// import 'package:flutter/scheduler.dart' show timeDilation;
/// ```
/// ```dart
/// @override
/// Widget build(BuildContext context) {
/// 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),
/// );
/// }
/// ```
/// {@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.
///
/// {@tool sample --template=stateful_widget_scaffold_center}
///
/// ![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) {
/// return LinkedLabelCheckbox(
/// label: 'Linked, tappable label text',
/// padding: const EdgeInsets.symmetric(horizontal: 20.0),
/// value: _isSelected,
/// onChanged: (bool newValue) {
/// setState(() {
/// _isSelected = newValue;
/// });
/// },
/// );
/// }
/// ```
/// {@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].
///
/// {@tool dartpad --template=stateful_widget_scaffold_center}
///
/// ![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) {
/// return LabeledCheckbox(
/// label: 'This is the label text',
/// padding: const EdgeInsets.symmetric(horizontal: 20.0),
/// value: _isSelected,
/// onChanged: (bool newValue) {
/// setState(() {
/// _isSelected = newValue;
/// });
/// },
/// );
/// }
/// ```
/// {@end-tool}
///
/// 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:
///
/// * [value], which determines whether the checkbox is checked, and must not
/// be null.
///
/// * [onChanged], which is called when the value of the checkbox should
/// change. It can be set to null to disable the checkbox.
const CheckboxListTile({
Key key,
@required this.value,
@required this.onChanged,
this.activeColor,
this.checkColor,
this.title,
this.subtitle,
this.isThreeLine = false,
this.dense,
this.secondary,
this.selected = false,
this.controlAffinity = ListTileControlAffinity.platform,
}) : assert(value != null),
assert(isThreeLine != null),
assert(!isThreeLine || subtitle != null),
assert(selected != null),
assert(controlAffinity != null),
super(key: key);
/// Whether this checkbox is checked.
///
/// This property must not be null.
final bool value;
/// 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
/// CheckboxListTile(
/// value: _throwShotAway,
/// onChanged: (bool newValue) {
/// setState(() {
/// _throwShotAway = newValue;
/// });
/// },
/// title: Text('Throw away your shot'),
/// )
/// ```
final ValueChanged<bool> onChanged;
/// The color to use when this checkbox is checked.
///
/// Defaults to accent color of the current [Theme].
final Color activeColor;
/// The color to use for the check icon when this checkbox is checked.
///
/// Defaults to Color(0xFFFFFFFF).
final Color checkColor;
/// 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 on the opposite side of the tile from the checkbox.
///
/// Typically an [Icon] widget.
final Widget secondary;
/// 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;
/// 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;
@override
Widget build(BuildContext context) {
final Widget control = Checkbox(
value: value,
onChanged: onChanged,
activeColor: activeColor,
checkColor: checkColor,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
);
Widget leading, trailing;
switch (controlAffinity) {
case ListTileControlAffinity.leading:
leading = control;
trailing = secondary;
break;
case ListTileControlAffinity.trailing:
case ListTileControlAffinity.platform:
leading = secondary;
trailing = control;
break;
}
return MergeSemantics(
child: ListTileTheme.merge(
selectedColor: activeColor ?? Theme.of(context).accentColor,
child: ListTile(
leading: leading,
title: title,
subtitle: subtitle,
trailing: trailing,
isThreeLine: isThreeLine,
dense: dense,
enabled: onChanged != null,
onTap: onChanged != null ? () { onChanged(!value); } : null,
selected: selected,
),
),
);
}
}