Last active
November 14, 2025 09:12
-
-
Save davidhicks980/bc70d14c81942224a58e2caa2706c484 to your computer and use it in GitHub Desktop.
WidgetStateScope demo
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import 'package:flutter/gestures.dart'; | |
| import 'package:flutter/material.dart'; | |
| void main() { | |
| runApp(const RawButtonApp()); | |
| } | |
| class RawButtonApp extends StatelessWidget { | |
| const RawButtonApp({super.key}); | |
| @override | |
| Widget build(BuildContext context) { | |
| return MaterialApp( | |
| debugShowCheckedModeBanner: false, | |
| title: 'Threshold Demo', | |
| theme: ThemeData(primarySwatch: Colors.blue), | |
| home: const Scaffold(body: Center(child: RawButtonDemo())), | |
| ); | |
| } | |
| } | |
| class RawButtonDemo extends StatefulWidget { | |
| const RawButtonDemo({super.key}); | |
| @override | |
| State<RawButtonDemo> createState() => _RawButtonDemoState(); | |
| } | |
| class _RawButtonDemoState extends State<RawButtonDemo> with TickerProviderStateMixin { | |
| static final decoration = WidgetStateProperty.fromMap({ | |
| WidgetState.pressed: const AnimatedBoxDecoration( | |
| color: Colors.indigoAccent, | |
| duration: Duration(milliseconds: 100), | |
| curve: Curves.easeIn, | |
| ), | |
| WidgetState.hovered: const AnimatedBoxDecoration( | |
| color: Colors.indigo, | |
| duration: Duration(milliseconds: 200), | |
| curve: Curves.easeOutExpo, | |
| ), | |
| WidgetState.any: AnimatedBoxDecoration( | |
| color: Colors.indigo[100], | |
| duration: const Duration(milliseconds: 300), | |
| curve: Curves.easeOut, | |
| ), | |
| }); | |
| static final textStyle = WidgetStateProperty.fromMap({ | |
| WidgetState.hovered: const TextStyle(fontWeight: FontWeight.w600, color: Colors.white), | |
| WidgetState.any: TextStyle(color: Colors.indigo[900], fontWeight: FontWeight.w600), | |
| }); | |
| @override | |
| Widget build(BuildContext context) { | |
| return Column( | |
| mainAxisAlignment: MainAxisAlignment.center, | |
| children: [ | |
| RawButton( | |
| onPressed: () { | |
| ScaffoldMessenger.of(context).showSnackBar( | |
| const SnackBar(content: Text('Button Pressed!')), | |
| ); | |
| }, | |
| child: AnimatedWidgetStatesDecoration( | |
| decoration: decoration, | |
| child: WidgetStatesDefaultTextStyle( | |
| textStyle: textStyle, | |
| child: const Padding( | |
| padding: EdgeInsets.all(16.0), | |
| child: Text('Press Me'), | |
| ), | |
| ), | |
| ), | |
| ), | |
| ], | |
| ); | |
| } | |
| } | |
| class WidgetStateScope extends InheritedNotifier<WidgetStatesController> { | |
| const WidgetStateScope({ | |
| super.key, | |
| required super.notifier, | |
| required super.child, | |
| }); | |
| @override | |
| WidgetStatesController? get notifier => super.notifier; | |
| static Set<WidgetState>? maybeOf(BuildContext context) { | |
| return context.dependOnInheritedWidgetOfExactType<WidgetStateScope>()?.notifier?.value; | |
| } | |
| } | |
| class AnimatedBoxDecoration extends BoxDecoration { | |
| const AnimatedBoxDecoration({ | |
| this.duration = Duration.zero, | |
| this.curve = Curves.linear, | |
| super.backgroundBlendMode, | |
| super.border, | |
| super.borderRadius, | |
| super.boxShadow, | |
| super.gradient, | |
| super.image, | |
| super.color, | |
| super.shape, | |
| }); | |
| final Duration duration; | |
| final Curve curve; | |
| } | |
| class AnimatedWidgetStatesDecoration extends StatelessWidget { | |
| const AnimatedWidgetStatesDecoration({ | |
| super.key, | |
| required this.child, | |
| required this.decoration, | |
| }); | |
| final Widget child; | |
| final WidgetStateProperty<AnimatedBoxDecoration> decoration; | |
| @override | |
| Widget build(BuildContext context) { | |
| final state = WidgetStateScope.maybeOf(context)!; | |
| final resolvedDecoration = decoration.resolve(state); | |
| return AnimatedContainer( | |
| duration: resolvedDecoration.duration, | |
| curve: resolvedDecoration.curve, | |
| decoration: resolvedDecoration, | |
| child: child, | |
| ); | |
| } | |
| } | |
| class WidgetStatesDefaultTextStyle extends StatelessWidget { | |
| const WidgetStatesDefaultTextStyle({ | |
| super.key, | |
| required this.child, | |
| required this.textStyle, | |
| }); | |
| final Widget child; | |
| final WidgetStateProperty<TextStyle> textStyle; | |
| @override | |
| Widget build(BuildContext context) { | |
| final state = WidgetStateScope.maybeOf(context)!; | |
| final resolvedTextStyle = textStyle.resolve(state); | |
| return DefaultTextStyle( | |
| style: resolvedTextStyle, | |
| child: child, | |
| ); | |
| } | |
| } | |
| class RawButton extends StatefulWidget { | |
| const RawButton({ | |
| super.key, | |
| this.onHover, | |
| this.onPressed, | |
| this.onFocusChange, | |
| this.focusNode, | |
| this.autofocus = false, | |
| this.behavior = HitTestBehavior.deferToChild, | |
| this.statesController, | |
| required this.child, | |
| }); | |
| final ValueChanged<bool>? onHover; | |
| final VoidCallback? onPressed; | |
| final ValueChanged<bool>? onFocusChange; | |
| final FocusNode? focusNode; | |
| final bool autofocus; | |
| final HitTestBehavior behavior; | |
| final WidgetStatesController? statesController; | |
| final Widget child; | |
| @override | |
| State<RawButton> createState() => _RawButtonState(); | |
| } | |
| class _RawButtonState extends State<RawButton> { | |
| late final Map<Type, Action<Intent>> _actionMap = <Type, Action<Intent>>{ | |
| ActivateIntent: CallbackAction<ActivateIntent>(onInvoke: _handleActivation), | |
| ButtonActivateIntent: CallbackAction<ButtonActivateIntent>(onInvoke: _handleActivation), | |
| }; | |
| // If a focus node isn't given to the widget, then we have to manage our own. | |
| FocusNode? _internalFocusNode; | |
| FocusNode get _focusNode => widget.focusNode ?? _internalFocusNode!; | |
| WidgetStatesController? _internalStatesController; | |
| WidgetStatesController get _statesController { | |
| return widget.statesController ?? _internalStatesController!; | |
| } | |
| Map<Type, GestureRecognizerFactory>? gestures; | |
| bool get isHovered => _isHovered; | |
| bool _isHovered = false; | |
| set isHovered(bool value) { | |
| if (_isHovered != value) { | |
| _isHovered = value; | |
| _statesController.update(WidgetState.hovered, value); | |
| } | |
| } | |
| bool get isPressed => _isPressed; | |
| bool _isPressed = false; | |
| set isPressed(bool value) { | |
| if (_isPressed != value) { | |
| _isPressed = value; | |
| _statesController.update(WidgetState.pressed, value); | |
| } | |
| } | |
| bool get isFocused => _isFocused; | |
| bool _isFocused = false; | |
| set isFocused(bool value) { | |
| if (_isFocused != value) { | |
| _isFocused = value; | |
| _statesController.update(WidgetState.focused, value); | |
| } | |
| } | |
| bool get isEnabled => _isEnabled; | |
| bool _isEnabled = false; | |
| set isEnabled(bool value) { | |
| if (_isEnabled != value) { | |
| _isEnabled = value; | |
| _statesController.update(WidgetState.disabled, !value); | |
| } | |
| } | |
| @override | |
| void initState() { | |
| super.initState(); | |
| if (widget.focusNode == null) { | |
| _internalFocusNode = FocusNode(); | |
| } | |
| if (widget.statesController == null) { | |
| _internalStatesController = WidgetStatesController(); | |
| } | |
| isEnabled = widget.onPressed != null; | |
| isFocused = _focusNode.hasPrimaryFocus; | |
| } | |
| @override | |
| void didUpdateWidget(RawButton oldWidget) { | |
| super.didUpdateWidget(oldWidget); | |
| if (widget.focusNode != oldWidget.focusNode) { | |
| if (widget.focusNode != null) { | |
| _internalFocusNode?.dispose(); | |
| _internalFocusNode = null; | |
| } else { | |
| assert(_internalFocusNode == null); | |
| _internalFocusNode = FocusNode(); | |
| } | |
| isFocused = _focusNode.hasPrimaryFocus; | |
| } | |
| if (widget.statesController != oldWidget.statesController) { | |
| if (widget.statesController != null) { | |
| _internalStatesController?.dispose(); | |
| _internalStatesController = null; | |
| } else { | |
| assert(_internalStatesController == null); | |
| _internalStatesController = WidgetStatesController(); | |
| } | |
| } | |
| if (widget.onPressed != oldWidget.onPressed) { | |
| if (widget.onPressed == null) { | |
| isEnabled = isHovered = isPressed = isFocused = false; | |
| } else { | |
| isEnabled = true; | |
| } | |
| } | |
| } | |
| @override | |
| void dispose() { | |
| _internalStatesController?.dispose(); | |
| _internalStatesController = null; | |
| _internalFocusNode?.dispose(); | |
| _internalFocusNode = null; | |
| super.dispose(); | |
| } | |
| void _handleFocusChange([bool? focused]) { | |
| isFocused = _focusNode.hasPrimaryFocus; | |
| widget.onFocusChange?.call(isFocused); | |
| } | |
| void _handleActivation([Intent? intent]) { | |
| isPressed = false; | |
| widget.onPressed?.call(); | |
| } | |
| void _handleTapDown(TapDownDetails details) { | |
| isPressed = true; | |
| } | |
| void _handleTapUp(TapUpDetails? details) { | |
| isPressed = false; | |
| widget.onPressed?.call(); | |
| } | |
| void _handleTapCancel() { | |
| isPressed = false; | |
| } | |
| void _handlePointerExit(PointerExitEvent event) { | |
| if (isHovered) { | |
| isHovered = isFocused = false; | |
| widget.onHover?.call(false); | |
| } | |
| } | |
| // TextButton.onHover and MouseRegion.onHover can't be used without triggering | |
| // focus on scroll. | |
| void _handlePointerHover(PointerHoverEvent event) { | |
| if (!isHovered) { | |
| isHovered = true; | |
| widget.onHover?.call(true); | |
| } | |
| } | |
| @override | |
| Widget build(BuildContext context) { | |
| if (isEnabled) { | |
| final DeviceGestureSettings? gestureSettings = MediaQuery.maybeGestureSettingsOf(context); | |
| gestures ??= <Type, GestureRecognizerFactory>{ | |
| TapGestureRecognizer: GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>( | |
| () => TapGestureRecognizer(), | |
| (TapGestureRecognizer instance) { | |
| instance | |
| ..onTapDown = _handleTapDown | |
| ..onTapUp = _handleTapUp | |
| ..onTapCancel = _handleTapCancel | |
| ..gestureSettings = gestureSettings; | |
| }, | |
| ), | |
| }; | |
| } else { | |
| gestures = null; | |
| } | |
| return Actions( | |
| actions: isEnabled ? _actionMap : <Type, Action<Intent>>{}, | |
| child: Focus( | |
| autofocus: isEnabled && widget.autofocus, | |
| focusNode: _focusNode, | |
| canRequestFocus: isEnabled, | |
| skipTraversal: !isEnabled, | |
| onFocusChange: _handleFocusChange, | |
| child: WidgetStateScope( | |
| notifier: _statesController, | |
| child: MouseRegion( | |
| onExit: isEnabled ? _handlePointerExit : null, | |
| onHover: isEnabled ? _handlePointerHover : null, | |
| cursor: isEnabled ? MouseCursor.defer : SystemMouseCursors.forbidden, | |
| hitTestBehavior: widget.behavior, | |
| child: RawGestureDetector( | |
| behavior: widget.behavior, | |
| gestures: gestures ?? const <Type, GestureRecognizerFactory>{}, | |
| child: widget.child, | |
| ), | |
| ), | |
| ), | |
| ), | |
| ); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment