Created
March 10, 2026 19:58
-
-
Save davidhicks980/45ab05be46c7a72a2ee59d52fba1d6a8 to your computer and use it in GitHub Desktop.
Button 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'; | |
| import 'package:flutter/semantics.dart'; | |
| void main() { | |
| runApp(const ButtonDemoApp()); | |
| } | |
| class ButtonDemoApp extends StatelessWidget { | |
| const ButtonDemoApp({super.key}); | |
| @override | |
| Widget build(BuildContext context) { | |
| return MaterialApp( | |
| title: 'Button Demo', | |
| debugShowCheckedModeBanner: false, | |
| home: Scaffold( | |
| appBar: AppBar(title: const Text('Button Example')), | |
| body: const SafeArea(child: ButtonExample()), | |
| ), | |
| ); | |
| } | |
| } | |
| class ButtonExample extends StatelessWidget { | |
| const ButtonExample({super.key}); | |
| @override | |
| Widget build(BuildContext context) { | |
| return Center( | |
| child: Button( | |
| onPressed: () { | |
| ScaffoldMessenger.of( | |
| context, | |
| ).showSnackBar(const SnackBar(content: Text('Button Pressed!'))); | |
| }, | |
| behavior: HitTestBehavior.opaque, | |
| mouseCursor: WidgetStateProperty.all(SystemMouseCursors.click), | |
| child: Builder( | |
| builder: (context) { | |
| final ButtonState state = Button.of(context)!; | |
| final bool isHovered = state.hovered; | |
| final bool isPressed = state.pressed; | |
| final bool isFocused = state.focused; | |
| return AnimatedContainer( | |
| duration: const Duration(milliseconds: 50), | |
| padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), | |
| decoration: BoxDecoration( | |
| color: isPressed | |
| ? Colors.blue.shade700 | |
| : isHovered | |
| ? Colors.blue.shade600 | |
| : Colors.blue.shade500, | |
| borderRadius: BorderRadius.circular(8), | |
| border: isFocused ? Border.all(color: Colors.yellow, width: 2) : null, | |
| ), | |
| child: const Text('Button', style: TextStyle(color: Colors.white, fontSize: 16)), | |
| ); | |
| }, | |
| ), | |
| ), | |
| ); | |
| } | |
| } | |
| // Impl | |
| class _ButtonStateScope extends InheritedNotifier<WidgetStatesController> { | |
| const _ButtonStateScope({required super.notifier, required super.child}); | |
| } | |
| extension type ButtonState._(Set<WidgetState> value) { | |
| bool get hovered => value.contains(WidgetState.hovered); | |
| bool get pressed => value.contains(WidgetState.pressed); | |
| bool get focused => value.contains(WidgetState.focused); | |
| bool get disabled => value.contains(WidgetState.disabled); | |
| Set<WidgetState> get states => value; | |
| } | |
| class Button extends StatefulWidget { | |
| const Button({ | |
| super.key, | |
| this.onHover, | |
| this.onPressed, | |
| this.onFocusChange, | |
| this.focusNode, | |
| this.autofocus = false, | |
| this.behavior = HitTestBehavior.deferToChild, | |
| this.state, | |
| this.mouseCursor, | |
| required this.child, | |
| }); | |
| final ValueChanged<bool>? onHover; | |
| final VoidCallback? onPressed; | |
| final ValueChanged<bool>? onFocusChange; | |
| final FocusNode? focusNode; | |
| final bool autofocus; | |
| final HitTestBehavior behavior; | |
| final Set<WidgetState>? state; | |
| final WidgetStateProperty<MouseCursor>? mouseCursor; | |
| final Widget child; | |
| static ButtonState of(BuildContext context) { | |
| final Set<WidgetState> value = context | |
| .dependOnInheritedWidgetOfExactType<_ButtonStateScope>() | |
| !.notifier | |
| !.value; | |
| return ButtonState._(value); | |
| } | |
| @override | |
| State<Button> createState() => _ButtonState(); | |
| } | |
| class _ButtonState extends State<Button> { | |
| late final Map<Type, Action<Intent>> _actions = <Type, Action<Intent>>{ | |
| ActivateIntent: CallbackAction<ActivateIntent>(onInvoke: _handleActivation), | |
| ButtonActivateIntent: CallbackAction<ButtonActivateIntent>(onInvoke: _handleActivation), | |
| }; | |
| Map<Type, GestureRecognizerFactory>? _gestures; | |
| DeviceGestureSettings? _gestureSettings; | |
| // 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!; | |
| final WidgetStatesController _statesController = WidgetStatesController(); | |
| 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(); | |
| } | |
| _statesController.value = widget.state ?? <WidgetState>{}; | |
| isEnabled = widget.onPressed != null; | |
| isFocused = _focusNode.hasPrimaryFocus; | |
| } | |
| @override | |
| void didUpdateWidget(Button 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.state != oldWidget.state && widget.state != null) { | |
| _statesController.value = widget.state!; | |
| } | |
| if (widget.onPressed != oldWidget.onPressed) { | |
| if (widget.onPressed == null) { | |
| isEnabled = isHovered = isPressed = isFocused = false; | |
| } else { | |
| isEnabled = true; | |
| } | |
| } | |
| } | |
| @override | |
| void dispose() { | |
| _statesController.dispose(); | |
| _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 = false; | |
| widget.onHover?.call(false); | |
| } | |
| } | |
| void _handlePointerEnter(PointerEnterEvent event) { | |
| if (!isHovered) { | |
| isHovered = true; | |
| widget.onHover?.call(true); | |
| } | |
| } | |
| void _handleDismiss() { | |
| Actions.invoke(context, const DismissIntent()); | |
| } | |
| @override | |
| Widget build(BuildContext context) { | |
| final DeviceGestureSettings? newGestureSettings = MediaQuery.maybeGestureSettingsOf(context); | |
| if (_gestureSettings != newGestureSettings) { | |
| _gestureSettings = newGestureSettings; | |
| _gestures = null; | |
| } | |
| _gestures ??= <Type, GestureRecognizerFactory>{ | |
| TapGestureRecognizer: GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>( | |
| () => TapGestureRecognizer(debugOwner: this), | |
| (TapGestureRecognizer instance) { | |
| instance | |
| ..onTapDown = _handleTapDown | |
| ..onTapUp = _handleTapUp | |
| ..onTapCancel = _handleTapCancel | |
| ..gestureSettings = _gestureSettings; | |
| }, | |
| ), | |
| }; | |
| final child = RawGestureDetector( | |
| behavior: widget.behavior, | |
| gestures: isEnabled ? _gestures! : const <Type, GestureRecognizerFactory>{}, | |
| child: widget.child, | |
| ); | |
| return MergeSemantics( | |
| child: Semantics.fromProperties( | |
| properties: SemanticsProperties( | |
| enabled: isEnabled, | |
| onDismiss: isEnabled ? _handleDismiss : null, | |
| ), | |
| child: Actions( | |
| actions: isEnabled ? _actions : <Type, Action<Intent>>{}, | |
| child: Focus( | |
| autofocus: isEnabled && widget.autofocus, | |
| focusNode: _focusNode, | |
| canRequestFocus: isEnabled, | |
| skipTraversal: !isEnabled, | |
| onFocusChange: _handleFocusChange, | |
| child: _ButtonStateScope( | |
| notifier: _statesController, | |
| child: Builder( | |
| builder: (context) { | |
| final MouseCursor? cursor = widget.mouseCursor?.resolve( | |
| Button.of(context)?.states ?? <WidgetState>{}, | |
| ); | |
| return MouseRegion( | |
| onEnter: isEnabled ? _handlePointerEnter : null, | |
| onExit: isEnabled ? _handlePointerExit : null, | |
| hitTestBehavior: HitTestBehavior.deferToChild, | |
| cursor: cursor ?? MouseCursor.defer, | |
| child: child, | |
| ); | |
| }, | |
| ), | |
| ), | |
| ), | |
| ), | |
| ), | |
| ); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment