material_state_mixin.dart 6.34 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
// 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/foundation.dart';
import 'package:flutter/widgets.dart';

import 'material_state.dart';

/// Mixin for [State] classes that require knowledge of changing [MaterialState]
/// values for their child widgets.
///
/// This mixin does nothing by mere application to a [State] class, but is
/// helpful when writing `build` methods that include child [InkWell],
/// [GestureDetector], [MouseRegion], or [Focus] widgets. Instead of manually
/// creating handlers for each type of user interaction, such [State] classes can
/// instead provide a `ValueChanged<bool>` function and allow [MaterialStateMixin]
/// to manage the set of active [MaterialState]s, and the calling of [setState]
/// as necessary.
///
/// {@tool snippet}
/// This example shows how to write a [StatefulWidget] that uses the
/// [MaterialStateMixin] class to watch [MaterialState] values.
///
/// ```dart
/// class MyWidget extends StatefulWidget {
27
///   const MyWidget({super.key, required this.color, required this.child});
28 29 30 31 32 33 34 35 36 37 38 39 40
///
///   final MaterialStateColor color;
///   final Widget child;
///
///   @override
///   State<MyWidget> createState() => MyWidgetState();
/// }
///
/// class MyWidgetState extends State<MyWidget> with MaterialStateMixin<MyWidget> {
///   @override
///   Widget build(BuildContext context) {
///     return InkWell(
///       onFocusChange: updateMaterialState(MaterialState.focused),
41
///       child: ColoredBox(
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 75 76 77 78
///         color: widget.color.resolve(materialStates),
///         child: widget.child,
///       ),
///     );
///   }
/// }
/// ```
/// {@end-tool}
@optionalTypeArgs
mixin MaterialStateMixin<T extends StatefulWidget> on State<T> {
  /// Managed set of active [MaterialState] values; designed to be passed to
  /// [MaterialStateProperty.resolve] methods.
  ///
  /// To mutate and have [setState] called automatically for you, use
  /// [setMaterialState], [addMaterialState], or [removeMaterialState]. Directly
  /// mutating the set is possible, and may be necessary if you need to alter its
  /// list without calling [setState] (and thus triggering a re-render).
  ///
  /// To check for a single condition, convenience getters [isPressed], [isHovered],
  /// [isFocused], etc, are available for each [MaterialState] value.
  @protected
  Set<MaterialState> materialStates = <MaterialState>{};

  /// Callback factory which accepts a [MaterialState] value and returns a
  /// closure to mutate [materialStates] and call [setState].
  ///
  /// Accepts an optional second named parameter, `onChanged`, which allows
  /// arbitrary functionality to be wired through the [MaterialStateMixin].
  /// If supplied, the [onChanged] function is only called when child widgets
  /// report events that make changes to the current set of [MaterialState]s.
  ///
  /// {@tool snippet}
  /// This example shows how to use the [updateMaterialState] callback factory
  /// in other widgets, including the optional [onChanged] callback.
  ///
  /// ```dart
  /// class MyWidget extends StatefulWidget {
79
  ///   const MyWidget({super.key, this.onPressed});
80 81 82 83 84 85 86 87 88 89 90
  ///
  ///   /// Something important this widget must do when pressed.
  ///   final VoidCallback? onPressed;
  ///
  ///   @override
  ///   State<MyWidget> createState() => MyWidgetState();
  /// }
  ///
  /// class MyWidgetState extends State<MyWidget> with MaterialStateMixin<MyWidget> {
  ///   @override
  ///   Widget build(BuildContext context) {
91
  ///     return ColoredBox(
92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110
  ///       color: isPressed ? Colors.black : Colors.white,
  ///       child: InkWell(
  ///         onHighlightChanged: updateMaterialState(
  ///           MaterialState.pressed,
  ///           onChanged: (bool val) {
  ///             if (val) {
  ///               widget.onPressed?.call();
  ///             }
  ///           },
  ///         ),
  ///       ),
  ///     );
  ///   }
  /// }
  /// ```
  /// {@end-tool}
  @protected
  ValueChanged<bool> updateMaterialState(MaterialState key, {ValueChanged<bool>? onChanged}) {
    return (bool value) {
111
      if (materialStates.contains(key) == value) {
112
        return;
113
      }
114 115 116 117 118 119 120
      setMaterialState(key, value);
      onChanged?.call(value);
    };
  }

  /// Mutator to mark a [MaterialState] value as either active or inactive.
  @protected
121 122
  void setMaterialState(MaterialState state, bool isSet) {
    return isSet ? addMaterialState(state) : removeMaterialState(state);
123 124 125 126
  }

  /// Mutator to mark a [MaterialState] value as active.
  @protected
127
  void addMaterialState(MaterialState state) {
128
    if (materialStates.add(state)) {
129
      setState((){});
130
    }
131 132 133 134
  }

  /// Mutator to mark a [MaterialState] value as inactive.
  @protected
135
  void removeMaterialState(MaterialState state) {
136
    if (materialStates.remove(state)) {
137
      setState((){});
138
    }
139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170
  }

  /// Getter for whether this class considers [MaterialState.disabled] to be active.
  bool get isDisabled => materialStates.contains(MaterialState.disabled);

  /// Getter for whether this class considers [MaterialState.dragged] to be active.
  bool get isDragged => materialStates.contains(MaterialState.dragged);

  /// Getter for whether this class considers [MaterialState.error] to be active.
  bool get isErrored => materialStates.contains(MaterialState.error);

  /// Getter for whether this class considers [MaterialState.focused] to be active.
  bool get isFocused => materialStates.contains(MaterialState.focused);

  /// Getter for whether this class considers [MaterialState.hovered] to be active.
  bool get isHovered => materialStates.contains(MaterialState.hovered);

  /// Getter for whether this class considers [MaterialState.pressed] to be active.
  bool get isPressed => materialStates.contains(MaterialState.pressed);

  /// Getter for whether this class considers [MaterialState.scrolledUnder] to be active.
  bool get isScrolledUnder => materialStates.contains(MaterialState.scrolledUnder);

  /// Getter for whether this class considers [MaterialState.selected] to be active.
  bool get isSelected => materialStates.contains(MaterialState.selected);

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(DiagnosticsProperty<Set<MaterialState>>('materialStates', materialStates, defaultValue: <MaterialState>{}));
  }
}