blob: 2a8d9de2aff0a5f9ee042aa00ca97196deab98bb [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/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 {
/// const MyWidget({super.key, required this.color, required this.child});
///
/// 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),
/// child: Container(
/// 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 {
/// const MyWidget({super.key, this.onPressed});
///
/// /// 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) {
/// return Container(
/// 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) {
if (materialStates.contains(key) == value) {
return;
}
setMaterialState(key, value);
onChanged?.call(value);
};
}
/// Mutator to mark a [MaterialState] value as either active or inactive.
@protected
void setMaterialState(MaterialState state, bool isSet) {
return isSet ? addMaterialState(state) : removeMaterialState(state);
}
/// Mutator to mark a [MaterialState] value as active.
@protected
void addMaterialState(MaterialState state) {
if (materialStates.add(state)) {
setState((){});
}
}
/// Mutator to mark a [MaterialState] value as inactive.
@protected
void removeMaterialState(MaterialState state) {
if (materialStates.remove(state)) {
setState((){});
}
}
/// 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>{}));
}
}