switch_list_tile.dart 14.6 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 'list_tile.dart';
import 'switch.dart';
import 'theme.dart';
10
import 'theme_data.dart';
11

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

16 17
enum _SwitchListTileType { material, adaptive }

18 19
/// A [ListTile] with a [Switch]. In other words, a switch with a label.
///
20 21
/// {@youtube 560 315 https://www.youtube.com/watch?v=0igIjvtEWNU}
///
22
/// The entire list tile is interactive: tapping anywhere in the tile toggles
23 24 25 26 27 28
/// the switch. Tapping and dragging the [Switch] also triggers the [onChanged]
/// callback.
///
/// To ensure that [onChanged] correctly triggers, the state passed
/// into [value] must be properly managed. This is typically done by invoking
/// [State.setState] in [onChanged] to toggle the state value.
29 30 31 32 33 34 35 36 37
///
/// The [value], [onChanged], [activeColor], [activeThumbImage], and
/// [inactiveThumbImage] properties of this widget are identical to the
/// similarly-named properties on the [Switch] 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]
38 39 40 41 42 43
/// property. This tile's [activeColor] is used for the selected item's text color, or
/// the theme's [ThemeData.toggleableActiveColor] if [activeColor] is null.
///
/// This widget does not coordinate the [selected] state and the
/// [value]; to have the list tile appear selected when the
/// switch button is on, use the same value for both.
44 45
///
/// The switch is shown on the right by default in left-to-right languages (i.e.
46 47
/// in the [ListTile.trailing] slot) which can be changed using [controlAffinity].
/// The [secondary] widget is placed in the [ListTile.leading] slot.
48
///
49 50 51
/// To show the [SwitchListTile] as disabled, pass null as the [onChanged]
/// callback.
///
52
/// {@tool dartpad}
53
/// ![SwitchListTile sample](https://flutter.github.io/assets-for-api-docs/assets/material/switch_list_tile.png)
54 55 56 57
///
/// This widget shows a switch that, when toggled, changes the state of a [bool]
/// member field called `_lights`.
///
58
/// ** See code in examples/api/lib/material/switch_list_tile/switch_list_tile.0.dart **
59
/// {@end-tool}
60
///
61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77
/// ## Semantics in SwitchListTile
///
/// Since the entirety of the SwitchListTile is interactive, it should represent
/// itself as a single interactive entity.
///
/// To do so, a SwitchListTile 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, SwitchListTile 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
/// SwitchListTile. [RichText] has an embedded gesture recognizer that
/// requires its own [Semantics] node, which directly conflicts with
/// SwitchListTile'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.
///
78
/// {@tool dartpad}
79 80 81 82 83 84
/// ![Switch list tile semantics sample](https://flutter.github.io/assets-for-api-docs/assets/material/switch_list_tile_semantics.png)
///
/// Here is an example of a custom labeled radio widget, called
/// LinkedLabelRadio, that includes an interactive [RichText] widget that
/// handles tap gestures.
///
85
/// ** See code in examples/api/lib/material/switch_list_tile/switch_list_tile.1.dart **
86 87 88 89 90 91 92 93 94
/// {@end-tool}
///
/// ## SwitchListTile isn't exactly what I want
///
/// If the way SwitchListTile pads and positions its elements isn't quite what
/// you're looking for, you can create custom labeled switch widgets by
/// combining [Switch] with other widgets, such as [Text], [Padding] and
/// [InkWell].
///
95
/// {@tool dartpad}
96 97 98 99 100
/// ![Custom switch list tile sample](https://flutter.github.io/assets-for-api-docs/assets/material/switch_list_tile_custom.png)
///
/// Here is an example of a custom LabeledSwitch widget, but you can easily
/// make your own configurable widget.
///
101
/// ** See code in examples/api/lib/material/switch_list_tile/switch_list_tile.2.dart **
102 103
/// {@end-tool}
///
104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124
/// See also:
///
///  * [ListTileTheme], which can be used to affect the style of list tiles,
///    including switch list tiles.
///  * [CheckboxListTile], a similar widget for checkboxes.
///  * [RadioListTile], a similar widget for radio buttons.
///  * [ListTile] and [Switch], the widgets from which this widget is made.
class SwitchListTile extends StatelessWidget {
  /// Creates a combination of a list tile and a switch.
  ///
  /// The switch tile itself does not maintain any state. Instead, when the
  /// state of the switch changes, the widget calls the [onChanged] callback.
  /// Most widgets that use a switch will listen for the [onChanged] callback
  /// and rebuild the switch tile with a new [value] to update the visual
  /// appearance of the switch.
  ///
  /// The following arguments are required:
  ///
  /// * [value] determines whether this switch is on or off.
  /// * [onChanged] is called when the user toggles the switch on or off.
  const SwitchListTile({
125 126 127
    Key? key,
    required this.value,
    required this.onChanged,
128
    this.tileColor,
129
    this.activeColor,
130 131 132
    this.activeTrackColor,
    this.inactiveThumbColor,
    this.inactiveTrackColor,
133 134 135 136
    this.activeThumbImage,
    this.inactiveThumbImage,
    this.title,
    this.subtitle,
137
    this.isThreeLine = false,
138
    this.dense,
139
    this.contentPadding,
140
    this.secondary,
141
    this.selected = false,
142
    this.autofocus = false,
143
    this.controlAffinity = ListTileControlAffinity.platform,
144
    this.shape,
145
    this.selectedTileColor,
146 147 148
    this.visualDensity,
    this.focusNode,
    this.enableFeedback,
149
    this.hoverColor,
150 151 152 153 154
  }) : _switchListTileType = _SwitchListTileType.material,
       assert(value != null),
       assert(isThreeLine != null),
       assert(!isThreeLine || subtitle != null),
       assert(selected != null),
155
       assert(autofocus != null),
156 157
       super(key: key);

158 159 160
  /// Creates a Material [ListTile] with an adaptive [Switch], following
  /// Material design's
  /// [Cross-platform guidelines](https://material.io/design/platform-guidance/cross-platform-adaptation.html).
161
  ///
162 163 164 165
  /// This widget uses [Switch.adaptive] to change the graphics of the switch
  /// component based on the ambient [ThemeData.platform]. On iOS and macOS, a
  /// [CupertinoSwitch] will be used. On other platforms a Material design
  /// [Switch] will be used.
166 167 168
  ///
  /// If a [CupertinoSwitch] is created, the following parameters are
  /// ignored: [activeTrackColor], [inactiveThumbColor], [inactiveTrackColor],
169
  /// [activeThumbImage], [inactiveThumbImage].
170
  const SwitchListTile.adaptive({
171 172 173
    Key? key,
    required this.value,
    required this.onChanged,
174
    this.tileColor,
175 176 177 178 179 180 181 182 183 184
    this.activeColor,
    this.activeTrackColor,
    this.inactiveThumbColor,
    this.inactiveTrackColor,
    this.activeThumbImage,
    this.inactiveThumbImage,
    this.title,
    this.subtitle,
    this.isThreeLine = false,
    this.dense,
185
    this.contentPadding,
186 187
    this.secondary,
    this.selected = false,
188
    this.autofocus = false,
189
    this.controlAffinity = ListTileControlAffinity.platform,
190
    this.shape,
191
    this.selectedTileColor,
192 193 194
    this.visualDensity,
    this.focusNode,
    this.enableFeedback,
195
    this.hoverColor,
196 197
  }) : _switchListTileType = _SwitchListTileType.adaptive,
       assert(value != null),
198 199 200
       assert(isThreeLine != null),
       assert(!isThreeLine || subtitle != null),
       assert(selected != null),
201
       assert(autofocus != null),
202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221
       super(key: key);

  /// Whether this switch is checked.
  ///
  /// This property must not be null.
  final bool value;

  /// Called when the user toggles the switch on or off.
  ///
  /// The switch passes the new value to the callback but does not actually
  /// change state until the parent widget rebuilds the switch tile with the
  /// new value.
  ///
  /// If null, the switch 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
222
  /// SwitchListTile(
223
  ///   value: _isSelected,
224 225
  ///   onChanged: (bool newValue) {
  ///     setState(() {
226
  ///       _isSelected = newValue;
227 228
  ///     });
  ///   },
229
  ///   title: Text('Selection'),
230
  /// )
231
  /// ```
232
  final ValueChanged<bool>? onChanged;
233 234 235 236

  /// The color to use when this switch is on.
  ///
  /// Defaults to accent color of the current [Theme].
237
  final Color? activeColor;
238

239 240 241
  /// The color to use on the track when this switch is on.
  ///
  /// Defaults to [ThemeData.toggleableActiveColor] with the opacity set at 50%.
242 243
  ///
  /// Ignored if created with [SwitchListTile.adaptive].
244
  final Color? activeTrackColor;
245 246 247 248

  /// The color to use on the thumb when this switch is off.
  ///
  /// Defaults to the colors described in the Material design specification.
249 250
  ///
  /// Ignored if created with [SwitchListTile.adaptive].
251
  final Color? inactiveThumbColor;
252 253 254 255

  /// The color to use on the track when this switch is off.
  ///
  /// Defaults to the colors described in the Material design specification.
256 257
  ///
  /// Ignored if created with [SwitchListTile.adaptive].
258
  final Color? inactiveTrackColor;
259

260 261 262
  /// {@macro flutter.material.ListTile.tileColor}
  final Color? tileColor;

263
  /// An image to use on the thumb of this switch when the switch is on.
264
  final ImageProvider? activeThumbImage;
265 266

  /// An image to use on the thumb of this switch when the switch is off.
267 268
  ///
  /// Ignored if created with [SwitchListTile.adaptive].
269
  final ImageProvider? inactiveThumbImage;
270 271 272 273

  /// The primary content of the list tile.
  ///
  /// Typically a [Text] widget.
274
  final Widget? title;
275 276 277 278

  /// Additional content displayed below the title.
  ///
  /// Typically a [Text] widget.
279
  final Widget? subtitle;
280 281 282 283

  /// A widget to display on the opposite side of the tile from the switch.
  ///
  /// Typically an [Icon] widget.
284
  final Widget? secondary;
285 286 287 288 289 290 291 292 293

  /// 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.
  ///
294
  /// If this property is null then its value is based on [ListTileThemeData.dense].
295
  final bool? dense;
296

297 298 299 300 301 302 303
  /// The tile's internal padding.
  ///
  /// Insets a [SwitchListTile]'s contents: its [title], [subtitle],
  /// [secondary], and [Switch] widgets.
  ///
  /// If null, [ListTile]'s default of `EdgeInsets.symmetric(horizontal: 16.0)`
  /// is used.
304
  final EdgeInsetsGeometry? contentPadding;
305

306 307 308 309 310 311 312 313 314
  /// 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 switch is
  /// on, pass the same value to both.
  ///
  /// Normally, this property is left to its default value, false.
  final bool selected;

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

318 319 320
  /// If adaptive, creates the switch with [Switch.adaptive].
  final _SwitchListTileType _switchListTileType;

321 322 323 324 325
  /// Defines the position of control and [secondary], relative to text.
  ///
  /// By default, the value of `controlAffinity` is [ListTileControlAffinity.platform].
  final ListTileControlAffinity controlAffinity;

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

329 330 331
  /// If non-null, defines the background color when [SwitchListTile.selected] is true.
  final Color? selectedTileColor;

332 333 334 335 336 337 338 339 340 341 342 343 344 345 346
  /// 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.material.ListTile.enableFeedback}
  ///
  /// See also:
  ///
  ///  * [Feedback] for providing platform-specific feedback to certain actions.
  final bool? enableFeedback;

347 348 349
  /// The color for the tile's [Material] when a pointer is hovering over it.
  final Color? hoverColor;

350 351
  @override
  Widget build(BuildContext context) {
352
    final Widget control;
353 354 355 356 357 358 359 360 361 362 363 364
    switch (_switchListTileType) {
      case _SwitchListTileType.adaptive:
        control = Switch.adaptive(
          value: value,
          onChanged: onChanged,
          activeColor: activeColor,
          activeThumbImage: activeThumbImage,
          inactiveThumbImage: inactiveThumbImage,
          materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
          activeTrackColor: activeTrackColor,
          inactiveTrackColor: inactiveTrackColor,
          inactiveThumbColor: inactiveThumbColor,
365
          autofocus: autofocus,
366 367 368 369 370 371 372 373 374 375 376 377 378 379
        );
        break;

      case _SwitchListTileType.material:
        control = Switch(
          value: value,
          onChanged: onChanged,
          activeColor: activeColor,
          activeThumbImage: activeThumbImage,
          inactiveThumbImage: inactiveThumbImage,
          materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
          activeTrackColor: activeTrackColor,
          inactiveTrackColor: inactiveTrackColor,
          inactiveThumbColor: inactiveThumbColor,
380
          autofocus: autofocus,
381 382
        );
    }
383

384
    Widget? leading, trailing;
385 386 387 388 389 390 391 392 393 394 395 396
    switch (controlAffinity) {
      case ListTileControlAffinity.leading:
        leading = control;
        trailing = secondary;
        break;
      case ListTileControlAffinity.trailing:
      case ListTileControlAffinity.platform:
        leading = secondary;
        trailing = control;
        break;
    }

397
    return MergeSemantics(
398
      child: ListTileTheme.merge(
399
        selectedColor: activeColor ?? Theme.of(context).toggleableActiveColor,
400
        child: ListTile(
401
          leading: leading,
402 403
          title: title,
          subtitle: subtitle,
404
          trailing: trailing,
405 406
          isThreeLine: isThreeLine,
          dense: dense,
407
          contentPadding: contentPadding,
408
          enabled: onChanged != null,
409
          onTap: onChanged != null ? () { onChanged!(!value); } : null,
410
          selected: selected,
411
          selectedTileColor: selectedTileColor,
412
          autofocus: autofocus,
413
          shape: shape,
414
          tileColor: tileColor,
415 416 417
          visualDensity: visualDensity,
          focusNode: focusNode,
          enableFeedback: enableFeedback,
418
          hoverColor: hoverColor,
419 420 421 422 423
        ),
      ),
    );
  }
}