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

import 'dart:ui' show Color;

7 8 9
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';

10 11 12 13 14 15 16 17
/// Interactive states that some of the Material widgets can take on when
/// receiving input from the user.
///
/// States are defined by https://material.io/design/interaction/states.html#usage.
///
/// Some Material widgets track their current state in a `Set<MaterialState>`.
///
/// See also:
18
///
19
///  * [MaterialStateProperty], an interface for objects that "resolve" to
20 21 22 23
///    different values depending on a widget's material state.
///  * [MaterialStateColor], a [Color] that implements `MaterialStateProperty`
///    which is used in APIs that need to accept either a [Color] or a
///    `MaterialStateProperty<Color>`.
24
///  * [MaterialStateMouseCursor], a [MouseCursor] that implements `MaterialStateProperty`
25 26 27
///    which is used in APIs that need to accept either a [MouseCursor] or a
///    [MaterialStateProperty<MouseCursor>].

28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 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
enum MaterialState {
  /// The state when the user drags their mouse cursor over the given widget.
  ///
  /// See: https://material.io/design/interaction/states.html#hover.
  hovered,

  /// The state when the user navigates with the keyboard to a given widget.
  ///
  /// This can also sometimes be triggered when a widget is tapped. For example,
  /// when a [TextField] is tapped, it becomes [focused].
  ///
  /// See: https://material.io/design/interaction/states.html#focus.
  focused,

  /// The state when the user is actively pressing down on the given widget.
  ///
  /// See: https://material.io/design/interaction/states.html#pressed.
  pressed,

  /// The state when this widget is being dragged from one place to another by
  /// the user.
  ///
  /// https://material.io/design/interaction/states.html#dragged.
  dragged,

  /// The state when this item has been selected.
  ///
  /// This applies to things that can be toggled (such as chips and checkboxes)
  /// and things that are selected from a set of options (such as tabs and radio buttons).
  ///
  /// See: https://material.io/design/interaction/states.html#selected.
  selected,

  /// The state when this widget disabled and can not be interacted with.
  ///
  /// Disabled widgets should not respond to hover, focus, press, or drag
  /// interactions.
  ///
  /// See: https://material.io/design/interaction/states.html#disabled.
  disabled,

  /// The state when the widget has entered some form of invalid state.
  ///
  /// See https://material.io/design/interaction/states.html#usage.
  error,
}

75 76
/// Signature for the function that returns a value of type `T` based on a given
/// set of states.
77
typedef MaterialPropertyResolver<T> = T? Function(Set<MaterialState> states);
78

79
/// Defines a [Color] that is also a [MaterialStateProperty].
80
///
81 82 83 84 85 86
/// This class exists to enable widgets with [Color] valued properties
/// to also accept [MaterialStateProperty<Color>] values. A material
/// state color property represents a color which depends on
/// a widget's "interactive state". This state is represented as a
/// [Set] of [MaterialState]s, like [MaterialState.pressed],
/// [MaterialState.focused] and [MaterialState.hovered].
87 88 89 90 91 92
///
/// To use a [MaterialStateColor], you can either:
///   1. Create a subclass of [MaterialStateColor] and implement the abstract `resolve` method.
///   2. Use [MaterialStateColor.resolveWith] and pass in a callback that
///      will be used to resolve the color in the given states.
///
93 94 95 96 97 98 99 100
/// If a [MaterialStateColor] is used for a property or a parameter that doesn't
/// support resolving [MaterialStateProperty<Color>]s, then its default color
/// value will be used for all states.
///
/// To define a `const` [MaterialStateColor], you'll need to extend
/// [MaterialStateColor] and override its [resolve] method. You'll also need
/// to provide a `defaultValue` to the super constructor, so that we can know
/// at compile-time what its default color is.
101
///
102
/// {@tool snippet}
103
///
104
/// This example defines a `MaterialStateColor` with a const constructor.
105 106
///
/// ```dart
107 108 109 110 111 112 113 114 115 116 117 118
/// class MyColor extends MaterialStateColor {
///   static const int _defaultColor = 0xcafefeed;
///   static const int _pressedColor = 0xdeadbeef;
///
///   const MyColor() : super(_defaultColor);
///
///   @override
///   Color resolve(Set<MaterialState> states) {
///     if (states.contains(MaterialState.pressed)) {
///       return const Color(_pressedColor);
///     }
///     return const Color(_defaultColor);
119 120 121 122
///   }
/// }
/// ```
/// {@end-tool}
123
abstract class MaterialStateColor extends Color implements MaterialStateProperty<Color> {
124 125 126
  /// Creates a [MaterialStateColor].
  const MaterialStateColor(int defaultValue) : super(defaultValue);

127 128
  /// Creates a [MaterialStateColor] from a [MaterialPropertyResolver<Color>]
  /// callback function.
129 130 131 132 133 134
  ///
  /// If used as a regular color, the color resolved in the default state (the
  /// empty set of states) will be used.
  ///
  /// The given callback parameter must return a non-null color in the default
  /// state.
135
  static MaterialStateColor resolveWith(MaterialPropertyResolver<Color> callback) => _MaterialStateColor(callback);
136 137 138

  /// Returns a [Color] that's to be used when a Material component is in the
  /// specified state.
139
  @override
140
  Color? resolve(Set<MaterialState> states);
141 142
}

143 144
/// A [MaterialStateColor] created from a [MaterialPropertyResolver<Color>]
/// callback alone.
145 146 147 148 149 150
///
/// If used as a regular color, the color resolved in the default state will
/// be used.
///
/// Used by [MaterialStateColor.resolveWith].
class _MaterialStateColor extends MaterialStateColor {
151
  _MaterialStateColor(this._resolve) : super(_resolve(_defaultStates)!.value);
152

153
  final MaterialPropertyResolver<Color> _resolve;
154 155 156 157 158

  /// The default state for a Material component, the empty set of interaction states.
  static const Set<MaterialState> _defaultStates = <MaterialState>{};

  @override
159
  Color? resolve(Set<MaterialState> states) => _resolve(states);
160
}
161

162 163 164
/// Defines a [MouseCursor] whose value depends on a set of [MaterialState]s which
/// represent the interactive state of a component.
///
165 166 167 168 169 170 171
/// This kind of [MouseCursor] is useful when the set of interactive
/// actions a widget supports varies with its state. For example, a
/// mouse pointer hovering over a disabled [ListTile] should not
/// display [SystemMouseCursors.click], since a disabled list tile
/// doesn't respond to mouse clicks. [ListTile]'s default mouse cursor
/// is a [MaterialStateMouseCursor.clickable], which resolves to
/// [SystemMouseCursors.basic] when the button is disabled.
172 173 174 175
///
/// To use a [MaterialStateMouseCursor], you should create a subclass of
/// [MaterialStateMouseCursor] and implement the abstract `resolve` method.
///
176
/// {@tool dartpad --template=stateless_widget_scaffold_center}
177
///
178 179
/// This example defines a mouse cursor that resolves to
/// [SystemMouseCursors.forbidden] when its widget is disabled.
180
///
181 182 183
/// ```dart imports
/// import 'package:flutter/rendering.dart';
/// ```
184
///
185 186
/// ```dart preamble
/// class ListTileCursor extends MaterialStateMouseCursor {
187 188 189 190 191 192 193 194
///   @override
///   MouseCursor resolve(Set<MaterialState> states) {
///     if (states.contains(MaterialState.disabled)) {
///       return SystemMouseCursors.forbidden;
///     }
///     return SystemMouseCursors.click;
///   }
///   @override
195
///   String get debugDescription => 'ListTileCursor()';
196
/// }
197
/// ```
198
///
199 200 201 202 203 204 205
/// ```dart
/// Widget build(BuildContext context) {
///   return ListTile(
///     title: Text('Disabled ListTile'),
///     enabled: false,
///     mouseCursor: ListTileCursor(),
///   );
206 207 208 209
/// }
/// ```
/// {@end-tool}
///
210
/// This class should only be used for parameters which are documented to take
211
/// [MaterialStateMouseCursor], otherwise only the default state will be used.
212 213 214 215 216 217
///
/// See also:
///
///  * [MouseCursor] for introduction on the mouse cursor system.
///  * [SystemMouseCursors], which defines cursors that are supported by
///    native platforms.
218 219 220 221 222 223 224
abstract class MaterialStateMouseCursor extends MouseCursor implements MaterialStateProperty<MouseCursor> {
  /// Creates a [MaterialStateMouseCursor].
  const MaterialStateMouseCursor();

  @protected
  @override
  MouseCursorSession createSession(int device) {
225
    return resolve(<MaterialState>{})!.createSession(device);
226 227 228 229 230 231 232
  }

  /// Returns a [MouseCursor] that's to be used when a Material component is in
  /// the specified state.
  ///
  /// This method should never return null.
  @override
233
  MouseCursor? resolve(Set<MaterialState> states);
234 235 236 237 238 239 240 241

  /// A mouse cursor for clickable material widgets, which resolves differently
  /// when the widget is disabled.
  ///
  /// By default this cursor resolves to [SystemMouseCursors.click]. If the widget is
  /// disabled, the cursor resolves to [SystemMouseCursors.basic].
  ///
  /// This cursor is the default for many Material widgets.
242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259
  static const MaterialStateMouseCursor clickable = _EnabledAndDisabledMouseCursor(
    enabledCursor: SystemMouseCursors.click,
    disabledCursor: SystemMouseCursors.basic,
    name: 'clickable',
  );

  /// A mouse cursor for material widgets related to text, which resolves differently
  /// when the widget is disabled.
  ///
  /// By default this cursor resolves to [SystemMouseCursors.text]. If the widget is
  /// disabled, the cursor resolves to [SystemMouseCursors.basic].
  ///
  /// This cursor is the default for many Material widgets.
  static const MaterialStateMouseCursor textable = _EnabledAndDisabledMouseCursor(
    enabledCursor: SystemMouseCursors.text,
    disabledCursor: SystemMouseCursors.basic,
    name: 'textable',
  );
260 261
}

262 263
class _EnabledAndDisabledMouseCursor extends MaterialStateMouseCursor {
  const _EnabledAndDisabledMouseCursor({
264 265 266
    required this.enabledCursor,
    required this.disabledCursor,
    required this.name,
267 268 269 270 271
  });

  final MouseCursor enabledCursor;
  final MouseCursor disabledCursor;
  final String name;
272 273 274 275

  @override
  MouseCursor resolve(Set<MaterialState> states) {
    if (states.contains(MaterialState.disabled)) {
276
      return disabledCursor;
277
    }
278
    return enabledCursor;
279 280 281
  }

  @override
282
  String get debugDescription => 'MaterialStateMouseCursor($name)';
283 284
}

285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335
/// Interface for classes that [resolve] to a value of type `T` based
/// on a widget's interactive "state", which is defined as a set
/// of [MaterialState]s.
///
/// Material state properties represent values that depend on a widget's material
/// "state".  The state is encoded as a set of [MaterialState] values, like
/// [MaterialState.focused], [MaterialState.hovered], [MaterialState.pressed].  For
/// example the [InkWell.overlayColor] defines the color that fills the ink well
/// when it's pressed (the "splash color"), focused, or hovered. The [InkWell]
/// uses the overlay color's [resolve] method to compute the color for the
/// ink well's current state.
///
/// [ButtonStyle], which is used to configure the appearance of
/// buttons like [TextButton], [ElevatedButton], and [OutlinedButton],
/// has many material state properties.  The button widgets keep track
/// of their current material state and [resolve] the button style's
/// material state properties when their value is needed.
///
/// {@tool dartpad --template=stateless_widget_scaffold_center}
///
/// This example shows how you can override the default text and icon
/// color (the "foreground color") of a [TextButton] with a
/// [MaterialStateProperty]. In this example, the button's text color
/// will be `Colors.blue` when the button is being pressed, hovered,
/// or focused. Otherwise, the text color will be `Colors.red`.
///
/// ```dart
/// Widget build(BuildContext context) {
///   Color getColor(Set<MaterialState> states) {
///     const Set<MaterialState> interactiveStates = <MaterialState>{
///       MaterialState.pressed,
///       MaterialState.hovered,
///       MaterialState.focused,
///     };
///     if (states.any(interactiveStates.contains)) {
///       return Colors.blue;
///     }
///     return Colors.red;
///   }
///   return TextButton(
///     style: ButtonStyle(
///       foregroundColor: MaterialStateProperty.resolveWith(getColor),
///     ),
///     onPressed: () {},
///     child: Text('TextButton'),
///   );
/// }
/// ```
/// {@end-tool}
///
/// See also:
336
///
337 338 339
///  * [MaterialStateColor], a [Color] that implements `MaterialStateProperty`
///    which is used in APIs that need to accept either a [Color] or a
///    `MaterialStateProperty<Color>`.
340
///  * [MaterialStateMouseCursor], a [MouseCursor] that implements `MaterialStateProperty`
341 342
///    which is used in APIs that need to accept either a [MouseCursor] or a
///    [MaterialStateProperty<MouseCursor>].
343 344
abstract class MaterialStateProperty<T> {

345
  /// Returns a value of type `T` that depends on [states].
346
  ///
347 348 349
  /// Widgets like [TextButton] and [ElevatedButton] apply this method to their
  /// current [MaterialState]s to compute colors and other visual parameters
  /// at build time.
350
  T? resolve(Set<MaterialState> states);
351

352
  /// Resolves the value for the given set of states if `value` is a
353 354 355
  /// [MaterialStateProperty], otherwise returns the value itself.
  ///
  /// This is useful for widgets that have parameters which can optionally be a
356 357
  /// [MaterialStateProperty]. For example, [InkWell.mouseCursor] can be a
  /// [MouseCursor] or a [MaterialStateProperty<MouseCursor>].
358
  static T? resolveAs<T>(T? value, Set<MaterialState> states) {
359
    if (value is MaterialStateProperty<T>) {
360
      final MaterialStateProperty<T> property = value as MaterialStateProperty<T>;
361 362 363 364 365 366 367
      return property.resolve(states);
    }
    return value;
  }

  /// Convenience method for creating a [MaterialStateProperty] from a
  /// [MaterialPropertyResolver] function alone.
368 369 370 371
  static MaterialStateProperty<T> resolveWith<T>(MaterialPropertyResolver<T> callback) => _MaterialStatePropertyWith<T>(callback);

  /// Convenience method for creating a [MaterialStateProperty] that resolves
  /// to a single value for all states.
372
  static MaterialStateProperty<T> all<T>(T? value) => _MaterialStatePropertyAll<T>(value);
373 374
}

375 376
class _MaterialStatePropertyWith<T> implements MaterialStateProperty<T> {
  _MaterialStatePropertyWith(this._resolve);
377 378 379 380

  final MaterialPropertyResolver<T> _resolve;

  @override
381
  T? resolve(Set<MaterialState> states) => _resolve(states);
382
}
383 384 385 386

class _MaterialStatePropertyAll<T> implements MaterialStateProperty<T> {
  _MaterialStatePropertyAll(this.value);

387
  final T? value;
388 389

  @override
390
  T? resolve(Set<MaterialState> states) => value;
391 392 393

  @override
  String toString() => 'MaterialStateProperty.all($value)';
394
}