@@ -16,6 +16,12 @@ const EdgeInsets _kBackgroundButtonPadding = EdgeInsets.symmetric(
16
16
horizontal: 64.0 ,
17
17
);
18
18
19
+ // The relative values needed to transform a color to it's equivalent focus
20
+ // outline color.
21
+ const double _kCupertinoFocusColorOpacity = 0.80 ;
22
+ const double _kCupertinoFocusColorBrightness = 0.69 ;
23
+ const double _kCupertinoFocusColorSaturation = 0.835 ;
24
+
19
25
/// An iOS-style button.
20
26
///
21
27
/// Takes in a text or an icon that fades out and in on touch. May optionally have a
@@ -48,6 +54,10 @@ class CupertinoButton extends StatefulWidget {
48
54
this .pressedOpacity = 0.4 ,
49
55
this .borderRadius = const BorderRadius .all (Radius .circular (8.0 )),
50
56
this .alignment = Alignment .center,
57
+ this .focusColor,
58
+ this .focusNode,
59
+ this .onFocusChange,
60
+ this .autofocus = false ,
51
61
required this .onPressed,
52
62
}) : assert (pressedOpacity == null || (pressedOpacity >= 0.0 && pressedOpacity <= 1.0 )),
53
63
_filled = false ;
@@ -67,6 +77,10 @@ class CupertinoButton extends StatefulWidget {
67
77
this .pressedOpacity = 0.4 ,
68
78
this .borderRadius = const BorderRadius .all (Radius .circular (8.0 )),
69
79
this .alignment = Alignment .center,
80
+ this .focusColor,
81
+ this .focusNode,
82
+ this .onFocusChange,
83
+ this .autofocus = false ,
70
84
required this .onPressed,
71
85
}) : assert (pressedOpacity == null || (pressedOpacity >= 0.0 && pressedOpacity <= 1.0 )),
72
86
color = null ,
@@ -131,6 +145,26 @@ class CupertinoButton extends StatefulWidget {
131
145
/// Always defaults to [Alignment.center] .
132
146
final AlignmentGeometry alignment;
133
147
148
+ /// The color to use for the focus highlight for keyboard interactions.
149
+ ///
150
+ /// Defaults to a slightly transparent [color] . If [color] is null, defaults
151
+ /// to a slightly transparent [CupertinoColors.activeBlue] . Slightly
152
+ /// transparent in this context means the color is used with an opacity of
153
+ /// 0.80, a brightness of 0.69 and a saturation of 0.835.
154
+ final Color ? focusColor;
155
+
156
+ /// {@macro flutter.widgets.Focus.focusNode}
157
+ final FocusNode ? focusNode;
158
+
159
+ /// Handler called when the focus changes.
160
+ ///
161
+ /// Called with true if this widget's node gains focus, and false if it loses
162
+ /// focus.
163
+ final ValueChanged <bool >? onFocusChange;
164
+
165
+ /// {@macro flutter.widgets.Focus.autofocus}
166
+ final bool autofocus;
167
+
134
168
final bool _filled;
135
169
136
170
/// Whether the button is enabled or disabled. Buttons are disabled by default. To
@@ -156,9 +190,12 @@ class _CupertinoButtonState extends State<CupertinoButton> with SingleTickerProv
156
190
late AnimationController _animationController;
157
191
late Animation <double > _opacityAnimation;
158
192
193
+ late bool isFocused;
194
+
159
195
@override
160
196
void initState () {
161
197
super .initState ();
198
+ isFocused = false ;
162
199
_animationController = AnimationController (
163
200
duration: const Duration (milliseconds: 200 ),
164
201
value: 0.0 ,
@@ -224,6 +261,12 @@ class _CupertinoButtonState extends State<CupertinoButton> with SingleTickerProv
224
261
});
225
262
}
226
263
264
+ void _onShowFocusHighlight (bool showHighlight) {
265
+ setState (() {
266
+ isFocused = showHighlight;
267
+ });
268
+ }
269
+
227
270
@override
228
271
Widget build (BuildContext context) {
229
272
final bool enabled = widget.enabled;
@@ -239,47 +282,71 @@ class _CupertinoButtonState extends State<CupertinoButton> with SingleTickerProv
239
282
? primaryColor
240
283
: CupertinoDynamicColor .resolve (CupertinoColors .placeholderText, context);
241
284
285
+ final Color effectiveFocusOutlineColor = widget.focusColor ??
286
+ HSLColor
287
+ .fromColor ((backgroundColor ?? CupertinoColors .activeBlue)
288
+ .withOpacity (_kCupertinoFocusColorOpacity))
289
+ .withLightness (_kCupertinoFocusColorBrightness)
290
+ .withSaturation (_kCupertinoFocusColorSaturation)
291
+ .toColor ();
292
+
242
293
final TextStyle textStyle = themeData.textTheme.textStyle.copyWith (color: foregroundColor);
243
294
244
295
return MouseRegion (
245
296
cursor: enabled && kIsWeb ? SystemMouseCursors .click : MouseCursor .defer,
246
- child: GestureDetector (
247
- behavior: HitTestBehavior .opaque,
248
- onTapDown: enabled ? _handleTapDown : null ,
249
- onTapUp: enabled ? _handleTapUp : null ,
250
- onTapCancel: enabled ? _handleTapCancel : null ,
251
- onTap: widget.onPressed,
252
- child: Semantics (
253
- button: true ,
254
- child: ConstrainedBox (
255
- constraints: widget.minSize == null
256
- ? const BoxConstraints ()
257
- : BoxConstraints (
258
- minWidth: widget.minSize! ,
259
- minHeight: widget.minSize! ,
260
- ),
261
- child: FadeTransition (
262
- opacity: _opacityAnimation,
263
- child: DecoratedBox (
264
- decoration: BoxDecoration (
265
- borderRadius: widget.borderRadius,
266
- color: backgroundColor != null && ! enabled
267
- ? CupertinoDynamicColor .resolve (widget.disabledColor, context)
268
- : backgroundColor,
269
- ),
270
- child: Padding (
271
- padding: widget.padding ?? (backgroundColor != null
272
- ? _kBackgroundButtonPadding
273
- : _kButtonPadding),
274
- child: Align (
275
- alignment: widget.alignment,
276
- widthFactor: 1.0 ,
277
- heightFactor: 1.0 ,
278
- child: DefaultTextStyle (
279
- style: textStyle,
280
- child: IconTheme (
281
- data: IconThemeData (color: foregroundColor),
282
- child: widget.child,
297
+ child: FocusableActionDetector (
298
+ focusNode: widget.focusNode,
299
+ autofocus: widget.autofocus,
300
+ onFocusChange: widget.onFocusChange,
301
+ onShowFocusHighlight: _onShowFocusHighlight,
302
+ enabled: enabled,
303
+ child: GestureDetector (
304
+ behavior: HitTestBehavior .opaque,
305
+ onTapDown: enabled ? _handleTapDown : null ,
306
+ onTapUp: enabled ? _handleTapUp : null ,
307
+ onTapCancel: enabled ? _handleTapCancel : null ,
308
+ onTap: widget.onPressed,
309
+ child: Semantics (
310
+ button: true ,
311
+ child: ConstrainedBox (
312
+ constraints: widget.minSize == null
313
+ ? const BoxConstraints ()
314
+ : BoxConstraints (
315
+ minWidth: widget.minSize! ,
316
+ minHeight: widget.minSize! ,
317
+ ),
318
+ child: FadeTransition (
319
+ opacity: _opacityAnimation,
320
+ child: DecoratedBox (
321
+ decoration: BoxDecoration (
322
+ border: enabled && isFocused
323
+ ? Border .fromBorderSide (
324
+ BorderSide (
325
+ color: effectiveFocusOutlineColor,
326
+ width: 3.5 ,
327
+ strokeAlign: BorderSide .strokeAlignOutside,
328
+ ),
329
+ )
330
+ : null ,
331
+ borderRadius: widget.borderRadius,
332
+ color: backgroundColor != null && ! enabled
333
+ ? CupertinoDynamicColor .resolve (widget.disabledColor, context)
334
+ : backgroundColor,
335
+ ),
336
+ child: Padding (
337
+ padding: widget.padding ?? (backgroundColor != null
338
+ ? _kBackgroundButtonPadding
339
+ : _kButtonPadding),
340
+ child: Align (
341
+ alignment: widget.alignment,
342
+ widthFactor: 1.0 ,
343
+ heightFactor: 1.0 ,
344
+ child: DefaultTextStyle (
345
+ style: textStyle,
346
+ child: IconTheme (
347
+ data: IconThemeData (color: foregroundColor),
348
+ child: widget.child,
349
+ ),
283
350
),
284
351
),
285
352
),
0 commit comments