Unverified Commit bb3c6605 authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

Implements focus handling and hover for Material buttons. (#31438)

This implements focus and hover handling for Material buttons. It inserts Focus widgets into the tree in order to allow buttons to be focusable via keyboard traversal (a.k.a. TAB traversal), and Listener widgets into the InkWell to allow the detection of hover states for widgets.

Addresses #11344, #1608, and #13264.
parent ed90d055
// Copyright 2019 The Chromium 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/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
void main() {
runApp(const MaterialApp(
title: 'Focus Demo',
home: FocusDemo(),
));
}
class DemoButton extends StatelessWidget {
const DemoButton({this.name});
final String name;
void _handleOnPressed() {
print('Button $name pressed.');
}
@override
Widget build(BuildContext context) {
return FlatButton(
focusColor: Colors.red,
hoverColor: Colors.blue,
onPressed: () => _handleOnPressed(),
child: Text(name),
);
}
}
class FocusDemo extends StatefulWidget {
const FocusDemo({Key key}) : super(key: key);
@override
_FocusDemoState createState() => _FocusDemoState();
}
class _FocusDemoState extends State<FocusDemo> {
FocusNode outlineFocus;
@override
void initState() {
super.initState();
outlineFocus = FocusNode(debugLabel: 'Demo Focus Node');
}
@override
void dispose() {
outlineFocus.dispose();
super.dispose();
}
bool _handleKeyPress(FocusNode node, RawKeyEvent event) {
if (event is RawKeyDownEvent) {
print('Scope got key event: ${event.logicalKey}, $node');
print('Keys down: ${RawKeyboard.instance.keysPressed}');
if (event.logicalKey == LogicalKeyboardKey.tab) {
debugDumpFocusTree();
if (event.isShiftPressed) {
print('Moving to previous.');
node.previousFocus();
return true;
} else {
print('Moving to next.');
node.nextFocus();
return true;
}
}
if (event.logicalKey == LogicalKeyboardKey.arrowLeft) {
node.focusInDirection(TraversalDirection.left);
return true;
}
if (event.logicalKey == LogicalKeyboardKey.arrowRight) {
node.focusInDirection(TraversalDirection.right);
return true;
}
if (event.logicalKey == LogicalKeyboardKey.arrowUp) {
node.focusInDirection(TraversalDirection.up);
return true;
}
if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
node.focusInDirection(TraversalDirection.down);
return true;
}
}
return false;
}
@override
Widget build(BuildContext context) {
final TextTheme textTheme = Theme.of(context).textTheme;
return DefaultFocusTraversal(
policy: ReadingOrderTraversalPolicy(),
child: FocusScope(
debugLabel: 'Scope',
onKey: _handleKeyPress,
autofocus: true,
child: DefaultTextStyle(
style: textTheme.display1,
child: Scaffold(
appBar: AppBar(
title: const Text('Focus Demo'),
),
floatingActionButton: FloatingActionButton(
child: const Text('+'),
onPressed: () {},
),
body: Center(
child: Builder(builder: (BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: const <Widget>[
DemoButton(name: 'One'),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: const <Widget>[
DemoButton(name: 'Two'),
DemoButton(name: 'Three'),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: const <Widget>[
DemoButton(name: 'Four'),
DemoButton(name: 'Five'),
DemoButton(name: 'Six'),
],
),
OutlineButton(onPressed: () => print('pressed'), child: const Text('PRESS ME')),
Padding(
padding: const EdgeInsets.all(8.0),
child: TextField(
decoration: InputDecoration(labelText: 'Enter Text', filled: true),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: TextField(
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: 'Enter Text',
filled: false,
),
),
),
],
);
}),
),
),
),
),
);
}
}
// Copyright 2019 The Chromium 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/material.dart';
import 'package:flutter/widgets.dart';
void main() {
runApp(const MaterialApp(
title: 'Hover Demo',
home: HoverDemo(),
));
}
class DemoButton extends StatelessWidget {
const DemoButton({this.name});
final String name;
void _handleOnPressed() {
print('Button $name pressed.');
}
@override
Widget build(BuildContext context) {
return FlatButton(
onPressed: () => _handleOnPressed(),
child: Text(name),
);
}
}
class HoverDemo extends StatefulWidget {
const HoverDemo({Key key}) : super(key: key);
@override
_HoverDemoState createState() => _HoverDemoState();
}
class _HoverDemoState extends State<HoverDemo> {
@override
Widget build(BuildContext context) {
final TextTheme textTheme = Theme.of(context).textTheme;
return DefaultTextStyle(
style: textTheme.display1,
child: Scaffold(
appBar: AppBar(
title: const Text('Hover Demo'),
),
floatingActionButton: FloatingActionButton(
child: const Text('+'),
onPressed: () {},
),
body: Center(
child: Builder(builder: (BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
RaisedButton(
onPressed: () => print('Button pressed.'),
child: const Text('Button'),
focusColor: Colors.deepOrangeAccent,
),
FlatButton(
onPressed: () => print('Button pressed.'),
child: const Text('Button'),
focusColor: Colors.deepOrangeAccent,
),
IconButton(
onPressed: () => print('Button pressed'),
icon: const Icon(Icons.access_alarm),
focusColor: Colors.deepOrangeAccent,
),
Padding(
padding: const EdgeInsets.all(8.0),
child: TextField(
decoration: InputDecoration(labelText: 'Enter Text', filled: true),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: TextField(
autofocus: false,
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: 'Enter Text',
filled: false,
),
),
),
],
);
}),
),
),
);
}
}
...@@ -60,6 +60,7 @@ class _CategoryItem extends StatelessWidget { ...@@ -60,6 +60,7 @@ class _CategoryItem extends StatelessWidget {
return RepaintBoundary( return RepaintBoundary(
child: RawMaterialButton( child: RawMaterialButton(
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
hoverColor: theme.primaryColor.withOpacity(0.05),
splashColor: theme.primaryColor.withOpacity(0.12), splashColor: theme.primaryColor.withOpacity(0.12),
highlightColor: Colors.transparent, highlightColor: Colors.transparent,
onPressed: onTap, onPressed: onTap,
......
...@@ -475,43 +475,45 @@ class _BottomNavigationTile extends StatelessWidget { ...@@ -475,43 +475,45 @@ class _BottomNavigationTile extends StatelessWidget {
container: true, container: true,
header: true, header: true,
selected: selected, selected: selected,
child: Stack( child: Focus(
children: <Widget>[ child: Stack(
InkResponse( children: <Widget>[
onTap: onTap, InkResponse(
child: Padding( onTap: onTap,
padding: EdgeInsets.only(top: topPadding, bottom: bottomPadding), child: Padding(
child: Column( padding: EdgeInsets.only(top: topPadding, bottom: bottomPadding),
crossAxisAlignment: CrossAxisAlignment.center, child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[ mainAxisSize: MainAxisSize.min,
_TileIcon( children: <Widget>[
colorTween: colorTween, _TileIcon(
animation: animation, colorTween: colorTween,
iconSize: iconSize, animation: animation,
selected: selected, iconSize: iconSize,
item: item, selected: selected,
selectedIconTheme: selectedIconTheme, item: item,
unselectedIconTheme: unselectedIconTheme, selectedIconTheme: selectedIconTheme,
), unselectedIconTheme: unselectedIconTheme,
_Label( ),
colorTween: colorTween, _Label(
animation: animation, colorTween: colorTween,
item: item, animation: animation,
selectedLabelStyle: selectedLabelStyle, item: item,
unselectedLabelStyle: unselectedLabelStyle, selectedLabelStyle: selectedLabelStyle,
showSelectedLabels: showSelectedLabels, unselectedLabelStyle: unselectedLabelStyle,
showUnselectedLabels: showUnselectedLabels, showSelectedLabels: showSelectedLabels,
), showUnselectedLabels: showUnselectedLabels,
], ),
],
),
), ),
), ),
), Semantics(
Semantics( label: indexLabel,
label: indexLabel, ),
), ],
], ),
), ),
), ),
); );
......
...@@ -78,6 +78,8 @@ class ButtonTheme extends InheritedWidget { ...@@ -78,6 +78,8 @@ class ButtonTheme extends InheritedWidget {
bool alignedDropdown = false, bool alignedDropdown = false,
Color buttonColor, Color buttonColor,
Color disabledColor, Color disabledColor,
Color focusColor,
Color hoverColor,
Color highlightColor, Color highlightColor,
Color splashColor, Color splashColor,
ColorScheme colorScheme, ColorScheme colorScheme,
...@@ -98,6 +100,8 @@ class ButtonTheme extends InheritedWidget { ...@@ -98,6 +100,8 @@ class ButtonTheme extends InheritedWidget {
layoutBehavior: layoutBehavior, layoutBehavior: layoutBehavior,
buttonColor: buttonColor, buttonColor: buttonColor,
disabledColor: disabledColor, disabledColor: disabledColor,
focusColor: focusColor,
hoverColor: hoverColor,
highlightColor: highlightColor, highlightColor: highlightColor,
splashColor: splashColor, splashColor: splashColor,
colorScheme: colorScheme, colorScheme: colorScheme,
...@@ -139,6 +143,8 @@ class ButtonTheme extends InheritedWidget { ...@@ -139,6 +143,8 @@ class ButtonTheme extends InheritedWidget {
bool alignedDropdown = false, bool alignedDropdown = false,
Color buttonColor, Color buttonColor,
Color disabledColor, Color disabledColor,
Color focusColor,
Color hoverColor,
Color highlightColor, Color highlightColor,
Color splashColor, Color splashColor,
ColorScheme colorScheme, ColorScheme colorScheme,
...@@ -158,6 +164,8 @@ class ButtonTheme extends InheritedWidget { ...@@ -158,6 +164,8 @@ class ButtonTheme extends InheritedWidget {
layoutBehavior: layoutBehavior, layoutBehavior: layoutBehavior,
buttonColor: buttonColor, buttonColor: buttonColor,
disabledColor: disabledColor, disabledColor: disabledColor,
focusColor: focusColor,
hoverColor: hoverColor,
highlightColor: highlightColor, highlightColor: highlightColor,
splashColor: splashColor, splashColor: splashColor,
colorScheme: colorScheme, colorScheme: colorScheme,
...@@ -220,6 +228,8 @@ class ButtonThemeData extends Diagnosticable { ...@@ -220,6 +228,8 @@ class ButtonThemeData extends Diagnosticable {
this.alignedDropdown = false, this.alignedDropdown = false,
Color buttonColor, Color buttonColor,
Color disabledColor, Color disabledColor,
Color focusColor,
Color hoverColor,
Color highlightColor, Color highlightColor,
Color splashColor, Color splashColor,
this.colorScheme, this.colorScheme,
...@@ -231,6 +241,8 @@ class ButtonThemeData extends Diagnosticable { ...@@ -231,6 +241,8 @@ class ButtonThemeData extends Diagnosticable {
assert(layoutBehavior != null), assert(layoutBehavior != null),
_buttonColor = buttonColor, _buttonColor = buttonColor,
_disabledColor = disabledColor, _disabledColor = disabledColor,
_focusColor = focusColor,
_hoverColor = hoverColor,
_highlightColor = highlightColor, _highlightColor = highlightColor,
_splashColor = splashColor, _splashColor = splashColor,
_padding = padding, _padding = padding,
...@@ -350,6 +362,10 @@ class ButtonThemeData extends Diagnosticable { ...@@ -350,6 +362,10 @@ class ButtonThemeData extends Diagnosticable {
/// ///
/// This property is null by default. /// This property is null by default.
/// ///
/// If the button is in the focused, hovering, or highlighted state, then the
/// [focusColor], [hoverColor], or [highlightColor] will take precedence over
/// the [focusColor].
///
/// See also: /// See also:
/// ///
/// * [getFillColor], which is used by [RaisedButton] to compute its /// * [getFillColor], which is used by [RaisedButton] to compute its
...@@ -366,6 +382,32 @@ class ButtonThemeData extends Diagnosticable { ...@@ -366,6 +382,32 @@ class ButtonThemeData extends Diagnosticable {
/// background fill color. /// background fill color.
final Color _disabledColor; final Color _disabledColor;
/// The fill color of the button when it has the input focus.
///
/// This property is null by default.
///
/// If the button is in the hovering or highlighted state, then the [hoverColor]
/// or [highlightColor] will take precedence over the [focusColor].
///
/// See also:
///
/// * [getFocusColor], which is used by [RaisedButton], [OutlineButton]
/// and [FlatButton].
final Color _focusColor;
/// The fill color of the button when a pointer is hovering over it.
///
/// This property is null by default.
///
/// If the button is in the highlighted state, then the [highlightColor] will
/// take precedence over the [hoverColor].
///
/// See also:
///
/// * [getHoverColor], which is used by [RaisedButton], [OutlineButton]
/// and [FlatButton].
final Color _hoverColor;
/// The color of the overlay that appears when a button is pressed. /// The color of the overlay that appears when a button is pressed.
/// ///
/// This property is null by default. /// This property is null by default.
...@@ -590,6 +632,32 @@ class ButtonThemeData extends Diagnosticable { ...@@ -590,6 +632,32 @@ class ButtonThemeData extends Diagnosticable {
return getTextColor(button).withOpacity(0.12); return getTextColor(button).withOpacity(0.12);
} }
/// The fill color of the button when it has input focus.
///
/// Returns the button's [MaterialButton.focusColor] if it is non-null.
/// Otherwise the focus color depends on [getTextTheme]:
///
/// * [ButtonTextTheme.normal], [ButtonTextTheme.accent]: returns the
/// value of the `focusColor` constructor parameter if it is non-null,
/// otherwise the value of [getTextColor] with opacity 0.12.
/// * [ButtonTextTheme.primary], returns [Colors.transparent].
Color getFocusColor(MaterialButton button) {
return button.focusColor ?? _focusColor ?? getTextColor(button).withOpacity(0.12);
}
/// The fill color of the button when it has input focus.
///
/// Returns the button's [MaterialButton.focusColor] if it is non-null.
/// Otherwise the focus color depends on [getTextTheme]:
///
/// * [ButtonTextTheme.normal], [ButtonTextTheme.accent],
/// [ButtonTextTheme.primary]: returns the value of the `focusColor`
/// constructor parameter if it is non-null, otherwise the value of
/// [getTextColor] with opacity 0.04.
Color getHoverColor(MaterialButton button) {
return button.hoverColor ?? _hoverColor ?? getTextColor(button).withOpacity(0.04);
}
/// The color of the overlay that appears when the [button] is pressed. /// The color of the overlay that appears when the [button] is pressed.
/// ///
/// Returns the button's [MaterialButton.highlightColor] if it is non-null. /// Returns the button's [MaterialButton.highlightColor] if it is non-null.
...@@ -628,6 +696,38 @@ class ButtonThemeData extends Diagnosticable { ...@@ -628,6 +696,38 @@ class ButtonThemeData extends Diagnosticable {
return 2.0; return 2.0;
} }
/// The [button]'s elevation when it is enabled and has focus.
///
/// Returns the button's [MaterialButton.focusElevation] if it is non-null.
///
/// If button is a [FlatButton] or an [OutlineButton] then the focus
/// elevation is 0.0, otherwise the highlight elevation is 4.0.
double getFocusElevation(MaterialButton button) {
if (button.focusElevation != null)
return button.focusElevation;
if (button is FlatButton)
return 0.0;
if (button is OutlineButton)
return 0.0;
return 4.0;
}
/// The [button]'s elevation when it is enabled and has focus.
///
/// Returns the button's [MaterialButton.hoverElevation] if it is non-null.
///
/// If button is a [FlatButton] or an [OutlineButton] then the hover
/// elevation is 0.0, otherwise the highlight elevation is 4.0.
double getHoverElevation(MaterialButton button) {
if (button.hoverElevation != null)
return button.hoverElevation;
if (button is FlatButton)
return 0.0;
if (button is OutlineButton)
return 0.0;
return 4.0;
}
/// The [button]'s elevation when it is enabled and has been pressed. /// The [button]'s elevation when it is enabled and has been pressed.
/// ///
/// Returns the button's [MaterialButton.highlightElevation] if it is non-null. /// Returns the button's [MaterialButton.highlightElevation] if it is non-null.
...@@ -737,6 +837,8 @@ class ButtonThemeData extends Diagnosticable { ...@@ -737,6 +837,8 @@ class ButtonThemeData extends Diagnosticable {
bool alignedDropdown, bool alignedDropdown,
Color buttonColor, Color buttonColor,
Color disabledColor, Color disabledColor,
Color focusColor,
Color hoverColor,
Color highlightColor, Color highlightColor,
Color splashColor, Color splashColor,
ColorScheme colorScheme, ColorScheme colorScheme,
...@@ -752,6 +854,8 @@ class ButtonThemeData extends Diagnosticable { ...@@ -752,6 +854,8 @@ class ButtonThemeData extends Diagnosticable {
alignedDropdown: alignedDropdown ?? this.alignedDropdown, alignedDropdown: alignedDropdown ?? this.alignedDropdown,
buttonColor: buttonColor ?? _buttonColor, buttonColor: buttonColor ?? _buttonColor,
disabledColor: disabledColor ?? _disabledColor, disabledColor: disabledColor ?? _disabledColor,
focusColor: focusColor ?? _focusColor,
hoverColor: hoverColor ?? _hoverColor,
highlightColor: highlightColor ?? _highlightColor, highlightColor: highlightColor ?? _highlightColor,
splashColor: splashColor ?? _splashColor, splashColor: splashColor ?? _splashColor,
colorScheme: colorScheme ?? this.colorScheme, colorScheme: colorScheme ?? this.colorScheme,
...@@ -772,6 +876,8 @@ class ButtonThemeData extends Diagnosticable { ...@@ -772,6 +876,8 @@ class ButtonThemeData extends Diagnosticable {
&& alignedDropdown == typedOther.alignedDropdown && alignedDropdown == typedOther.alignedDropdown
&& _buttonColor == typedOther._buttonColor && _buttonColor == typedOther._buttonColor
&& _disabledColor == typedOther._disabledColor && _disabledColor == typedOther._disabledColor
&& _focusColor == typedOther._focusColor
&& _hoverColor == typedOther._hoverColor
&& _highlightColor == typedOther._highlightColor && _highlightColor == typedOther._highlightColor
&& _splashColor == typedOther._splashColor && _splashColor == typedOther._splashColor
&& colorScheme == typedOther.colorScheme && colorScheme == typedOther.colorScheme
...@@ -789,6 +895,8 @@ class ButtonThemeData extends Diagnosticable { ...@@ -789,6 +895,8 @@ class ButtonThemeData extends Diagnosticable {
alignedDropdown, alignedDropdown,
_buttonColor, _buttonColor,
_disabledColor, _disabledColor,
_focusColor,
_hoverColor,
_highlightColor, _highlightColor,
_splashColor, _splashColor,
colorScheme, colorScheme,
...@@ -812,6 +920,8 @@ class ButtonThemeData extends Diagnosticable { ...@@ -812,6 +920,8 @@ class ButtonThemeData extends Diagnosticable {
)); ));
properties.add(DiagnosticsProperty<Color>('buttonColor', _buttonColor, defaultValue: null)); properties.add(DiagnosticsProperty<Color>('buttonColor', _buttonColor, defaultValue: null));
properties.add(DiagnosticsProperty<Color>('disabledColor', _disabledColor, defaultValue: null)); properties.add(DiagnosticsProperty<Color>('disabledColor', _disabledColor, defaultValue: null));
properties.add(DiagnosticsProperty<Color>('focusColor', _focusColor, defaultValue: null));
properties.add(DiagnosticsProperty<Color>('hoverColor', _hoverColor, defaultValue: null));
properties.add(DiagnosticsProperty<Color>('highlightColor', _highlightColor, defaultValue: null)); properties.add(DiagnosticsProperty<Color>('highlightColor', _highlightColor, defaultValue: null));
properties.add(DiagnosticsProperty<Color>('splashColor', _splashColor, defaultValue: null)); properties.add(DiagnosticsProperty<Color>('splashColor', _splashColor, defaultValue: null));
properties.add(DiagnosticsProperty<ColorScheme>('colorScheme', colorScheme, defaultValue: defaultTheme.colorScheme)); properties.add(DiagnosticsProperty<ColorScheme>('colorScheme', colorScheme, defaultValue: defaultTheme.colorScheme));
......
...@@ -102,12 +102,15 @@ class FlatButton extends MaterialButton { ...@@ -102,12 +102,15 @@ class FlatButton extends MaterialButton {
Color disabledTextColor, Color disabledTextColor,
Color color, Color color,
Color disabledColor, Color disabledColor,
Color focusColor,
Color hoverColor,
Color highlightColor, Color highlightColor,
Color splashColor, Color splashColor,
Brightness colorBrightness, Brightness colorBrightness,
EdgeInsetsGeometry padding, EdgeInsetsGeometry padding,
ShapeBorder shape, ShapeBorder shape,
Clip clipBehavior = Clip.none, Clip clipBehavior,
FocusNode focusNode,
MaterialTapTargetSize materialTapTargetSize, MaterialTapTargetSize materialTapTargetSize,
@required Widget child, @required Widget child,
}) : super( }) : super(
...@@ -119,12 +122,15 @@ class FlatButton extends MaterialButton { ...@@ -119,12 +122,15 @@ class FlatButton extends MaterialButton {
disabledTextColor: disabledTextColor, disabledTextColor: disabledTextColor,
color: color, color: color,
disabledColor: disabledColor, disabledColor: disabledColor,
focusColor: focusColor,
hoverColor: hoverColor,
highlightColor: highlightColor, highlightColor: highlightColor,
splashColor: splashColor, splashColor: splashColor,
colorBrightness: colorBrightness, colorBrightness: colorBrightness,
padding: padding, padding: padding,
shape: shape, shape: shape,
clipBehavior: clipBehavior, clipBehavior: clipBehavior,
focusNode: focusNode,
materialTapTargetSize: materialTapTargetSize, materialTapTargetSize: materialTapTargetSize,
child: child, child: child,
); );
...@@ -145,12 +151,15 @@ class FlatButton extends MaterialButton { ...@@ -145,12 +151,15 @@ class FlatButton extends MaterialButton {
Color disabledTextColor, Color disabledTextColor,
Color color, Color color,
Color disabledColor, Color disabledColor,
Color focusColor,
Color hoverColor,
Color highlightColor, Color highlightColor,
Color splashColor, Color splashColor,
Brightness colorBrightness, Brightness colorBrightness,
EdgeInsetsGeometry padding, EdgeInsetsGeometry padding,
ShapeBorder shape, ShapeBorder shape,
Clip clipBehavior, Clip clipBehavior,
FocusNode focusNode,
MaterialTapTargetSize materialTapTargetSize, MaterialTapTargetSize materialTapTargetSize,
@required Widget icon, @required Widget icon,
@required Widget label, @required Widget label,
...@@ -163,39 +172,27 @@ class FlatButton extends MaterialButton { ...@@ -163,39 +172,27 @@ class FlatButton extends MaterialButton {
return RawMaterialButton( return RawMaterialButton(
onPressed: onPressed, onPressed: onPressed,
onHighlightChanged: onHighlightChanged, onHighlightChanged: onHighlightChanged,
clipBehavior: clipBehavior ?? Clip.none,
fillColor: buttonTheme.getFillColor(this), fillColor: buttonTheme.getFillColor(this),
textStyle: theme.textTheme.button.copyWith(color: buttonTheme.getTextColor(this)), textStyle: theme.textTheme.button.copyWith(color: buttonTheme.getTextColor(this)),
focusColor: buttonTheme.getFocusColor(this),
hoverColor: buttonTheme.getHoverColor(this),
highlightColor: buttonTheme.getHighlightColor(this), highlightColor: buttonTheme.getHighlightColor(this),
splashColor: buttonTheme.getSplashColor(this), splashColor: buttonTheme.getSplashColor(this),
elevation: buttonTheme.getElevation(this), elevation: buttonTheme.getElevation(this),
focusElevation: buttonTheme.getFocusElevation(this),
hoverElevation: buttonTheme.getHoverElevation(this),
highlightElevation: buttonTheme.getHighlightElevation(this), highlightElevation: buttonTheme.getHighlightElevation(this),
disabledElevation: buttonTheme.getDisabledElevation(this), disabledElevation: buttonTheme.getDisabledElevation(this),
padding: buttonTheme.getPadding(this), padding: buttonTheme.getPadding(this),
constraints: buttonTheme.getConstraints(this), constraints: buttonTheme.getConstraints(this),
shape: buttonTheme.getShape(this), shape: buttonTheme.getShape(this),
animationDuration: buttonTheme.getAnimationDuration(this), clipBehavior: clipBehavior ?? Clip.none,
focusNode: focusNode,
materialTapTargetSize: buttonTheme.getMaterialTapTargetSize(this), materialTapTargetSize: buttonTheme.getMaterialTapTargetSize(this),
animationDuration: buttonTheme.getAnimationDuration(this),
child: child, child: child,
); );
} }
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(ObjectFlagProperty<VoidCallback>('onPressed', onPressed, ifNull: 'disabled'));
properties.add(DiagnosticsProperty<ButtonTextTheme>('textTheme', textTheme, defaultValue: null));
properties.add(DiagnosticsProperty<Color>('textColor', textColor, defaultValue: null));
properties.add(DiagnosticsProperty<Color>('disabledTextColor', disabledTextColor, defaultValue: null));
properties.add(DiagnosticsProperty<Color>('color', color, defaultValue: null));
properties.add(DiagnosticsProperty<Color>('disabledColor', disabledColor, defaultValue: null));
properties.add(DiagnosticsProperty<Color>('highlightColor', highlightColor, defaultValue: null));
properties.add(DiagnosticsProperty<Color>('splashColor', splashColor, defaultValue: null));
properties.add(DiagnosticsProperty<Brightness>('colorBrightness', colorBrightness, defaultValue: null));
properties.add(DiagnosticsProperty<EdgeInsetsGeometry>('padding', padding, defaultValue: null));
properties.add(DiagnosticsProperty<ShapeBorder>('shape', shape, defaultValue: null));
properties.add(DiagnosticsProperty<MaterialTapTargetSize>('materialTapTargetSize', materialTapTargetSize, defaultValue: null));
}
} }
/// The type of of FlatButtons created with [FlatButton.icon]. /// The type of of FlatButtons created with [FlatButton.icon].
...@@ -212,12 +209,15 @@ class _FlatButtonWithIcon extends FlatButton with MaterialButtonWithIconMixin { ...@@ -212,12 +209,15 @@ class _FlatButtonWithIcon extends FlatButton with MaterialButtonWithIconMixin {
Color disabledTextColor, Color disabledTextColor,
Color color, Color color,
Color disabledColor, Color disabledColor,
Color focusColor,
Color hoverColor,
Color highlightColor, Color highlightColor,
Color splashColor, Color splashColor,
Brightness colorBrightness, Brightness colorBrightness,
EdgeInsetsGeometry padding, EdgeInsetsGeometry padding,
ShapeBorder shape, ShapeBorder shape,
Clip clipBehavior, Clip clipBehavior,
FocusNode focusNode,
MaterialTapTargetSize materialTapTargetSize, MaterialTapTargetSize materialTapTargetSize,
@required Widget icon, @required Widget icon,
@required Widget label, @required Widget label,
...@@ -232,12 +232,15 @@ class _FlatButtonWithIcon extends FlatButton with MaterialButtonWithIconMixin { ...@@ -232,12 +232,15 @@ class _FlatButtonWithIcon extends FlatButton with MaterialButtonWithIconMixin {
disabledTextColor: disabledTextColor, disabledTextColor: disabledTextColor,
color: color, color: color,
disabledColor: disabledColor, disabledColor: disabledColor,
focusColor: focusColor,
hoverColor: hoverColor,
highlightColor: highlightColor, highlightColor: highlightColor,
splashColor: splashColor, splashColor: splashColor,
colorBrightness: colorBrightness, colorBrightness: colorBrightness,
padding: padding, padding: padding,
shape: shape, shape: shape,
clipBehavior: clipBehavior, clipBehavior: clipBehavior,
focusNode: focusNode,
materialTapTargetSize: materialTapTargetSize, materialTapTargetSize: materialTapTargetSize,
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
import 'dart:math' as math; import 'dart:math' as math;
import 'package:flutter/foundation.dart';
import 'package:flutter/painting.dart'; import 'package:flutter/painting.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
...@@ -126,17 +127,24 @@ class FloatingActionButton extends StatelessWidget { ...@@ -126,17 +127,24 @@ class FloatingActionButton extends StatelessWidget {
this.tooltip, this.tooltip,
this.foregroundColor, this.foregroundColor,
this.backgroundColor, this.backgroundColor,
this.focusColor,
this.hoverColor,
this.heroTag = const _DefaultHeroTag(), this.heroTag = const _DefaultHeroTag(),
this.elevation, this.elevation,
this.focusElevation,
this.hoverElevation,
this.highlightElevation, this.highlightElevation,
this.disabledElevation, this.disabledElevation,
@required this.onPressed, @required this.onPressed,
this.mini = false, this.mini = false,
this.shape, this.shape,
this.clipBehavior = Clip.none, this.clipBehavior = Clip.none,
this.focusNode,
this.materialTapTargetSize, this.materialTapTargetSize,
this.isExtended = false, this.isExtended = false,
}) : assert(elevation == null || elevation >= 0.0), }) : assert(elevation == null || elevation >= 0.0),
assert(focusElevation == null || focusElevation >= 0.0),
assert(hoverElevation == null || hoverElevation >= 0.0),
assert(highlightElevation == null || highlightElevation >= 0.0), assert(highlightElevation == null || highlightElevation >= 0.0),
assert(disabledElevation == null || disabledElevation >= 0.0), assert(disabledElevation == null || disabledElevation >= 0.0),
assert(mini != null), assert(mini != null),
...@@ -155,8 +163,12 @@ class FloatingActionButton extends StatelessWidget { ...@@ -155,8 +163,12 @@ class FloatingActionButton extends StatelessWidget {
this.tooltip, this.tooltip,
this.foregroundColor, this.foregroundColor,
this.backgroundColor, this.backgroundColor,
this.focusColor,
this.hoverColor,
this.heroTag = const _DefaultHeroTag(), this.heroTag = const _DefaultHeroTag(),
this.elevation, this.elevation,
this.focusElevation,
this.hoverElevation,
this.highlightElevation, this.highlightElevation,
this.disabledElevation, this.disabledElevation,
@required this.onPressed, @required this.onPressed,
...@@ -164,13 +176,15 @@ class FloatingActionButton extends StatelessWidget { ...@@ -164,13 +176,15 @@ class FloatingActionButton extends StatelessWidget {
this.isExtended = true, this.isExtended = true,
this.materialTapTargetSize, this.materialTapTargetSize,
this.clipBehavior = Clip.none, this.clipBehavior = Clip.none,
this.focusNode,
Widget icon, Widget icon,
@required Widget label, @required Widget label,
}) : assert(elevation == null || elevation >= 0.0), }) : assert(elevation == null || elevation >= 0.0),
assert(focusElevation == null || focusElevation >= 0.0),
assert(hoverElevation == null || hoverElevation >= 0.0),
assert(highlightElevation == null || highlightElevation >= 0.0), assert(highlightElevation == null || highlightElevation >= 0.0),
assert(disabledElevation == null || disabledElevation >= 0.0), assert(disabledElevation == null || disabledElevation >= 0.0),
assert(isExtended != null), assert(isExtended != null),
assert(clipBehavior != null),
_sizeConstraints = _kExtendedSizeConstraints, _sizeConstraints = _kExtendedSizeConstraints,
mini = false, mini = false,
child = _ChildOverflowBox( child = _ChildOverflowBox(
...@@ -214,6 +228,17 @@ class FloatingActionButton extends StatelessWidget { ...@@ -214,6 +228,17 @@ class FloatingActionButton extends StatelessWidget {
/// Defaults to [ThemeData.accentColor] for the current theme. /// Defaults to [ThemeData.accentColor] for the current theme.
final Color backgroundColor; final Color backgroundColor;
/// The color to use for filling the button when the button has input focus.
///
/// Defaults to [ThemeData.focusColor] for the current theme.
final Color focusColor;
/// The color to use for filling the button when the button has a pointer
/// hovering over it.
///
/// Defaults to [ThemeData.hoverColor] for the current theme.
final Color hoverColor;
/// The tag to apply to the button's [Hero] widget. /// The tag to apply to the button's [Hero] widget.
/// ///
/// Defaults to a tag that matches other floating action buttons. /// Defaults to a tag that matches other floating action buttons.
...@@ -246,6 +271,36 @@ class FloatingActionButton extends StatelessWidget { ...@@ -246,6 +271,36 @@ class FloatingActionButton extends StatelessWidget {
/// * [disabledElevation], the elevation when the button is disabled. /// * [disabledElevation], the elevation when the button is disabled.
final double elevation; final double elevation;
/// The z-coordinate at which to place this button relative to its parent when
/// the button has the input focus.
///
/// This controls the size of the shadow below the floating action button.
///
/// Defaults to 8, the appropriate elevation for floating action buttons
/// while they have focus. The value is always non-negative.
///
/// See also:
///
/// * [elevation], the default elevation.
/// * [highlightElevation], the elevation when the button is pressed.
/// * [disabledElevation], the elevation when the button is disabled.
final double focusElevation;
/// The z-coordinate at which to place this button relative to its parent when
/// the button is enabled and has a pointer hovering over it.
///
/// This controls the size of the shadow below the floating action button.
///
/// Defaults to 8, the appropriate elevation for floating action buttons while
/// they have a pointer hovering over them. The value is always non-negative.
///
/// See also:
///
/// * [elevation], the default elevation.
/// * [highlightElevation], the elevation when the button is pressed.
/// * [disabledElevation], the elevation when the button is disabled.
final double hoverElevation;
/// The z-coordinate at which to place this button relative to its parent when /// The z-coordinate at which to place this button relative to its parent when
/// the user is touching the button. /// the user is touching the button.
/// ///
...@@ -305,6 +360,14 @@ class FloatingActionButton extends StatelessWidget { ...@@ -305,6 +360,14 @@ class FloatingActionButton extends StatelessWidget {
/// floating action buttons are scaled and faded in. /// floating action buttons are scaled and faded in.
final bool isExtended; final bool isExtended;
/// An optional focus node to use for requesting focus when pressed.
///
/// If not supplied, the button will create and host its own [FocusNode].
///
/// If supplied, the given focusNode will be _hosted_ by this widget. See
/// [FocusNode] for more information on what that implies.
final FocusNode focusNode;
/// Configures the minimum size of the tap target. /// Configures the minimum size of the tap target.
/// ///
/// Defaults to [ThemeData.materialTapTargetSize]. /// Defaults to [ThemeData.materialTapTargetSize].
...@@ -317,6 +380,10 @@ class FloatingActionButton extends StatelessWidget { ...@@ -317,6 +380,10 @@ class FloatingActionButton extends StatelessWidget {
final BoxConstraints _sizeConstraints; final BoxConstraints _sizeConstraints;
static const double _defaultElevation = 6; static const double _defaultElevation = 6;
// TODO(gspencer): verify focus and hover elevation values are correct
// according to spec.
static const double _defaultFocusElevation = 8;
static const double _defaultHoverElevation = 10;
static const double _defaultHighlightElevation = 12; static const double _defaultHighlightElevation = 12;
static const ShapeBorder _defaultShape = CircleBorder(); static const ShapeBorder _defaultShape = CircleBorder();
static const ShapeBorder _defaultExtendedShape = StadiumBorder(); static const ShapeBorder _defaultExtendedShape = StadiumBorder();
...@@ -326,16 +393,28 @@ class FloatingActionButton extends StatelessWidget { ...@@ -326,16 +393,28 @@ class FloatingActionButton extends StatelessWidget {
final ThemeData theme = Theme.of(context); final ThemeData theme = Theme.of(context);
final FloatingActionButtonThemeData floatingActionButtonTheme = theme.floatingActionButtonTheme; final FloatingActionButtonThemeData floatingActionButtonTheme = theme.floatingActionButtonTheme;
final Color backgroundColor = this.backgroundColor
?? floatingActionButtonTheme.backgroundColor
?? theme.colorScheme.secondary;
final Color foregroundColor = this.foregroundColor final Color foregroundColor = this.foregroundColor
?? floatingActionButtonTheme.foregroundColor ?? floatingActionButtonTheme.foregroundColor
?? theme.accentIconTheme.color ?? theme.accentIconTheme.color
?? theme.colorScheme.onSecondary; ?? theme.colorScheme.onSecondary;
final Color backgroundColor = this.backgroundColor
?? floatingActionButtonTheme.backgroundColor
?? theme.colorScheme.secondary;
final Color focusColor = this.focusColor
?? floatingActionButtonTheme.focusColor
?? theme.focusColor;
final Color hoverColor = this.hoverColor
?? floatingActionButtonTheme.hoverColor
?? theme.hoverColor;
final double elevation = this.elevation final double elevation = this.elevation
?? floatingActionButtonTheme.elevation ?? floatingActionButtonTheme.elevation
?? _defaultElevation; ?? _defaultElevation;
final double focusElevation = this.focusElevation
?? floatingActionButtonTheme.focusElevation
?? _defaultFocusElevation;
final double hoverElevation = this.hoverElevation
?? floatingActionButtonTheme.hoverElevation
?? _defaultHoverElevation;
final double disabledElevation = this.disabledElevation final double disabledElevation = this.disabledElevation
?? floatingActionButtonTheme.disabledElevation ?? floatingActionButtonTheme.disabledElevation
?? elevation; ?? elevation;
...@@ -366,14 +445,19 @@ class FloatingActionButton extends StatelessWidget { ...@@ -366,14 +445,19 @@ class FloatingActionButton extends StatelessWidget {
result = RawMaterialButton( result = RawMaterialButton(
onPressed: onPressed, onPressed: onPressed,
elevation: elevation, elevation: elevation,
focusElevation: focusElevation,
hoverElevation: hoverElevation,
highlightElevation: highlightElevation, highlightElevation: highlightElevation,
disabledElevation: disabledElevation, disabledElevation: disabledElevation,
constraints: _sizeConstraints, constraints: _sizeConstraints,
materialTapTargetSize: materialTapTargetSize, materialTapTargetSize: materialTapTargetSize,
fillColor: backgroundColor, fillColor: backgroundColor,
focusColor: focusColor,
hoverColor: hoverColor,
textStyle: textStyle, textStyle: textStyle,
shape: shape, shape: shape,
clipBehavior: clipBehavior, clipBehavior: clipBehavior ?? Clip.none,
focusNode: focusNode,
child: result, child: result,
); );
...@@ -395,6 +479,27 @@ class FloatingActionButton extends StatelessWidget { ...@@ -395,6 +479,27 @@ class FloatingActionButton extends StatelessWidget {
return result; return result;
} }
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(ObjectFlagProperty<VoidCallback>('onPressed', onPressed, ifNull: 'disabled'));
properties.add(StringProperty('tooltip', tooltip, defaultValue: null));
properties.add(DiagnosticsProperty<Color>('foregroundColor', foregroundColor, defaultValue: null));
properties.add(DiagnosticsProperty<Color>('backgroundColor', backgroundColor, defaultValue: null));
properties.add(DiagnosticsProperty<Color>('focusColor', focusColor, defaultValue: null));
properties.add(DiagnosticsProperty<Color>('hoverColor', hoverColor, defaultValue: null));
properties.add(ObjectFlagProperty<Object>('heroTag', heroTag, ifPresent: 'hero'));
properties.add(DiagnosticsProperty<double>('elevation', elevation, defaultValue: null));
properties.add(DiagnosticsProperty<double>('focusElevation', focusElevation, defaultValue: null));
properties.add(DiagnosticsProperty<double>('hoverElevation', hoverElevation, defaultValue: null));
properties.add(DiagnosticsProperty<double>('highlightElevation', highlightElevation, defaultValue: null));
properties.add(DiagnosticsProperty<double>('disabledElevation', disabledElevation, defaultValue: null));
properties.add(DiagnosticsProperty<ShapeBorder>('shape', shape, defaultValue: null));
properties.add(DiagnosticsProperty<FocusNode>('focusNode', focusNode, defaultValue: null));
properties.add(FlagProperty('isExtended', value: isExtended, ifTrue: 'extended'));
properties.add(DiagnosticsProperty<MaterialTapTargetSize>('materialTapTargetSize', materialTapTargetSize, defaultValue: null));
}
} }
// This widget's size matches its child's size unless its constraints // This widget's size matches its child's size unless its constraints
......
...@@ -30,26 +30,49 @@ class FloatingActionButtonThemeData extends Diagnosticable { ...@@ -30,26 +30,49 @@ class FloatingActionButtonThemeData extends Diagnosticable {
/// Creates a theme that can be used for /// Creates a theme that can be used for
/// [ThemeData.floatingActionButtonTheme]. /// [ThemeData.floatingActionButtonTheme].
const FloatingActionButtonThemeData({ const FloatingActionButtonThemeData({
this.backgroundColor,
this.foregroundColor, this.foregroundColor,
this.backgroundColor,
this.focusColor,
this.hoverColor,
this.elevation, this.elevation,
this.focusElevation,
this.hoverElevation,
this.disabledElevation, this.disabledElevation,
this.highlightElevation, this.highlightElevation,
this.shape, this.shape,
}); });
/// Color to be used for the unselected, enabled [FloatingActionButton]'s
/// foreground.
final Color foregroundColor;
/// Color to be used for the unselected, enabled [FloatingActionButton]'s /// Color to be used for the unselected, enabled [FloatingActionButton]'s
/// background. /// background.
final Color backgroundColor; final Color backgroundColor;
/// Color to be used for the unselected, enabled [FloatingActionButton]'s /// The color to use for filling the button when the button has input focus.
/// foreground. final Color focusColor;
final Color foregroundColor;
/// The color to use for filling the button when the button has a pointer
/// hovering over it.
final Color hoverColor;
/// The z-coordinate to be used for the unselected, enabled /// The z-coordinate to be used for the unselected, enabled
/// [FloatingActionButton]'s elevation foreground. /// [FloatingActionButton]'s elevation foreground.
final double elevation; final double elevation;
/// The z-coordinate at which to place this button relative to its parent when
/// the button has the input focus.
///
/// This controls the size of the shadow below the floating action button.
final double focusElevation;
/// The z-coordinate at which to place this button relative to its parent when
/// the button is enabled and has a pointer hovering over it.
///
/// This controls the size of the shadow below the floating action button.
final double hoverElevation;
/// The z-coordinate to be used for the disabled [FloatingActionButton]'s /// The z-coordinate to be used for the disabled [FloatingActionButton]'s
/// elevation foreground. /// elevation foreground.
final double disabledElevation; final double disabledElevation;
...@@ -64,17 +87,25 @@ class FloatingActionButtonThemeData extends Diagnosticable { ...@@ -64,17 +87,25 @@ class FloatingActionButtonThemeData extends Diagnosticable {
/// Creates a copy of this object with the given fields replaced with the /// Creates a copy of this object with the given fields replaced with the
/// new values. /// new values.
FloatingActionButtonThemeData copyWith({ FloatingActionButtonThemeData copyWith({
Color backgroundColor,
Color foregroundColor, Color foregroundColor,
Color backgroundColor,
Color focusColor,
Color hoverColor,
double elevation, double elevation,
double focusElevation,
double hoverElevation,
double disabledElevation, double disabledElevation,
double highlightElevation, double highlightElevation,
ShapeBorder shape, ShapeBorder shape,
}) { }) {
return FloatingActionButtonThemeData( return FloatingActionButtonThemeData(
backgroundColor: backgroundColor ?? this.backgroundColor,
foregroundColor: foregroundColor ?? this.foregroundColor, foregroundColor: foregroundColor ?? this.foregroundColor,
backgroundColor: backgroundColor ?? this.backgroundColor,
focusColor: focusColor ?? this.focusColor,
hoverColor: hoverColor ?? this.hoverColor,
elevation: elevation ?? this.elevation, elevation: elevation ?? this.elevation,
focusElevation: focusElevation ?? this.focusElevation,
hoverElevation: hoverElevation ?? this.hoverElevation,
disabledElevation: disabledElevation ?? this.disabledElevation, disabledElevation: disabledElevation ?? this.disabledElevation,
highlightElevation: highlightElevation ?? this.highlightElevation, highlightElevation: highlightElevation ?? this.highlightElevation,
shape: shape ?? this.shape, shape: shape ?? this.shape,
...@@ -91,9 +122,13 @@ class FloatingActionButtonThemeData extends Diagnosticable { ...@@ -91,9 +122,13 @@ class FloatingActionButtonThemeData extends Diagnosticable {
if (a == null && b == null) if (a == null && b == null)
return null; return null;
return FloatingActionButtonThemeData( return FloatingActionButtonThemeData(
backgroundColor: Color.lerp(a?.backgroundColor, b?.backgroundColor, t),
foregroundColor: Color.lerp(a?.foregroundColor, b?.foregroundColor, t), foregroundColor: Color.lerp(a?.foregroundColor, b?.foregroundColor, t),
backgroundColor: Color.lerp(a?.backgroundColor, b?.backgroundColor, t),
focusColor: Color.lerp(a?.focusColor, b?.focusColor, t),
hoverColor: Color.lerp(a?.hoverColor, b?.hoverColor, t),
elevation: lerpDouble(a?.elevation, b?.elevation, t), elevation: lerpDouble(a?.elevation, b?.elevation, t),
focusElevation: lerpDouble(a?.focusElevation, b?.focusElevation, t),
hoverElevation: lerpDouble(a?.hoverElevation, b?.hoverElevation, t),
disabledElevation: lerpDouble(a?.disabledElevation, b?.disabledElevation, t), disabledElevation: lerpDouble(a?.disabledElevation, b?.disabledElevation, t),
highlightElevation: lerpDouble(a?.highlightElevation, b?.highlightElevation, t), highlightElevation: lerpDouble(a?.highlightElevation, b?.highlightElevation, t),
shape: ShapeBorder.lerp(a?.shape, b?.shape, t), shape: ShapeBorder.lerp(a?.shape, b?.shape, t),
...@@ -103,9 +138,13 @@ class FloatingActionButtonThemeData extends Diagnosticable { ...@@ -103,9 +138,13 @@ class FloatingActionButtonThemeData extends Diagnosticable {
@override @override
int get hashCode { int get hashCode {
return hashValues( return hashValues(
backgroundColor,
foregroundColor, foregroundColor,
backgroundColor,
focusColor,
hoverColor,
elevation, elevation,
focusElevation,
hoverElevation,
disabledElevation, disabledElevation,
highlightElevation, highlightElevation,
shape, shape,
...@@ -119,9 +158,13 @@ class FloatingActionButtonThemeData extends Diagnosticable { ...@@ -119,9 +158,13 @@ class FloatingActionButtonThemeData extends Diagnosticable {
if (other.runtimeType != runtimeType) if (other.runtimeType != runtimeType)
return false; return false;
final FloatingActionButtonThemeData otherData = other; final FloatingActionButtonThemeData otherData = other;
return otherData.backgroundColor == backgroundColor return otherData.foregroundColor == foregroundColor
&& otherData.foregroundColor == foregroundColor && otherData.backgroundColor == backgroundColor
&& otherData.focusColor == focusColor
&& otherData.hoverColor == hoverColor
&& otherData.elevation == elevation && otherData.elevation == elevation
&& otherData.focusElevation == focusElevation
&& otherData.hoverElevation == hoverElevation
&& otherData.disabledElevation == disabledElevation && otherData.disabledElevation == disabledElevation
&& otherData.highlightElevation == highlightElevation && otherData.highlightElevation == highlightElevation
&& otherData.shape == shape; && otherData.shape == shape;
...@@ -132,9 +175,13 @@ class FloatingActionButtonThemeData extends Diagnosticable { ...@@ -132,9 +175,13 @@ class FloatingActionButtonThemeData extends Diagnosticable {
super.debugFillProperties(properties); super.debugFillProperties(properties);
const FloatingActionButtonThemeData defaultData = FloatingActionButtonThemeData(); const FloatingActionButtonThemeData defaultData = FloatingActionButtonThemeData();
properties.add(DiagnosticsProperty<Color>('backgroundColor', backgroundColor, defaultValue: defaultData.backgroundColor));
properties.add(DiagnosticsProperty<Color>('foregroundColor', foregroundColor, defaultValue: defaultData.foregroundColor)); properties.add(DiagnosticsProperty<Color>('foregroundColor', foregroundColor, defaultValue: defaultData.foregroundColor));
properties.add(DiagnosticsProperty<Color>('backgroundColor', backgroundColor, defaultValue: defaultData.backgroundColor));
properties.add(DiagnosticsProperty<Color>('focusColor', focusColor, defaultValue: defaultData.focusColor));
properties.add(DiagnosticsProperty<Color>('hoverColor', hoverColor, defaultValue: defaultData.hoverColor));
properties.add(DiagnosticsProperty<double>('elevation', elevation, defaultValue: defaultData.elevation)); properties.add(DiagnosticsProperty<double>('elevation', elevation, defaultValue: defaultData.elevation));
properties.add(DiagnosticsProperty<double>('focusElevation', focusElevation, defaultValue: defaultData.focusElevation));
properties.add(DiagnosticsProperty<double>('hoverElevation', hoverElevation, defaultValue: defaultData.hoverElevation));
properties.add(DiagnosticsProperty<double>('disabledElevation', disabledElevation, defaultValue: defaultData.disabledElevation)); properties.add(DiagnosticsProperty<double>('disabledElevation', disabledElevation, defaultValue: defaultData.disabledElevation));
properties.add(DiagnosticsProperty<double>('highlightElevation', highlightElevation, defaultValue: defaultData.highlightElevation)); properties.add(DiagnosticsProperty<double>('highlightElevation', highlightElevation, defaultValue: defaultData.highlightElevation));
properties.add(DiagnosticsProperty<ShapeBorder>('shape', shape, defaultValue: defaultData.shape)); properties.add(DiagnosticsProperty<ShapeBorder>('shape', shape, defaultValue: defaultData.shape));
......
...@@ -142,10 +142,13 @@ class IconButton extends StatelessWidget { ...@@ -142,10 +142,13 @@ class IconButton extends StatelessWidget {
this.alignment = Alignment.center, this.alignment = Alignment.center,
@required this.icon, @required this.icon,
this.color, this.color,
this.focusColor,
this.hoverColor,
this.highlightColor, this.highlightColor,
this.splashColor, this.splashColor,
this.disabledColor, this.disabledColor,
@required this.onPressed, @required this.onPressed,
this.focusNode,
this.tooltip, this.tooltip,
}) : assert(iconSize != null), }) : assert(iconSize != null),
assert(padding != null), assert(padding != null),
...@@ -195,6 +198,16 @@ class IconButton extends StatelessWidget { ...@@ -195,6 +198,16 @@ class IconButton extends StatelessWidget {
/// See [Icon], [ImageIcon]. /// See [Icon], [ImageIcon].
final Widget icon; final Widget icon;
/// The color for the button's icon when it has the input focus.
///
/// Defaults to [ThemeData.focusColor] of the ambient theme.
final Color focusColor;
/// The color for the button's icon when a pointer is hovering over it.
///
/// Defaults to [ThemeData.hoverColor] of the ambient theme.
final Color hoverColor;
/// The color to use for the icon inside the button, if the icon is enabled. /// The color to use for the icon inside the button, if the icon is enabled.
/// Defaults to leaving this up to the [icon] widget. /// Defaults to leaving this up to the [icon] widget.
/// ///
...@@ -242,6 +255,14 @@ class IconButton extends StatelessWidget { ...@@ -242,6 +255,14 @@ class IconButton extends StatelessWidget {
/// If this is set to null, the button will be disabled. /// If this is set to null, the button will be disabled.
final VoidCallback onPressed; final VoidCallback onPressed;
/// An optional focus node to use for requesting focus when pressed.
///
/// If not supplied, the button will create and host its own [FocusNode].
///
/// If supplied, the given focusNode will be _hosted_ by this widget. See
/// [FocusNode] for more information on what that implies.
final FocusNode focusNode;
/// Text that describes the action that will occur when the button is pressed. /// Text that describes the action that will occur when the button is pressed.
/// ///
/// This text is displayed when the user long-presses on the button and is /// This text is displayed when the user long-presses on the button and is
...@@ -257,25 +278,21 @@ class IconButton extends StatelessWidget { ...@@ -257,25 +278,21 @@ class IconButton extends StatelessWidget {
else else
currentColor = disabledColor ?? Theme.of(context).disabledColor; currentColor = disabledColor ?? Theme.of(context).disabledColor;
Widget result = Semantics( Widget result = ConstrainedBox(
button: true, constraints: const BoxConstraints(minWidth: _kMinButtonSize, minHeight: _kMinButtonSize),
enabled: onPressed != null, child: Padding(
child: ConstrainedBox( padding: padding,
constraints: const BoxConstraints(minWidth: _kMinButtonSize, minHeight: _kMinButtonSize), child: SizedBox(
child: Padding( height: iconSize,
padding: padding, width: iconSize,
child: SizedBox( child: Align(
height: iconSize, alignment: alignment,
width: iconSize, child: IconTheme.merge(
child: Align( data: IconThemeData(
alignment: alignment, size: iconSize,
child: IconTheme.merge( color: currentColor,
data: IconThemeData(
size: iconSize,
color: currentColor,
),
child: icon,
), ),
child: icon,
), ),
), ),
), ),
...@@ -288,15 +305,25 @@ class IconButton extends StatelessWidget { ...@@ -288,15 +305,25 @@ class IconButton extends StatelessWidget {
child: result, child: result,
); );
} }
return InkResponse(
onTap: onPressed, return Semantics(
child: result, button: true,
highlightColor: highlightColor ?? Theme.of(context).highlightColor, enabled: onPressed != null,
splashColor: splashColor ?? Theme.of(context).splashColor, child: Focus(
radius: math.max( focusNode: focusNode,
Material.defaultSplashRadius, child: InkResponse(
(iconSize + math.min(padding.horizontal, padding.vertical)) * 0.7, onTap: onPressed,
// x 0.5 for diameter -> radius and + 40% overflow derived from other Material apps. child: result,
focusColor: focusColor ?? Theme.of(context).focusColor,
hoverColor: hoverColor ?? Theme.of(context).hoverColor,
highlightColor: highlightColor ?? Theme.of(context).highlightColor,
splashColor: splashColor ?? Theme.of(context).splashColor,
radius: math.max(
Material.defaultSplashRadius,
(iconSize + math.min(padding.horizontal, padding.vertical)) * 0.7,
// x 0.5 for diameter -> radius and + 40% overflow derived from other Material apps.
),
),
), ),
); );
} }
...@@ -305,7 +332,15 @@ class IconButton extends StatelessWidget { ...@@ -305,7 +332,15 @@ class IconButton extends StatelessWidget {
void debugFillProperties(DiagnosticPropertiesBuilder properties) { void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties); super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<Widget>('icon', icon, showName: false)); properties.add(DiagnosticsProperty<Widget>('icon', icon, showName: false));
properties.add(ObjectFlagProperty<VoidCallback>('onPressed', onPressed, ifNull: 'disabled'));
properties.add(StringProperty('tooltip', tooltip, defaultValue: null, quoted: false)); properties.add(StringProperty('tooltip', tooltip, defaultValue: null, quoted: false));
properties.add(ObjectFlagProperty<VoidCallback>('onPressed', onPressed, ifNull: 'disabled'));
properties.add(DiagnosticsProperty<Color>('color', color, defaultValue: null));
properties.add(DiagnosticsProperty<Color>('disabledColor', disabledColor, defaultValue: null));
properties.add(DiagnosticsProperty<Color>('focusColor', focusColor, defaultValue: null));
properties.add(DiagnosticsProperty<Color>('hoverColor', hoverColor, defaultValue: null));
properties.add(DiagnosticsProperty<Color>('highlightColor', highlightColor, defaultValue: null));
properties.add(DiagnosticsProperty<Color>('splashColor', splashColor, defaultValue: null));
properties.add(DiagnosticsProperty<EdgeInsetsGeometry>('padding', padding, defaultValue: null));
properties.add(DiagnosticsProperty<FocusNode>('focusNode', focusNode, defaultValue: null));
} }
} }
...@@ -8,7 +8,7 @@ import 'package:flutter/widgets.dart'; ...@@ -8,7 +8,7 @@ import 'package:flutter/widgets.dart';
import 'ink_well.dart' show InteractiveInkFeature; import 'ink_well.dart' show InteractiveInkFeature;
import 'material.dart'; import 'material.dart';
const Duration _kHighlightFadeDuration = Duration(milliseconds: 200); const Duration _kDefaultHighlightFadeDuration = Duration(milliseconds: 200);
/// A visual emphasis on a part of a [Material] receiving user interaction. /// A visual emphasis on a part of a [Material] receiving user interaction.
/// ///
...@@ -45,16 +45,18 @@ class InkHighlight extends InteractiveInkFeature { ...@@ -45,16 +45,18 @@ class InkHighlight extends InteractiveInkFeature {
ShapeBorder customBorder, ShapeBorder customBorder,
RectCallback rectCallback, RectCallback rectCallback,
VoidCallback onRemoved, VoidCallback onRemoved,
Duration fadeDuration = _kDefaultHighlightFadeDuration,
}) : assert(color != null), }) : assert(color != null),
assert(shape != null), assert(shape != null),
assert(textDirection != null), assert(textDirection != null),
assert(fadeDuration != null),
_shape = shape, _shape = shape,
_borderRadius = borderRadius ?? BorderRadius.zero, _borderRadius = borderRadius ?? BorderRadius.zero,
_customBorder = customBorder, _customBorder = customBorder,
_textDirection = textDirection, _textDirection = textDirection,
_rectCallback = rectCallback, _rectCallback = rectCallback,
super(controller: controller, referenceBox: referenceBox, color: color, onRemoved: onRemoved) { super(controller: controller, referenceBox: referenceBox, color: color, onRemoved: onRemoved) {
_alphaController = AnimationController(duration: _kHighlightFadeDuration, vsync: controller.vsync) _alphaController = AnimationController(duration: fadeDuration, vsync: controller.vsync)
..addListener(controller.markNeedsPaint) ..addListener(controller.markNeedsPaint)
..addStatusListener(_handleAlphaStatusChanged) ..addStatusListener(_handleAlphaStatusChanged)
..forward(); ..forward();
......
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:ui';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
...@@ -50,15 +52,20 @@ class MaterialButton extends StatelessWidget { ...@@ -50,15 +52,20 @@ class MaterialButton extends StatelessWidget {
this.disabledTextColor, this.disabledTextColor,
this.color, this.color,
this.disabledColor, this.disabledColor,
this.focusColor,
this.hoverColor,
this.highlightColor, this.highlightColor,
this.splashColor, this.splashColor,
this.colorBrightness, this.colorBrightness,
this.elevation, this.elevation,
this.focusElevation,
this.hoverElevation,
this.highlightElevation, this.highlightElevation,
this.disabledElevation, this.disabledElevation,
this.padding, this.padding,
this.shape, this.shape,
this.clipBehavior = Clip.none, this.clipBehavior = Clip.none,
this.focusNode,
this.materialTapTargetSize, this.materialTapTargetSize,
this.animationDuration, this.animationDuration,
this.minWidth, this.minWidth,
...@@ -145,6 +152,19 @@ class MaterialButton extends StatelessWidget { ...@@ -145,6 +152,19 @@ class MaterialButton extends StatelessWidget {
/// factory, [ThemeData.splashFactory]. /// factory, [ThemeData.splashFactory].
final Color splashColor; final Color splashColor;
/// The fill color of the button's [Material] when it has the input focus.
///
/// The button changed focus color when the button has the input focus. It
/// appears behind the button's child.
final Color focusColor;
/// The fill color of the button's [Material] when a pointer is hovering over
/// it.
///
/// The button changes fill color when a pointer is hovering over the button.
/// It appears behind the button's child.
final Color hoverColor;
/// The highlight color of the button's [InkWell]. /// The highlight color of the button's [InkWell].
/// ///
/// The highlight indicates that the button is actively being pressed. It /// The highlight indicates that the button is actively being pressed. It
...@@ -166,10 +186,40 @@ class MaterialButton extends StatelessWidget { ...@@ -166,10 +186,40 @@ class MaterialButton extends StatelessWidget {
/// See also: /// See also:
/// ///
/// * [FlatButton], a button with no elevation or fill color. /// * [FlatButton], a button with no elevation or fill color.
/// * [focusElevation], the elevation when the button is focused.
/// * [hoverElevation], the elevation when a pointer is hovering over the
/// button.
/// * [disabledElevation], the elevation when the button is disabled. /// * [disabledElevation], the elevation when the button is disabled.
/// * [highlightElevation], the elevation when the button is pressed. /// * [highlightElevation], the elevation when the button is pressed.
final double elevation; final double elevation;
/// The elevation for the button's [Material] when the button
/// is [enabled] and a pointer is hovering over it.
///
/// Defaults to 4.0. The value is always non-negative.
///
/// See also:
///
/// * [elevation], the default elevation.
/// * [focusElevation], the elevation when the button is focused.
/// * [disabledElevation], the elevation when the button is disabled.
/// * [highlightElevation], the elevation when the button is pressed.
final double hoverElevation;
/// The elevation for the button's [Material] when the button
/// is [enabled] and has the input focus.
///
/// Defaults to 4.0. The value is always non-negative.
///
/// See also:
///
/// * [elevation], the default elevation.
/// * [hoverElevation], the elevation when a pointer is hovering over the
/// button.
/// * [disabledElevation], the elevation when the button is disabled.
/// * [highlightElevation], the elevation when the button is pressed.
final double focusElevation;
/// The elevation for the button's [Material] relative to its parent when the /// The elevation for the button's [Material] relative to its parent when the
/// button is [enabled] and pressed. /// button is [enabled] and pressed.
/// ///
...@@ -182,6 +232,9 @@ class MaterialButton extends StatelessWidget { ...@@ -182,6 +232,9 @@ class MaterialButton extends StatelessWidget {
/// See also: /// See also:
/// ///
/// * [elevation], the default elevation. /// * [elevation], the default elevation.
/// * [focusElevation], the elevation when the button is focused.
/// * [hoverElevation], the elevation when a pointer is hovering over the
/// button.
/// * [disabledElevation], the elevation when the button is disabled. /// * [disabledElevation], the elevation when the button is disabled.
final double highlightElevation; final double highlightElevation;
...@@ -231,6 +284,14 @@ class MaterialButton extends StatelessWidget { ...@@ -231,6 +284,14 @@ class MaterialButton extends StatelessWidget {
/// {@macro flutter.widgets.Clip} /// {@macro flutter.widgets.Clip}
final Clip clipBehavior; final Clip clipBehavior;
/// An optional focus node to use for requesting focus when pressed.
///
/// If not supplied, the button will create and host its own [FocusNode].
///
/// If supplied, the given focusNode will be _hosted_ by this widget. See
/// [FocusNode] for more information on what that implies.
final FocusNode focusNode;
/// Defines the duration of animated changes for [shape] and [elevation]. /// Defines the duration of animated changes for [shape] and [elevation].
/// ///
/// The default value is [kThemeChangeDuration]. /// The default value is [kThemeChangeDuration].
...@@ -265,9 +326,13 @@ class MaterialButton extends StatelessWidget { ...@@ -265,9 +326,13 @@ class MaterialButton extends StatelessWidget {
onHighlightChanged: onHighlightChanged, onHighlightChanged: onHighlightChanged,
fillColor: buttonTheme.getFillColor(this), fillColor: buttonTheme.getFillColor(this),
textStyle: theme.textTheme.button.copyWith(color: buttonTheme.getTextColor(this)), textStyle: theme.textTheme.button.copyWith(color: buttonTheme.getTextColor(this)),
focusColor: focusColor ?? buttonTheme.getFocusColor(this) ?? theme.focusColor,
hoverColor: hoverColor ?? buttonTheme.getHoverColor(this) ?? theme.hoverColor,
highlightColor: highlightColor ?? theme.highlightColor, highlightColor: highlightColor ?? theme.highlightColor,
splashColor: splashColor ?? theme.splashColor, splashColor: splashColor ?? theme.splashColor,
elevation: buttonTheme.getElevation(this), elevation: buttonTheme.getElevation(this),
focusElevation: buttonTheme.getFocusElevation(this),
hoverElevation: buttonTheme.getHoverElevation(this),
highlightElevation: buttonTheme.getHighlightElevation(this), highlightElevation: buttonTheme.getHighlightElevation(this),
padding: buttonTheme.getPadding(this), padding: buttonTheme.getPadding(this),
constraints: buttonTheme.getConstraints(this).copyWith( constraints: buttonTheme.getConstraints(this).copyWith(
...@@ -276,6 +341,7 @@ class MaterialButton extends StatelessWidget { ...@@ -276,6 +341,7 @@ class MaterialButton extends StatelessWidget {
), ),
shape: buttonTheme.getShape(this), shape: buttonTheme.getShape(this),
clipBehavior: clipBehavior ?? Clip.none, clipBehavior: clipBehavior ?? Clip.none,
focusNode: focusNode,
animationDuration: buttonTheme.getAnimationDuration(this), animationDuration: buttonTheme.getAnimationDuration(this),
child: child, child: child,
materialTapTargetSize: materialTapTargetSize ?? theme.materialTapTargetSize, materialTapTargetSize: materialTapTargetSize ?? theme.materialTapTargetSize,
...@@ -285,7 +351,21 @@ class MaterialButton extends StatelessWidget { ...@@ -285,7 +351,21 @@ class MaterialButton extends StatelessWidget {
@override @override
void debugFillProperties(DiagnosticPropertiesBuilder properties) { void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties); super.debugFillProperties(properties);
properties.add(FlagProperty('enabled', value: enabled, ifFalse: 'disabled')); properties.add(ObjectFlagProperty<VoidCallback>('onPressed', onPressed, ifNull: 'disabled'));
properties.add(DiagnosticsProperty<ButtonTextTheme>('textTheme', textTheme, defaultValue: null));
properties.add(DiagnosticsProperty<Color>('textColor', textColor, defaultValue: null));
properties.add(DiagnosticsProperty<Color>('disabledTextColor', disabledTextColor, defaultValue: null));
properties.add(DiagnosticsProperty<Color>('color', color, defaultValue: null));
properties.add(DiagnosticsProperty<Color>('disabledColor', disabledColor, defaultValue: null));
properties.add(DiagnosticsProperty<Color>('focusColor', focusColor, defaultValue: null));
properties.add(DiagnosticsProperty<Color>('hoverColor', hoverColor, defaultValue: null));
properties.add(DiagnosticsProperty<Color>('highlightColor', highlightColor, defaultValue: null));
properties.add(DiagnosticsProperty<Color>('splashColor', splashColor, defaultValue: null));
properties.add(DiagnosticsProperty<Brightness>('colorBrightness', colorBrightness, defaultValue: null));
properties.add(DiagnosticsProperty<EdgeInsetsGeometry>('padding', padding, defaultValue: null));
properties.add(DiagnosticsProperty<ShapeBorder>('shape', shape, defaultValue: null));
properties.add(DiagnosticsProperty<FocusNode>('focusNode', focusNode, defaultValue: null));
properties.add(DiagnosticsProperty<MaterialTapTargetSize>('materialTapTargetSize', materialTapTargetSize, defaultValue: null));
} }
} }
......
...@@ -64,6 +64,8 @@ class OutlineButton extends MaterialButton { ...@@ -64,6 +64,8 @@ class OutlineButton extends MaterialButton {
Color textColor, Color textColor,
Color disabledTextColor, Color disabledTextColor,
Color color, Color color,
Color focusColor,
Color hoverColor,
Color highlightColor, Color highlightColor,
Color splashColor, Color splashColor,
double highlightElevation, double highlightElevation,
...@@ -72,7 +74,8 @@ class OutlineButton extends MaterialButton { ...@@ -72,7 +74,8 @@ class OutlineButton extends MaterialButton {
this.highlightedBorderColor, this.highlightedBorderColor,
EdgeInsetsGeometry padding, EdgeInsetsGeometry padding,
ShapeBorder shape, ShapeBorder shape,
Clip clipBehavior = Clip.none, Clip clipBehavior,
FocusNode focusNode,
Widget child, Widget child,
}) : assert(highlightElevation == null || highlightElevation >= 0.0), }) : assert(highlightElevation == null || highlightElevation >= 0.0),
super( super(
...@@ -82,12 +85,15 @@ class OutlineButton extends MaterialButton { ...@@ -82,12 +85,15 @@ class OutlineButton extends MaterialButton {
textColor: textColor, textColor: textColor,
disabledTextColor: disabledTextColor, disabledTextColor: disabledTextColor,
color: color, color: color,
focusColor: focusColor,
hoverColor: hoverColor,
highlightColor: highlightColor, highlightColor: highlightColor,
splashColor: splashColor, splashColor: splashColor,
highlightElevation: highlightElevation, highlightElevation: highlightElevation,
padding: padding, padding: padding,
shape: shape, shape: shape,
clipBehavior: clipBehavior, clipBehavior: clipBehavior,
focusNode: focusNode,
child: child, child: child,
); );
...@@ -106,6 +112,8 @@ class OutlineButton extends MaterialButton { ...@@ -106,6 +112,8 @@ class OutlineButton extends MaterialButton {
Color textColor, Color textColor,
Color disabledTextColor, Color disabledTextColor,
Color color, Color color,
Color focusColor,
Color hoverColor,
Color highlightColor, Color highlightColor,
Color splashColor, Color splashColor,
double highlightElevation, double highlightElevation,
...@@ -115,6 +123,7 @@ class OutlineButton extends MaterialButton { ...@@ -115,6 +123,7 @@ class OutlineButton extends MaterialButton {
EdgeInsetsGeometry padding, EdgeInsetsGeometry padding,
ShapeBorder shape, ShapeBorder shape,
Clip clipBehavior, Clip clipBehavior,
FocusNode focusNode,
@required Widget icon, @required Widget icon,
@required Widget label, @required Widget label,
}) = _OutlineButtonWithIcon; }) = _OutlineButtonWithIcon;
...@@ -151,6 +160,8 @@ class OutlineButton extends MaterialButton { ...@@ -151,6 +160,8 @@ class OutlineButton extends MaterialButton {
textColor: buttonTheme.getTextColor(this), textColor: buttonTheme.getTextColor(this),
disabledTextColor: buttonTheme.getDisabledTextColor(this), disabledTextColor: buttonTheme.getDisabledTextColor(this),
color: color, color: color,
focusColor: buttonTheme.getFocusColor(this),
hoverColor: buttonTheme.getHoverColor(this),
highlightColor: buttonTheme.getHighlightColor(this), highlightColor: buttonTheme.getHighlightColor(this),
splashColor: buttonTheme.getSplashColor(this), splashColor: buttonTheme.getSplashColor(this),
highlightElevation: buttonTheme.getHighlightElevation(this), highlightElevation: buttonTheme.getHighlightElevation(this),
...@@ -160,6 +171,7 @@ class OutlineButton extends MaterialButton { ...@@ -160,6 +171,7 @@ class OutlineButton extends MaterialButton {
padding: buttonTheme.getPadding(this), padding: buttonTheme.getPadding(this),
shape: buttonTheme.getShape(this), shape: buttonTheme.getShape(this),
clipBehavior: clipBehavior, clipBehavior: clipBehavior,
focusNode: focusNode,
child: child, child: child,
); );
} }
...@@ -167,19 +179,9 @@ class OutlineButton extends MaterialButton { ...@@ -167,19 +179,9 @@ class OutlineButton extends MaterialButton {
@override @override
void debugFillProperties(DiagnosticPropertiesBuilder properties) { void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties); super.debugFillProperties(properties);
properties.add(ObjectFlagProperty<VoidCallback>('onPressed', onPressed, ifNull: 'disabled'));
properties.add(DiagnosticsProperty<ButtonTextTheme>('textTheme', textTheme, defaultValue: null));
properties.add(DiagnosticsProperty<Color>('textColor', textColor, defaultValue: null));
properties.add(DiagnosticsProperty<Color>('disabledTextColor', disabledTextColor, defaultValue: null));
properties.add(DiagnosticsProperty<Color>('color', color, defaultValue: null));
properties.add(DiagnosticsProperty<Color>('highlightColor', highlightColor, defaultValue: null));
properties.add(DiagnosticsProperty<Color>('splashColor', splashColor, defaultValue: null));
properties.add(DiagnosticsProperty<double>('highlightElevation', highlightElevation, defaultValue: null));
properties.add(DiagnosticsProperty<BorderSide>('borderSide', borderSide, defaultValue: null)); properties.add(DiagnosticsProperty<BorderSide>('borderSide', borderSide, defaultValue: null));
properties.add(DiagnosticsProperty<Color>('disabledBorderColor', disabledBorderColor, defaultValue: null)); properties.add(DiagnosticsProperty<Color>('disabledBorderColor', disabledBorderColor, defaultValue: null));
properties.add(DiagnosticsProperty<Color>('highlightedBorderColor', highlightedBorderColor, defaultValue: null)); properties.add(DiagnosticsProperty<Color>('highlightedBorderColor', highlightedBorderColor, defaultValue: null));
properties.add(DiagnosticsProperty<EdgeInsetsGeometry>('padding', padding, defaultValue: null));
properties.add(DiagnosticsProperty<ShapeBorder>('shape', shape, defaultValue: null));
} }
} }
...@@ -195,6 +197,8 @@ class _OutlineButtonWithIcon extends OutlineButton with MaterialButtonWithIconMi ...@@ -195,6 +197,8 @@ class _OutlineButtonWithIcon extends OutlineButton with MaterialButtonWithIconMi
Color textColor, Color textColor,
Color disabledTextColor, Color disabledTextColor,
Color color, Color color,
Color focusColor,
Color hoverColor,
Color highlightColor, Color highlightColor,
Color splashColor, Color splashColor,
double highlightElevation, double highlightElevation,
...@@ -204,6 +208,7 @@ class _OutlineButtonWithIcon extends OutlineButton with MaterialButtonWithIconMi ...@@ -204,6 +208,7 @@ class _OutlineButtonWithIcon extends OutlineButton with MaterialButtonWithIconMi
EdgeInsetsGeometry padding, EdgeInsetsGeometry padding,
ShapeBorder shape, ShapeBorder shape,
Clip clipBehavior, Clip clipBehavior,
FocusNode focusNode,
@required Widget icon, @required Widget icon,
@required Widget label, @required Widget label,
}) : assert(highlightElevation == null || highlightElevation >= 0.0), }) : assert(highlightElevation == null || highlightElevation >= 0.0),
...@@ -216,6 +221,8 @@ class _OutlineButtonWithIcon extends OutlineButton with MaterialButtonWithIconMi ...@@ -216,6 +221,8 @@ class _OutlineButtonWithIcon extends OutlineButton with MaterialButtonWithIconMi
textColor: textColor, textColor: textColor,
disabledTextColor: disabledTextColor, disabledTextColor: disabledTextColor,
color: color, color: color,
focusColor: focusColor,
hoverColor: hoverColor,
highlightColor: highlightColor, highlightColor: highlightColor,
splashColor: splashColor, splashColor: splashColor,
highlightElevation: highlightElevation, highlightElevation: highlightElevation,
...@@ -225,6 +232,7 @@ class _OutlineButtonWithIcon extends OutlineButton with MaterialButtonWithIconMi ...@@ -225,6 +232,7 @@ class _OutlineButtonWithIcon extends OutlineButton with MaterialButtonWithIconMi
padding: padding, padding: padding,
shape: shape, shape: shape,
clipBehavior: clipBehavior, clipBehavior: clipBehavior,
focusNode: focusNode,
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: <Widget>[ children: <Widget>[
...@@ -245,6 +253,8 @@ class _OutlineButton extends StatefulWidget { ...@@ -245,6 +253,8 @@ class _OutlineButton extends StatefulWidget {
this.textColor, this.textColor,
this.disabledTextColor, this.disabledTextColor,
this.color, this.color,
this.focusColor,
this.hoverColor,
this.highlightColor, this.highlightColor,
this.splashColor, this.splashColor,
@required this.highlightElevation, @required this.highlightElevation,
...@@ -254,6 +264,7 @@ class _OutlineButton extends StatefulWidget { ...@@ -254,6 +264,7 @@ class _OutlineButton extends StatefulWidget {
this.padding, this.padding,
this.shape, this.shape,
this.clipBehavior, this.clipBehavior,
this.focusNode,
this.child, this.child,
}) : assert(highlightElevation != null && highlightElevation >= 0.0), }) : assert(highlightElevation != null && highlightElevation >= 0.0),
assert(highlightedBorderColor != null), assert(highlightedBorderColor != null),
...@@ -266,6 +277,8 @@ class _OutlineButton extends StatefulWidget { ...@@ -266,6 +277,8 @@ class _OutlineButton extends StatefulWidget {
final Color disabledTextColor; final Color disabledTextColor;
final Color color; final Color color;
final Color splashColor; final Color splashColor;
final Color focusColor;
final Color hoverColor;
final Color highlightColor; final Color highlightColor;
final double highlightElevation; final double highlightElevation;
final BorderSide borderSide; final BorderSide borderSide;
...@@ -274,6 +287,7 @@ class _OutlineButton extends StatefulWidget { ...@@ -274,6 +287,7 @@ class _OutlineButton extends StatefulWidget {
final EdgeInsetsGeometry padding; final EdgeInsetsGeometry padding;
final ShapeBorder shape; final ShapeBorder shape;
final Clip clipBehavior; final Clip clipBehavior;
final FocusNode focusNode;
final Widget child; final Widget child;
bool get enabled => onPressed != null; bool get enabled => onPressed != null;
...@@ -391,11 +405,15 @@ class _OutlineButtonState extends State<_OutlineButton> with SingleTickerProvide ...@@ -391,11 +405,15 @@ class _OutlineButtonState extends State<_OutlineButton> with SingleTickerProvide
disabledTextColor: widget.disabledTextColor, disabledTextColor: widget.disabledTextColor,
color: _getFillColor(), color: _getFillColor(),
splashColor: widget.splashColor, splashColor: widget.splashColor,
focusColor: widget.focusColor,
hoverColor: widget.hoverColor,
highlightColor: widget.highlightColor, highlightColor: widget.highlightColor,
disabledColor: Colors.transparent, disabledColor: Colors.transparent,
onPressed: widget.onPressed, onPressed: widget.onPressed,
elevation: 0.0, elevation: 0.0,
disabledElevation: 0.0, disabledElevation: 0.0,
focusElevation: 0.0,
hoverElevation: 0.0,
highlightElevation: _getHighlightElevation(), highlightElevation: _getHighlightElevation(),
onHighlightChanged: _handleHighlightChanged, onHighlightChanged: _handleHighlightChanged,
padding: widget.padding, padding: widget.padding,
...@@ -404,6 +422,7 @@ class _OutlineButtonState extends State<_OutlineButton> with SingleTickerProvide ...@@ -404,6 +422,7 @@ class _OutlineButtonState extends State<_OutlineButton> with SingleTickerProvide
side: _getOutline(), side: _getOutline(),
), ),
clipBehavior: widget.clipBehavior, clipBehavior: widget.clipBehavior,
focusNode: widget.focusNode,
animationDuration: _kElevationDuration, animationDuration: _kElevationDuration,
child: widget.child, child: widget.child,
); );
......
...@@ -113,19 +113,26 @@ class RaisedButton extends MaterialButton { ...@@ -113,19 +113,26 @@ class RaisedButton extends MaterialButton {
Color disabledTextColor, Color disabledTextColor,
Color color, Color color,
Color disabledColor, Color disabledColor,
Color focusColor,
Color hoverColor,
Color highlightColor, Color highlightColor,
Color splashColor, Color splashColor,
Brightness colorBrightness, Brightness colorBrightness,
double elevation, double elevation,
double focusElevation,
double hoverElevation,
double highlightElevation, double highlightElevation,
double disabledElevation, double disabledElevation,
EdgeInsetsGeometry padding, EdgeInsetsGeometry padding,
ShapeBorder shape, ShapeBorder shape,
Clip clipBehavior = Clip.none, Clip clipBehavior,
FocusNode focusNode,
MaterialTapTargetSize materialTapTargetSize, MaterialTapTargetSize materialTapTargetSize,
Duration animationDuration, Duration animationDuration,
Widget child, Widget child,
}) : assert(elevation == null || elevation >= 0.0), }) : assert(elevation == null || elevation >= 0.0),
assert(focusElevation == null || focusElevation >= 0.0),
assert(hoverElevation == null || hoverElevation >= 0.0),
assert(highlightElevation == null || highlightElevation >= 0.0), assert(highlightElevation == null || highlightElevation >= 0.0),
assert(disabledElevation == null || disabledElevation >= 0.0), assert(disabledElevation == null || disabledElevation >= 0.0),
super( super(
...@@ -137,15 +144,20 @@ class RaisedButton extends MaterialButton { ...@@ -137,15 +144,20 @@ class RaisedButton extends MaterialButton {
disabledTextColor: disabledTextColor, disabledTextColor: disabledTextColor,
color: color, color: color,
disabledColor: disabledColor, disabledColor: disabledColor,
focusColor: focusColor,
hoverColor: hoverColor,
highlightColor: highlightColor, highlightColor: highlightColor,
splashColor: splashColor, splashColor: splashColor,
colorBrightness: colorBrightness, colorBrightness: colorBrightness,
elevation: elevation, elevation: elevation,
focusElevation: focusElevation,
hoverElevation: hoverElevation,
highlightElevation: highlightElevation, highlightElevation: highlightElevation,
disabledElevation: disabledElevation, disabledElevation: disabledElevation,
padding: padding, padding: padding,
shape: shape, shape: shape,
clipBehavior: clipBehavior, clipBehavior: clipBehavior,
focusNode: focusNode,
materialTapTargetSize: materialTapTargetSize, materialTapTargetSize: materialTapTargetSize,
animationDuration: animationDuration, animationDuration: animationDuration,
child: child, child: child,
...@@ -168,6 +180,8 @@ class RaisedButton extends MaterialButton { ...@@ -168,6 +180,8 @@ class RaisedButton extends MaterialButton {
Color disabledTextColor, Color disabledTextColor,
Color color, Color color,
Color disabledColor, Color disabledColor,
Color focusColor,
Color hoverColor,
Color highlightColor, Color highlightColor,
Color splashColor, Color splashColor,
Brightness colorBrightness, Brightness colorBrightness,
...@@ -176,6 +190,7 @@ class RaisedButton extends MaterialButton { ...@@ -176,6 +190,7 @@ class RaisedButton extends MaterialButton {
double disabledElevation, double disabledElevation,
ShapeBorder shape, ShapeBorder shape,
Clip clipBehavior, Clip clipBehavior,
FocusNode focusNode,
MaterialTapTargetSize materialTapTargetSize, MaterialTapTargetSize materialTapTargetSize,
Duration animationDuration, Duration animationDuration,
@required Widget icon, @required Widget icon,
...@@ -192,14 +207,19 @@ class RaisedButton extends MaterialButton { ...@@ -192,14 +207,19 @@ class RaisedButton extends MaterialButton {
clipBehavior: clipBehavior ?? Clip.none, clipBehavior: clipBehavior ?? Clip.none,
fillColor: buttonTheme.getFillColor(this), fillColor: buttonTheme.getFillColor(this),
textStyle: theme.textTheme.button.copyWith(color: buttonTheme.getTextColor(this)), textStyle: theme.textTheme.button.copyWith(color: buttonTheme.getTextColor(this)),
focusColor: buttonTheme.getFocusColor(this),
hoverColor: buttonTheme.getHoverColor(this),
highlightColor: buttonTheme.getHighlightColor(this), highlightColor: buttonTheme.getHighlightColor(this),
splashColor: buttonTheme.getSplashColor(this), splashColor: buttonTheme.getSplashColor(this),
elevation: buttonTheme.getElevation(this), elevation: buttonTheme.getElevation(this),
focusElevation: buttonTheme.getFocusElevation(this),
hoverElevation: buttonTheme.getHoverElevation(this),
highlightElevation: buttonTheme.getHighlightElevation(this), highlightElevation: buttonTheme.getHighlightElevation(this),
disabledElevation: buttonTheme.getDisabledElevation(this), disabledElevation: buttonTheme.getDisabledElevation(this),
padding: buttonTheme.getPadding(this), padding: buttonTheme.getPadding(this),
constraints: buttonTheme.getConstraints(this), constraints: buttonTheme.getConstraints(this),
shape: buttonTheme.getShape(this), shape: buttonTheme.getShape(this),
focusNode: focusNode,
animationDuration: buttonTheme.getAnimationDuration(this), animationDuration: buttonTheme.getAnimationDuration(this),
materialTapTargetSize: buttonTheme.getMaterialTapTargetSize(this), materialTapTargetSize: buttonTheme.getMaterialTapTargetSize(this),
child: child, child: child,
...@@ -209,19 +229,11 @@ class RaisedButton extends MaterialButton { ...@@ -209,19 +229,11 @@ class RaisedButton extends MaterialButton {
@override @override
void debugFillProperties(DiagnosticPropertiesBuilder properties) { void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties); super.debugFillProperties(properties);
properties.add(ObjectFlagProperty<VoidCallback>('onPressed', onPressed, ifNull: 'disabled'));
properties.add(DiagnosticsProperty<Color>('textColor', textColor, defaultValue: null));
properties.add(DiagnosticsProperty<Color>('disabledTextColor', disabledTextColor, defaultValue: null));
properties.add(DiagnosticsProperty<Color>('color', color, defaultValue: null));
properties.add(DiagnosticsProperty<Color>('disabledColor', disabledColor, defaultValue: null));
properties.add(DiagnosticsProperty<Color>('highlightColor', highlightColor, defaultValue: null));
properties.add(DiagnosticsProperty<Color>('splashColor', splashColor, defaultValue: null));
properties.add(DiagnosticsProperty<Brightness>('colorBrightness', colorBrightness, defaultValue: null));
properties.add(DiagnosticsProperty<double>('elevation', elevation, defaultValue: null)); properties.add(DiagnosticsProperty<double>('elevation', elevation, defaultValue: null));
properties.add(DiagnosticsProperty<double>('focusElevation', focusElevation, defaultValue: null));
properties.add(DiagnosticsProperty<double>('hoverElevation', hoverElevation, defaultValue: null));
properties.add(DiagnosticsProperty<double>('highlightElevation', highlightElevation, defaultValue: null)); properties.add(DiagnosticsProperty<double>('highlightElevation', highlightElevation, defaultValue: null));
properties.add(DiagnosticsProperty<double>('disabledElevation', disabledElevation, defaultValue: null)); properties.add(DiagnosticsProperty<double>('disabledElevation', disabledElevation, defaultValue: null));
properties.add(DiagnosticsProperty<EdgeInsetsGeometry>('padding', padding, defaultValue: null));
properties.add(DiagnosticsProperty<ShapeBorder>('shape', shape, defaultValue: null));
} }
} }
...@@ -239,6 +251,8 @@ class _RaisedButtonWithIcon extends RaisedButton with MaterialButtonWithIconMixi ...@@ -239,6 +251,8 @@ class _RaisedButtonWithIcon extends RaisedButton with MaterialButtonWithIconMixi
Color disabledTextColor, Color disabledTextColor,
Color color, Color color,
Color disabledColor, Color disabledColor,
Color focusColor,
Color hoverColor,
Color highlightColor, Color highlightColor,
Color splashColor, Color splashColor,
Brightness colorBrightness, Brightness colorBrightness,
...@@ -247,6 +261,7 @@ class _RaisedButtonWithIcon extends RaisedButton with MaterialButtonWithIconMixi ...@@ -247,6 +261,7 @@ class _RaisedButtonWithIcon extends RaisedButton with MaterialButtonWithIconMixi
double disabledElevation, double disabledElevation,
ShapeBorder shape, ShapeBorder shape,
Clip clipBehavior = Clip.none, Clip clipBehavior = Clip.none,
FocusNode focusNode,
MaterialTapTargetSize materialTapTargetSize, MaterialTapTargetSize materialTapTargetSize,
Duration animationDuration, Duration animationDuration,
@required Widget icon, @required Widget icon,
...@@ -265,6 +280,8 @@ class _RaisedButtonWithIcon extends RaisedButton with MaterialButtonWithIconMixi ...@@ -265,6 +280,8 @@ class _RaisedButtonWithIcon extends RaisedButton with MaterialButtonWithIconMixi
disabledTextColor: disabledTextColor, disabledTextColor: disabledTextColor,
color: color, color: color,
disabledColor: disabledColor, disabledColor: disabledColor,
focusColor: focusColor,
hoverColor: hoverColor,
highlightColor: highlightColor, highlightColor: highlightColor,
splashColor: splashColor, splashColor: splashColor,
colorBrightness: colorBrightness, colorBrightness: colorBrightness,
...@@ -273,6 +290,7 @@ class _RaisedButtonWithIcon extends RaisedButton with MaterialButtonWithIconMixi ...@@ -273,6 +290,7 @@ class _RaisedButtonWithIcon extends RaisedButton with MaterialButtonWithIconMixi
disabledElevation: disabledElevation, disabledElevation: disabledElevation,
shape: shape, shape: shape,
clipBehavior: clipBehavior, clipBehavior: clipBehavior,
focusNode: focusNode,
materialTapTargetSize: materialTapTargetSize, materialTapTargetSize: materialTapTargetSize,
animationDuration: animationDuration, animationDuration: animationDuration,
child: Row( child: Row(
......
...@@ -355,7 +355,7 @@ class _SearchPageState<T> extends State<_SearchPage<T>> { ...@@ -355,7 +355,7 @@ class _SearchPageState<T> extends State<_SearchPage<T>> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
queryTextController.addListener(_onQueryChanged); widget.delegate._queryTextController.addListener(_onQueryChanged);
widget.animation.addStatusListener(_onAnimationStatusChanged); widget.animation.addStatusListener(_onAnimationStatusChanged);
widget.delegate._currentBodyNotifier.addListener(_onSearchBodyChanged); widget.delegate._currentBodyNotifier.addListener(_onSearchBodyChanged);
focusNode.addListener(_onFocusChanged); focusNode.addListener(_onFocusChanged);
...@@ -365,7 +365,7 @@ class _SearchPageState<T> extends State<_SearchPage<T>> { ...@@ -365,7 +365,7 @@ class _SearchPageState<T> extends State<_SearchPage<T>> {
@override @override
void dispose() { void dispose() {
super.dispose(); super.dispose();
queryTextController.removeListener(_onQueryChanged); widget.delegate._queryTextController.removeListener(_onQueryChanged);
widget.animation.removeStatusListener(_onAnimationStatusChanged); widget.animation.removeStatusListener(_onAnimationStatusChanged);
widget.delegate._currentBodyNotifier.removeListener(_onSearchBodyChanged); widget.delegate._currentBodyNotifier.removeListener(_onSearchBodyChanged);
widget.delegate._focusNode = null; widget.delegate._focusNode = null;
...@@ -382,6 +382,19 @@ class _SearchPageState<T> extends State<_SearchPage<T>> { ...@@ -382,6 +382,19 @@ class _SearchPageState<T> extends State<_SearchPage<T>> {
} }
} }
@override
void didUpdateWidget(_SearchPage<T> oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.delegate != oldWidget.delegate) {
oldWidget.delegate._queryTextController.removeListener(_onQueryChanged);
widget.delegate._queryTextController.addListener(_onQueryChanged);
oldWidget.delegate._currentBodyNotifier.removeListener(_onSearchBodyChanged);
widget.delegate._currentBodyNotifier.addListener(_onSearchBodyChanged);
oldWidget.delegate._focusNode = null;
widget.delegate._focusNode = focusNode;
}
}
void _onFocusChanged() { void _onFocusChanged() {
if (focusNode.hasFocus && widget.delegate._currentBody != _SearchBody.suggestions) { if (focusNode.hasFocus && widget.delegate._currentBody != _SearchBody.suggestions) {
widget.delegate.showSuggestions(context); widget.delegate.showSuggestions(context);
...@@ -443,7 +456,7 @@ class _SearchPageState<T> extends State<_SearchPage<T>> { ...@@ -443,7 +456,7 @@ class _SearchPageState<T> extends State<_SearchPage<T>> {
brightness: theme.primaryColorBrightness, brightness: theme.primaryColorBrightness,
leading: widget.delegate.buildLeading(context), leading: widget.delegate.buildLeading(context),
title: TextField( title: TextField(
controller: queryTextController, controller: widget.delegate._queryTextController,
focusNode: focusNode, focusNode: focusNode,
style: theme.textTheme.title, style: theme.textTheme.title,
textInputAction: TextInputAction.search, textInputAction: TextInputAction.search,
...@@ -464,6 +477,4 @@ class _SearchPageState<T> extends State<_SearchPage<T>> { ...@@ -464,6 +477,4 @@ class _SearchPageState<T> extends State<_SearchPage<T>> {
), ),
); );
} }
TextEditingController get queryTextController => widget.delegate._queryTextController;
} }
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:ui' show Color, hashValues; import 'dart:ui' show Color, hashList;
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
...@@ -125,6 +125,8 @@ class ThemeData extends Diagnosticable { ...@@ -125,6 +125,8 @@ class ThemeData extends Diagnosticable {
Color bottomAppBarColor, Color bottomAppBarColor,
Color cardColor, Color cardColor,
Color dividerColor, Color dividerColor,
Color focusColor,
Color hoverColor,
Color highlightColor, Color highlightColor,
Color splashColor, Color splashColor,
InteractiveInkFeatureFactory splashFactory, InteractiveInkFeatureFactory splashFactory,
...@@ -204,7 +206,7 @@ class ThemeData extends Diagnosticable { ...@@ -204,7 +206,7 @@ class ThemeData extends Diagnosticable {
// Spec doesn't specify a dark theme secondaryHeaderColor, this is a guess. // Spec doesn't specify a dark theme secondaryHeaderColor, this is a guess.
secondaryHeaderColor ??= isDark ? Colors.grey[700] : primarySwatch[50]; secondaryHeaderColor ??= isDark ? Colors.grey[700] : primarySwatch[50];
textSelectionColor ??= isDark ? accentColor : primarySwatch[200]; textSelectionColor ??= isDark ? accentColor : primarySwatch[200];
// todo (sandrasandeep): change to color provided by Material Design team // TODO(sandrasandeep): change to color provided by Material Design team
cursorColor = cursorColor ?? const Color.fromRGBO(66, 133, 244, 1.0); cursorColor = cursorColor ?? const Color.fromRGBO(66, 133, 244, 1.0);
textSelectionHandleColor ??= isDark ? Colors.tealAccent[400] : primarySwatch[300]; textSelectionHandleColor ??= isDark ? Colors.tealAccent[400] : primarySwatch[300];
backgroundColor ??= isDark ? Colors.grey[700] : primarySwatch[200]; backgroundColor ??= isDark ? Colors.grey[700] : primarySwatch[200];
...@@ -235,10 +237,14 @@ class ThemeData extends Diagnosticable { ...@@ -235,10 +237,14 @@ class ThemeData extends Diagnosticable {
// Used as the default color (fill color) for RaisedButtons. Computing the // Used as the default color (fill color) for RaisedButtons. Computing the
// default for ButtonThemeData for the sake of backwards compatibility. // default for ButtonThemeData for the sake of backwards compatibility.
buttonColor ??= isDark ? primarySwatch[600] : Colors.grey[300]; buttonColor ??= isDark ? primarySwatch[600] : Colors.grey[300];
focusColor ??= buttonColor;
hoverColor ??= buttonColor;
buttonTheme ??= ButtonThemeData( buttonTheme ??= ButtonThemeData(
colorScheme: colorScheme, colorScheme: colorScheme,
buttonColor: buttonColor, buttonColor: buttonColor,
disabledColor: disabledColor, disabledColor: disabledColor,
focusColor: focusColor,
hoverColor: hoverColor,
highlightColor: highlightColor, highlightColor: highlightColor,
splashColor: splashColor, splashColor: splashColor,
materialTapTargetSize: materialTapTargetSize, materialTapTargetSize: materialTapTargetSize,
...@@ -276,6 +282,8 @@ class ThemeData extends Diagnosticable { ...@@ -276,6 +282,8 @@ class ThemeData extends Diagnosticable {
bottomAppBarColor: bottomAppBarColor, bottomAppBarColor: bottomAppBarColor,
cardColor: cardColor, cardColor: cardColor,
dividerColor: dividerColor, dividerColor: dividerColor,
focusColor: focusColor,
hoverColor: hoverColor,
highlightColor: highlightColor, highlightColor: highlightColor,
splashColor: splashColor, splashColor: splashColor,
splashFactory: splashFactory, splashFactory: splashFactory,
...@@ -343,6 +351,8 @@ class ThemeData extends Diagnosticable { ...@@ -343,6 +351,8 @@ class ThemeData extends Diagnosticable {
@required this.bottomAppBarColor, @required this.bottomAppBarColor,
@required this.cardColor, @required this.cardColor,
@required this.dividerColor, @required this.dividerColor,
@required this.focusColor,
@required this.hoverColor,
@required this.highlightColor, @required this.highlightColor,
@required this.splashColor, @required this.splashColor,
@required this.splashFactory, @required this.splashFactory,
...@@ -396,6 +406,8 @@ class ThemeData extends Diagnosticable { ...@@ -396,6 +406,8 @@ class ThemeData extends Diagnosticable {
assert(bottomAppBarColor != null), assert(bottomAppBarColor != null),
assert(cardColor != null), assert(cardColor != null),
assert(dividerColor != null), assert(dividerColor != null),
assert(focusColor != null),
assert(hoverColor != null),
assert(highlightColor != null), assert(highlightColor != null),
assert(splashColor != null), assert(splashColor != null),
assert(splashFactory != null), assert(splashFactory != null),
...@@ -530,6 +542,13 @@ class ThemeData extends Diagnosticable { ...@@ -530,6 +542,13 @@ class ThemeData extends Diagnosticable {
/// [Divider.createBorderSide]. /// [Divider.createBorderSide].
final Color dividerColor; final Color dividerColor;
/// The focus color used indicate that a component has the input focus.
final Color focusColor;
/// The hover color used to indicate when a pointer is hovering over a
/// component.
final Color hoverColor;
/// The highlight color used during ink splash animations or to /// The highlight color used during ink splash animations or to
/// indicate an item in a menu is selected. /// indicate an item in a menu is selected.
final Color highlightColor; final Color highlightColor;
...@@ -727,6 +746,8 @@ class ThemeData extends Diagnosticable { ...@@ -727,6 +746,8 @@ class ThemeData extends Diagnosticable {
Color bottomAppBarColor, Color bottomAppBarColor,
Color cardColor, Color cardColor,
Color dividerColor, Color dividerColor,
Color focusColor,
Color hoverColor,
Color highlightColor, Color highlightColor,
Color splashColor, Color splashColor,
InteractiveInkFeatureFactory splashFactory, InteractiveInkFeatureFactory splashFactory,
...@@ -783,6 +804,8 @@ class ThemeData extends Diagnosticable { ...@@ -783,6 +804,8 @@ class ThemeData extends Diagnosticable {
bottomAppBarColor: bottomAppBarColor ?? this.bottomAppBarColor, bottomAppBarColor: bottomAppBarColor ?? this.bottomAppBarColor,
cardColor: cardColor ?? this.cardColor, cardColor: cardColor ?? this.cardColor,
dividerColor: dividerColor ?? this.dividerColor, dividerColor: dividerColor ?? this.dividerColor,
focusColor: focusColor ?? this.focusColor,
hoverColor: hoverColor ?? this.hoverColor,
highlightColor: highlightColor ?? this.highlightColor, highlightColor: highlightColor ?? this.highlightColor,
splashColor: splashColor ?? this.splashColor, splashColor: splashColor ?? this.splashColor,
splashFactory: splashFactory ?? this.splashFactory, splashFactory: splashFactory ?? this.splashFactory,
...@@ -917,6 +940,8 @@ class ThemeData extends Diagnosticable { ...@@ -917,6 +940,8 @@ class ThemeData extends Diagnosticable {
bottomAppBarColor: Color.lerp(a.bottomAppBarColor, b.bottomAppBarColor, t), bottomAppBarColor: Color.lerp(a.bottomAppBarColor, b.bottomAppBarColor, t),
cardColor: Color.lerp(a.cardColor, b.cardColor, t), cardColor: Color.lerp(a.cardColor, b.cardColor, t),
dividerColor: Color.lerp(a.dividerColor, b.dividerColor, t), dividerColor: Color.lerp(a.dividerColor, b.dividerColor, t),
focusColor: Color.lerp(a.focusColor, b.focusColor, t),
hoverColor: Color.lerp(a.hoverColor, b.hoverColor, t),
highlightColor: Color.lerp(a.highlightColor, b.highlightColor, t), highlightColor: Color.lerp(a.highlightColor, b.highlightColor, t),
splashColor: Color.lerp(a.splashColor, b.splashColor, t), splashColor: Color.lerp(a.splashColor, b.splashColor, t),
splashFactory: t < 0.5 ? a.splashFactory : b.splashFactory, splashFactory: t < 0.5 ? a.splashFactory : b.splashFactory,
...@@ -1026,11 +1051,10 @@ class ThemeData extends Diagnosticable { ...@@ -1026,11 +1051,10 @@ class ThemeData extends Diagnosticable {
@override @override
int get hashCode { int get hashCode {
// The hashValues() function supports up to 20 arguments. // Warning: For the sanity of the reader, please make sure these properties
return hashValues( // are in the exact same order as in operator == and in the raw constructor
// Warning: make sure these properties are in the exact same order as in // and in the order of fields in the class and in the lerp() method.
// operator == and in the raw constructor and in the order of fields in final List<Object> values = <Object>[
// the class and in the lerp() method.
brightness, brightness,
primaryColor, primaryColor,
primaryColorBrightness, primaryColorBrightness,
...@@ -1043,6 +1067,8 @@ class ThemeData extends Diagnosticable { ...@@ -1043,6 +1067,8 @@ class ThemeData extends Diagnosticable {
bottomAppBarColor, bottomAppBarColor,
cardColor, cardColor,
dividerColor, dividerColor,
focusColor,
hoverColor,
highlightColor, highlightColor,
splashColor, splashColor,
splashFactory, splashFactory,
...@@ -1050,45 +1076,42 @@ class ThemeData extends Diagnosticable { ...@@ -1050,45 +1076,42 @@ class ThemeData extends Diagnosticable {
unselectedWidgetColor, unselectedWidgetColor,
disabledColor, disabledColor,
buttonTheme, buttonTheme,
hashValues( buttonColor,
buttonColor, toggleableActiveColor,
toggleableActiveColor, secondaryHeaderColor,
secondaryHeaderColor, textSelectionColor,
textSelectionColor, cursorColor,
cursorColor, textSelectionHandleColor,
textSelectionHandleColor, backgroundColor,
backgroundColor, dialogBackgroundColor,
dialogBackgroundColor, indicatorColor,
indicatorColor, hintColor,
hintColor, errorColor,
errorColor, textTheme,
textTheme, primaryTextTheme,
primaryTextTheme, accentTextTheme,
accentTextTheme, inputDecorationTheme,
inputDecorationTheme, iconTheme,
iconTheme, primaryIconTheme,
primaryIconTheme, accentIconTheme,
accentIconTheme, sliderTheme,
sliderTheme, tabBarTheme,
hashValues( cardTheme,
tabBarTheme, chipTheme,
cardTheme, platform,
chipTheme, materialTapTargetSize,
platform, pageTransitionsTheme,
materialTapTargetSize, appBarTheme,
pageTransitionsTheme, bottomAppBarTheme,
appBarTheme, colorScheme,
bottomAppBarTheme, dialogTheme,
colorScheme, floatingActionButtonTheme,
dialogTheme, typography,
floatingActionButtonTheme, cupertinoOverrideTheme,
typography, snackBarTheme,
cupertinoOverrideTheme, bottomSheetTheme,
snackBarTheme, ];
bottomSheetTheme, return hashList(values);
),
),
);
} }
@override @override
...@@ -1106,6 +1129,8 @@ class ThemeData extends Diagnosticable { ...@@ -1106,6 +1129,8 @@ class ThemeData extends Diagnosticable {
properties.add(DiagnosticsProperty<Color>('bottomAppBarColor', bottomAppBarColor, defaultValue: defaultData.bottomAppBarColor)); properties.add(DiagnosticsProperty<Color>('bottomAppBarColor', bottomAppBarColor, defaultValue: defaultData.bottomAppBarColor));
properties.add(DiagnosticsProperty<Color>('cardColor', cardColor, defaultValue: defaultData.cardColor)); properties.add(DiagnosticsProperty<Color>('cardColor', cardColor, defaultValue: defaultData.cardColor));
properties.add(DiagnosticsProperty<Color>('dividerColor', dividerColor, defaultValue: defaultData.dividerColor)); properties.add(DiagnosticsProperty<Color>('dividerColor', dividerColor, defaultValue: defaultData.dividerColor));
properties.add(DiagnosticsProperty<Color>('focusColor', focusColor, defaultValue: defaultData.focusColor));
properties.add(DiagnosticsProperty<Color>('hoverColor', hoverColor, defaultValue: defaultData.hoverColor));
properties.add(DiagnosticsProperty<Color>('highlightColor', highlightColor, defaultValue: defaultData.highlightColor)); properties.add(DiagnosticsProperty<Color>('highlightColor', highlightColor, defaultValue: defaultData.highlightColor));
properties.add(DiagnosticsProperty<Color>('splashColor', splashColor, defaultValue: defaultData.splashColor)); properties.add(DiagnosticsProperty<Color>('splashColor', splashColor, defaultValue: defaultData.splashColor));
properties.add(DiagnosticsProperty<Color>('selectedRowColor', selectedRowColor, defaultValue: defaultData.selectedRowColor)); properties.add(DiagnosticsProperty<Color>('selectedRowColor', selectedRowColor, defaultValue: defaultData.selectedRowColor));
......
...@@ -340,11 +340,21 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier { ...@@ -340,11 +340,21 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
FocusNode({ FocusNode({
String debugLabel, String debugLabel,
FocusOnKeyCallback onKey, FocusOnKeyCallback onKey,
}) : _onKey = onKey { this.skipTraversal = false,
}) : assert(skipTraversal != null),
_onKey = onKey {
// Set it via the setter so that it does nothing on release builds. // Set it via the setter so that it does nothing on release builds.
this.debugLabel = debugLabel; this.debugLabel = debugLabel;
} }
/// If true, tells the focus traversal policy to skip over this node for
/// purposes of the traversal algorithm.
///
/// This may be used to place nodes in the focus tree that may be focused, but
/// not traversed, allowing them to receive key events as part of the focus
/// chain, but not be traversed to via focus traversal.
bool skipTraversal;
/// The context that was supplied to [attach]. /// The context that was supplied to [attach].
/// ///
/// This is typically the context for the widget that is being focused, as it /// This is typically the context for the widget that is being focused, as it
...@@ -374,6 +384,10 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier { ...@@ -374,6 +384,10 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
Iterable<FocusNode> get children => _children; Iterable<FocusNode> get children => _children;
final List<FocusNode> _children = <FocusNode>[]; final List<FocusNode> _children = <FocusNode>[];
/// An iterator over the children that are allowed to be traversed by the
/// [FocusTraversalPolicy].
Iterable<FocusNode> get traversalChildren => children.where((FocusNode node) => !node.skipTraversal);
/// A debug label that is used for diagnostic output. /// A debug label that is used for diagnostic output.
/// ///
/// Will always return null in release builds. /// Will always return null in release builds.
...@@ -398,6 +412,9 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier { ...@@ -398,6 +412,9 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
} }
} }
/// Returns all descendants which do not have the [skipTraversal] flag set.
Iterable<FocusNode> get traversalDescendants => descendants.where((FocusNode node) => !node.skipTraversal);
/// An [Iterable] over the ancestors of this node. /// An [Iterable] over the ancestors of this node.
/// ///
/// Iterates the ancestors of this node starting at the parent and iterating /// Iterates the ancestors of this node starting at the parent and iterating
......
...@@ -93,13 +93,9 @@ import 'inherited_notifier.dart'; ...@@ -93,13 +93,9 @@ import 'inherited_notifier.dart';
/// return GestureDetector( /// return GestureDetector(
/// onTap: () { /// onTap: () {
/// if (hasFocus) { /// if (hasFocus) {
/// setState(() { /// focusNode.unfocus();
/// focusNode.unfocus();
/// });
/// } else { /// } else {
/// setState(() { /// focusNode.requestFocus();
/// focusNode.requestFocus();
/// });
/// } /// }
/// }, /// },
/// child: Center( /// child: Center(
...@@ -150,8 +146,10 @@ class Focus extends StatefulWidget { ...@@ -150,8 +146,10 @@ class Focus extends StatefulWidget {
this.onFocusChange, this.onFocusChange,
this.onKey, this.onKey,
this.debugLabel, this.debugLabel,
this.skipTraversal = false,
}) : assert(child != null), }) : assert(child != null),
assert(autofocus != null), assert(autofocus != null),
assert(skipTraversal != null),
super(key: key); super(key: key);
/// A debug label for this widget. /// A debug label for this widget.
...@@ -212,6 +210,13 @@ class Focus extends StatefulWidget { ...@@ -212,6 +210,13 @@ class Focus extends StatefulWidget {
/// node when needed. /// node when needed.
final FocusNode focusNode; final FocusNode focusNode;
/// Sets the [FocusNode.skipTraversal] flag on the focus node so that it won't
/// be visited by the [FocusTraversalPolicy].
///
/// This is sometimes useful if a Focus widget should receive key events as
/// part of the focus chain, but shouldn't be accessible via focus traversal.
final bool skipTraversal;
/// Returns the [focusNode] of the [Focus] that most tightly encloses the given /// Returns the [focusNode] of the [Focus] that most tightly encloses the given
/// [BuildContext]. /// [BuildContext].
/// ///
...@@ -238,7 +243,7 @@ class Focus extends StatefulWidget { ...@@ -238,7 +243,7 @@ class Focus extends StatefulWidget {
super.debugFillProperties(properties); super.debugFillProperties(properties);
properties.add(StringProperty('debugLabel', debugLabel, defaultValue: null)); properties.add(StringProperty('debugLabel', debugLabel, defaultValue: null));
properties.add(FlagProperty('autofocus', value: autofocus, ifTrue: 'AUTOFOCUS', defaultValue: false)); properties.add(FlagProperty('autofocus', value: autofocus, ifTrue: 'AUTOFOCUS', defaultValue: false));
properties.add(DiagnosticsProperty<FocusScopeNode>('node', focusNode, defaultValue: null)); properties.add(DiagnosticsProperty<FocusNode>('node', focusNode, defaultValue: null));
} }
@override @override
...@@ -263,6 +268,7 @@ class _FocusState extends State<Focus> { ...@@ -263,6 +268,7 @@ class _FocusState extends State<Focus> {
// Only create a new node if the widget doesn't have one. // Only create a new node if the widget doesn't have one.
_internalNode ??= _createNode(); _internalNode ??= _createNode();
} }
node.skipTraversal = widget.skipTraversal;
_focusAttachment = node.attach(context, onKey: widget.onKey); _focusAttachment = node.attach(context, onKey: widget.onKey);
_hasFocus = node.hasFocus; _hasFocus = node.hasFocus;
// Add listener even if the _internalNode existed before, since it should // Add listener even if the _internalNode existed before, since it should
......
...@@ -227,7 +227,7 @@ mixin DirectionalFocusTraversalPolicyMixin on FocusTraversalPolicy { ...@@ -227,7 +227,7 @@ mixin DirectionalFocusTraversalPolicyMixin on FocusTraversalPolicy {
} }
FocusNode _sortAndFindInitial(FocusNode currentNode, { bool vertical, bool first }) { FocusNode _sortAndFindInitial(FocusNode currentNode, { bool vertical, bool first }) {
final Iterable<FocusNode> nodes = currentNode.nearestScope.descendants; final Iterable<FocusNode> nodes = currentNode.nearestScope.traversalDescendants;
final List<FocusNode> sorted = nodes.toList(); final List<FocusNode> sorted = nodes.toList();
sorted.sort((FocusNode a, FocusNode b) { sorted.sort((FocusNode a, FocusNode b) {
if (vertical) { if (vertical) {
...@@ -261,7 +261,7 @@ mixin DirectionalFocusTraversalPolicyMixin on FocusTraversalPolicy { ...@@ -261,7 +261,7 @@ mixin DirectionalFocusTraversalPolicyMixin on FocusTraversalPolicy {
FocusNode nearestScope, FocusNode nearestScope,
) { ) {
assert(direction == TraversalDirection.left || direction == TraversalDirection.right); assert(direction == TraversalDirection.left || direction == TraversalDirection.right);
final Iterable<FocusNode> nodes = nearestScope.descendants; final Iterable<FocusNode> nodes = nearestScope.traversalDescendants;
assert(!nodes.contains(nearestScope)); assert(!nodes.contains(nearestScope));
final List<FocusNode> sorted = nodes.toList(); final List<FocusNode> sorted = nodes.toList();
sorted.sort((FocusNode a, FocusNode b) => a.rect.center.dx.compareTo(b.rect.center.dx)); sorted.sort((FocusNode a, FocusNode b) => a.rect.center.dx.compareTo(b.rect.center.dx));
...@@ -397,7 +397,7 @@ mixin DirectionalFocusTraversalPolicyMixin on FocusTraversalPolicy { ...@@ -397,7 +397,7 @@ mixin DirectionalFocusTraversalPolicyMixin on FocusTraversalPolicy {
final Iterable<FocusNode> eligibleNodes = _sortAndFilterVertically( final Iterable<FocusNode> eligibleNodes = _sortAndFilterVertically(
direction, direction,
focusedChild.rect, focusedChild.rect,
nearestScope.descendants, nearestScope.traversalDescendants,
); );
if (eligibleNodes.isEmpty) { if (eligibleNodes.isEmpty) {
break; break;
...@@ -434,7 +434,7 @@ mixin DirectionalFocusTraversalPolicyMixin on FocusTraversalPolicy { ...@@ -434,7 +434,7 @@ mixin DirectionalFocusTraversalPolicyMixin on FocusTraversalPolicy {
final Rect band = Rect.fromLTRB(-double.infinity, focusedChild.rect.top, double.infinity, focusedChild.rect.bottom); final Rect band = Rect.fromLTRB(-double.infinity, focusedChild.rect.top, double.infinity, focusedChild.rect.bottom);
final Iterable<FocusNode> inBand = sorted.where((FocusNode node) => !node.rect.intersect(band).isEmpty); final Iterable<FocusNode> inBand = sorted.where((FocusNode node) => !node.rect.intersect(band).isEmpty);
if (inBand.isNotEmpty) { if (inBand.isNotEmpty) {
// The inBand list is already sorted by horizontal distance, so pick the closest one. // The inBand list is already sorted by vertical distance, so pick the closest one.
found = inBand.first; found = inBand.first;
break; break;
} }
...@@ -483,8 +483,8 @@ class WidgetOrderFocusTraversalPolicy extends FocusTraversalPolicy with Directio ...@@ -483,8 +483,8 @@ class WidgetOrderFocusTraversalPolicy extends FocusTraversalPolicy with Directio
// doesn't have a focusedChild, or a non-scope is encountered. // doesn't have a focusedChild, or a non-scope is encountered.
FocusNode candidate = scope.focusedChild; FocusNode candidate = scope.focusedChild;
if (candidate == null) { if (candidate == null) {
if (scope.children.isNotEmpty) { if (scope.traversalChildren.isNotEmpty) {
candidate = scope.children.first; candidate = scope.traversalChildren.first;
} else { } else {
candidate = currentNode; candidate = currentNode;
} }
...@@ -513,7 +513,7 @@ class WidgetOrderFocusTraversalPolicy extends FocusTraversalPolicy with Directio ...@@ -513,7 +513,7 @@ class WidgetOrderFocusTraversalPolicy extends FocusTraversalPolicy with Directio
FocusNode firstNode; FocusNode firstNode;
FocusNode lastNode; FocusNode lastNode;
bool visit(FocusNode node) { bool visit(FocusNode node) {
for (FocusNode visited in node.children) { for (FocusNode visited in node.traversalChildren) {
firstNode ??= visited; firstNode ??= visited;
if (!visit(visited)) { if (!visit(visited)) {
return false; return false;
...@@ -600,7 +600,7 @@ class ReadingOrderTraversalPolicy extends FocusTraversalPolicy with DirectionalF ...@@ -600,7 +600,7 @@ class ReadingOrderTraversalPolicy extends FocusTraversalPolicy with DirectionalF
// doesn't have a focusedChild, or a non-scope is encountered. // doesn't have a focusedChild, or a non-scope is encountered.
FocusNode candidate = scope.focusedChild; FocusNode candidate = scope.focusedChild;
while (candidate == null) { while (candidate == null) {
if (candidate.nearestScope.children.isNotEmpty) { if (candidate.nearestScope.traversalChildren.isNotEmpty) {
candidate = _sortByGeometry(scope).first; candidate = _sortByGeometry(scope).first;
} }
if (candidate is FocusScopeNode) { if (candidate is FocusScopeNode) {
...@@ -611,7 +611,7 @@ class ReadingOrderTraversalPolicy extends FocusTraversalPolicy with DirectionalF ...@@ -611,7 +611,7 @@ class ReadingOrderTraversalPolicy extends FocusTraversalPolicy with DirectionalF
} }
if (candidate == null) { if (candidate == null) {
if (scope.children.isNotEmpty) { if (scope.traversalChildren.isNotEmpty) {
candidate = _sortByGeometry(scope).first; candidate = _sortByGeometry(scope).first;
} else { } else {
candidate = currentNode; candidate = currentNode;
...@@ -627,7 +627,7 @@ class ReadingOrderTraversalPolicy extends FocusTraversalPolicy with DirectionalF ...@@ -627,7 +627,7 @@ class ReadingOrderTraversalPolicy extends FocusTraversalPolicy with DirectionalF
// Sorts the list of nodes based on their geometry into the desired reading // Sorts the list of nodes based on their geometry into the desired reading
// order based on the directionality of the context for each node. // order based on the directionality of the context for each node.
Iterable<FocusNode> _sortByGeometry(FocusNode scope) { Iterable<FocusNode> _sortByGeometry(FocusNode scope) {
final Iterable<FocusNode> nodes = scope.descendants; final Iterable<FocusNode> nodes = scope.traversalDescendants;
if (nodes.length <= 1) { if (nodes.length <= 1) {
return nodes; return nodes;
} }
......
...@@ -228,7 +228,7 @@ void main() { ...@@ -228,7 +228,7 @@ void main() {
expect(boxDecoration.color, const Color(0x00FF00)); expect(boxDecoration.color, const Color(0x00FF00));
}); });
testWidgets('Botton respects themes', (WidgetTester tester) async { testWidgets('Button respects themes', (WidgetTester tester) async {
TextStyle textStyle; TextStyle textStyle;
await tester.pumpWidget( await tester.pumpWidget(
......
...@@ -2,8 +2,9 @@ ...@@ -2,8 +2,9 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter/rendering.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
...@@ -66,7 +67,6 @@ void main() { ...@@ -66,7 +67,6 @@ void main() {
expect(material.textStyle.fontWeight, FontWeight.w500); expect(material.textStyle.fontWeight, FontWeight.w500);
expect(material.type, MaterialType.transparency); expect(material.type, MaterialType.transparency);
// Disabled MaterialButton // Disabled MaterialButton
await tester.pumpWidget( await tester.pumpWidget(
const Directionality( const Directionality(
...@@ -321,6 +321,217 @@ void main() { ...@@ -321,6 +321,217 @@ void main() {
expect(material.type, MaterialType.button); expect(material.type, MaterialType.button);
}); });
testWidgets('Do buttons work with hover', (WidgetTester tester) async {
const Color hoverColor = Color(0xff001122);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: MaterialButton(
hoverColor: hoverColor,
onPressed: () { },
child: const Text('button'),
),
),
);
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
await gesture.moveTo(tester.getCenter(find.byType(MaterialButton)));
await tester.pumpAndSettle();
RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures');
expect(inkFeatures, paints..rect(color: hoverColor));
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: FlatButton(
hoverColor: hoverColor,
onPressed: () { },
child: const Text('button'),
),
),
);
await tester.pumpAndSettle();
inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures');
expect(inkFeatures, paints..rect(color: hoverColor));
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: OutlineButton(
hoverColor: hoverColor,
onPressed: () { },
child: const Text('button'),
),
),
);
await tester.pumpAndSettle();
inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures');
expect(inkFeatures, paints..rect(color: hoverColor));
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: RaisedButton(
hoverColor: hoverColor,
onPressed: () { },
child: const Text('button'),
),
),
);
await tester.pumpAndSettle();
inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures');
expect(inkFeatures, paints..rect(color: hoverColor));
gesture.removePointer();
});
testWidgets('Do buttons work with focus', (WidgetTester tester) async {
const Color focusColor = Color(0xff001122);
FocusNode focusNode = FocusNode(debugLabel: 'MaterialButton Node');
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: MaterialButton(
focusColor: focusColor,
focusNode: focusNode,
onPressed: () { },
child: const Text('button'),
),
),
);
focusNode.requestFocus();
await tester.pumpAndSettle();
RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures');
expect(inkFeatures, paints..rect(color: focusColor));
focusNode = FocusNode(debugLabel: 'FlatButton Node');
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: FlatButton(
focusColor: focusColor,
focusNode: focusNode,
onPressed: () { },
child: const Text('button'),
),
),
);
focusNode.requestFocus();
await tester.pumpAndSettle();
inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures');
expect(inkFeatures, paints..rect(color: focusColor));
focusNode = FocusNode(debugLabel: 'RaisedButton Node');
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: RaisedButton(
focusColor: focusColor,
focusNode: focusNode,
onPressed: () { },
child: const Text('button'),
),
),
);
focusNode.requestFocus();
await tester.pumpAndSettle();
inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures');
expect(inkFeatures, paints..rect(color: focusColor));
focusNode = FocusNode(debugLabel: 'OutlineButton Node');
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: OutlineButton(
focusColor: focusColor,
focusNode: focusNode,
onPressed: () { },
child: const Text('button'),
),
),
);
focusNode.requestFocus();
await tester.pumpAndSettle();
inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures');
expect(inkFeatures, paints..rect(color: focusColor));
});
testWidgets('Button elevation and colors have proper precedence', (WidgetTester tester) async {
const double elevation = 10.0;
const double focusElevation = 11.0;
const double hoverElevation = 12.0;
const double highlightElevation = 13.0;
const Color focusColor = Color(0xff001122);
const Color hoverColor = Color(0xff112233);
const Color highlightColor = Color(0xff223344);
final Finder rawButtonMaterial = find.descendant(
of: find.byType(MaterialButton),
matching: find.byType(Material),
);
final FocusNode focusNode = FocusNode(debugLabel: 'MaterialButton Node');
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: MaterialButton(
focusColor: focusColor,
hoverColor: hoverColor,
highlightColor: highlightColor,
elevation: elevation,
focusElevation: focusElevation,
hoverElevation: hoverElevation,
highlightElevation: highlightElevation,
focusNode: focusNode,
onPressed: () { },
child: const Text('button'),
),
),
);
await tester.pumpAndSettle();
// Base elevation
Material material = tester.widget<Material>(rawButtonMaterial);
expect(material.elevation, equals(elevation));
// Focus elevation overrides base
focusNode.requestFocus();
await tester.pumpAndSettle();
material = tester.widget<Material>(rawButtonMaterial);
RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures');
expect(inkFeatures, paints..rect(color: focusColor));
expect(focusNode.hasPrimaryFocus, isTrue);
expect(material.elevation, equals(focusElevation));
// Hover elevation overrides focus
TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
await gesture.moveTo(tester.getCenter(find.byType(MaterialButton)));
await tester.pumpAndSettle();
material = tester.widget<Material>(rawButtonMaterial);
inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures');
expect(inkFeatures, paints..rect(color: focusColor)..rect(color: hoverColor));
expect(material.elevation, equals(hoverElevation));
await gesture.removePointer();
// Highlight elevation overrides hover
gesture = await tester.startGesture(tester.getCenter(find.byType(MaterialButton)));
await tester.pumpAndSettle();
material = tester.widget<Material>(rawButtonMaterial);
inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures');
expect(inkFeatures, paints..rect(color: focusColor)..rect(color: highlightColor));
expect(material.elevation, equals(highlightElevation));
await gesture.up();
});
testWidgets('Does FlatButton contribute semantics', (WidgetTester tester) async { testWidgets('Does FlatButton contribute semantics', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester); final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget( await tester.pumpWidget(
...@@ -952,7 +1163,7 @@ void main() { ...@@ -952,7 +1163,7 @@ void main() {
expect(tester.widget<Material>(rawButtonMaterial).shape, const StadiumBorder()); expect(tester.widget<Material>(rawButtonMaterial).shape, const StadiumBorder());
}); });
testWidgets('MaterialButton defaults', (WidgetTester tester) async { testWidgets('MaterialButton disabled default is correct.', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/30012. // Regression test for https://github.com/flutter/flutter/issues/30012.
final Finder rawButtonMaterial = find.descendant( final Finder rawButtonMaterial = find.descendant(
......
...@@ -105,8 +105,25 @@ void main() { ...@@ -105,8 +105,25 @@ void main() {
); );
expect(find.text('Add'), findsNothing); expect(find.text('Add'), findsNothing);
// Test hover for tooltip.
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
await gesture.moveTo(tester.getCenter(find.byType(FloatingActionButton)));
await tester.pumpAndSettle();
expect(find.text('Add'), findsOneWidget);
await gesture.moveTo(Offset.zero);
await gesture.removePointer();
await tester.pumpAndSettle();
expect(find.text('Add'), findsNothing);
// Test long press for tooltip.
await tester.longPress(find.byType(FloatingActionButton)); await tester.longPress(find.byType(FloatingActionButton));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.text('Add'), findsOneWidget); expect(find.text('Add'), findsOneWidget);
}); });
...@@ -687,20 +704,25 @@ void main() { ...@@ -687,20 +704,25 @@ void main() {
}); });
testWidgets('Floating Action Button has no clip by default', (WidgetTester tester) async { testWidgets('Floating Action Button has no clip by default', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
await tester.pumpWidget( await tester.pumpWidget(
Directionality( Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: Material( child: Material(
child: FloatingActionButton( child: FloatingActionButton(
onPressed: () { /* to make sure the button is enabled */ }, focusNode: focusNode,
), onPressed: () { /* to make sure the button is enabled */ },
), ),
),
), ),
); );
focusNode.unfocus();
await tester.pump();
expect( expect(
tester.renderObject(find.byType(FloatingActionButton)), tester.renderObject(find.byType(FloatingActionButton)),
paintsExactlyCountTimes(#clipPath, 0), paintsExactlyCountTimes(#clipPath, 0),
); );
}); });
} }
......
...@@ -171,8 +171,8 @@ void main() { ...@@ -171,8 +171,8 @@ void main() {
.toList(); .toList();
expect(description, <String>[ expect(description, <String>[
'backgroundColor: Color(0xcafecafe)',
'foregroundColor: Color(0xfeedfeed)', 'foregroundColor: Color(0xfeedfeed)',
'backgroundColor: Color(0xcafecafe)',
'elevation: 23.0', 'elevation: 23.0',
'disabledElevation: 11.0', 'disabledElevation: 11.0',
'highlightElevation: 43.0', 'highlightElevation: 43.0',
......
...@@ -15,32 +15,30 @@ void main() { ...@@ -15,32 +15,30 @@ void main() {
testWidgets('InkWell gestures control test', (WidgetTester tester) async { testWidgets('InkWell gestures control test', (WidgetTester tester) async {
final List<String> log = <String>[]; final List<String> log = <String>[];
await tester.pumpWidget( await tester.pumpWidget(Directionality(
Directionality( textDirection: TextDirection.ltr,
textDirection: TextDirection.ltr, child: Material(
child: Material( child: Center(
child: Center( child: InkWell(
child: InkWell( onTap: () {
onTap: () { log.add('tap');
log.add('tap'); },
}, onDoubleTap: () {
onDoubleTap: () { log.add('double-tap');
log.add('double-tap'); },
}, onLongPress: () {
onLongPress: () { log.add('long-press');
log.add('long-press'); },
}, onTapDown: (TapDownDetails details) {
onTapDown: (TapDownDetails details) { log.add('tap-down');
log.add('tap-down'); },
}, onTapCancel: () {
onTapCancel: () { log.add('tap-cancel');
log.add('tap-cancel'); },
},
),
), ),
), ),
) ),
); ));
await tester.tap(find.byType(InkWell), pointer: 1); await tester.tap(find.byType(InkWell), pointer: 1);
...@@ -92,6 +90,71 @@ void main() { ...@@ -92,6 +90,71 @@ void main() {
await tester.pump(const Duration(seconds: 1)); await tester.pump(const Duration(seconds: 1));
}); });
testWidgets('ink well changes color on hover', (WidgetTester tester) async {
await tester.pumpWidget(Material(
child: Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: Container(
width: 100,
height: 100,
child: InkWell(
hoverColor: const Color(0xff00ff00),
splashColor: const Color(0xffff0000),
focusColor: const Color(0xff0000ff),
highlightColor: const Color(0xf00fffff),
onTap: () {},
onLongPress: () {},
onHover: (bool hover) {}
),
),
),
),
));
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
await gesture.moveTo(tester.getCenter(find.byType(Container)));
await tester.pumpAndSettle();
final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures');
expect(inkFeatures, paints..rect(rect: const Rect.fromLTRB(350.0, 250.0, 450.0, 350.0), color: const Color(0xff00ff00)));
await gesture.removePointer();
});
testWidgets('ink response changes color on focus', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode(debugLabel: 'Ink Focus');
await tester.pumpWidget(Material(
child: Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: Focus(
focusNode: focusNode,
child: Container(
width: 100,
height: 100,
child: InkWell(
hoverColor: const Color(0xff00ff00),
splashColor: const Color(0xffff0000),
focusColor: const Color(0xff0000ff),
highlightColor: const Color(0xf00fffff),
onTap: () {},
onLongPress: () {},
onHover: (bool hover) {}
),
),
),
),
),
));
await tester.pumpAndSettle();
final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures');
expect(inkFeatures, paintsExactlyCountTimes(#rect, 0));
focusNode.requestFocus();
await tester.pumpAndSettle();
expect(inkFeatures, paints
..rect(rect: const Rect.fromLTRB(350.0, 250.0, 450.0, 350.0), color: const Color(0xff0000ff)));
});
group('feedback', () { group('feedback', () {
FeedbackTester feedback; FeedbackTester feedback;
...@@ -109,8 +172,8 @@ void main() { ...@@ -109,8 +172,8 @@ void main() {
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: Center( child: Center(
child: InkWell( child: InkWell(
onTap: () { }, onTap: () {},
onLongPress: () { }, onLongPress: () {},
), ),
), ),
), ),
...@@ -137,8 +200,8 @@ void main() { ...@@ -137,8 +200,8 @@ void main() {
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: Center( child: Center(
child: InkWell( child: InkWell(
onTap: () { }, onTap: () {},
onLongPress: () { }, onLongPress: () {},
enableFeedback: false, enableFeedback: false,
), ),
), ),
...@@ -162,13 +225,14 @@ void main() { ...@@ -162,13 +225,14 @@ void main() {
Directionality( Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: Material( child: Material(
child: CompositedTransformFollower( // forces a layer, which makes the paints easier to separate out child: CompositedTransformFollower(
// forces a layer, which makes the paints easier to separate out
link: LayerLink(), link: LayerLink(),
child: ListView( child: ListView(
addAutomaticKeepAlives: keepAlive, addAutomaticKeepAlives: keepAlive,
dragStartBehavior: DragStartBehavior.down, dragStartBehavior: DragStartBehavior.down,
children: <Widget>[ children: <Widget>[
Container(height: 500.0, child: InkWell(onTap: () { }, child: const Placeholder())), Container(height: 500.0, child: InkWell(onTap: () {}, child: const Placeholder())),
Container(height: 500.0), Container(height: 500.0),
Container(height: 500.0), Container(height: 500.0),
], ],
...@@ -191,6 +255,7 @@ void main() { ...@@ -191,6 +255,7 @@ void main() {
keepAlive ? (paints..circle()) : isNot(paints..circle()), keepAlive ? (paints..circle()) : isNot(paints..circle()),
); );
} }
await runTest(true); await runTest(true);
await runTest(false); await runTest(false);
}); });
...@@ -202,7 +267,7 @@ void main() { ...@@ -202,7 +267,7 @@ void main() {
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: Material( child: Material(
child: InkWell( child: InkWell(
onTap: () { }, onTap: () {},
child: const Text('Button'), child: const Text('Button'),
), ),
), ),
...@@ -213,7 +278,7 @@ void main() { ...@@ -213,7 +278,7 @@ void main() {
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: Material( child: Material(
child: InkWell( child: InkWell(
onTap: () { }, onTap: () {},
child: const Text('Button'), child: const Text('Button'),
excludeFromSemantics: true, excludeFromSemantics: true,
), ),
......
...@@ -13,7 +13,7 @@ void main() { ...@@ -13,7 +13,7 @@ void main() {
testWidgets('OutlineButton implements debugFillProperties', (WidgetTester tester) async { testWidgets('OutlineButton implements debugFillProperties', (WidgetTester tester) async {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
OutlineButton( OutlineButton(
onPressed: () { }, onPressed: () {},
textColor: const Color(0xFF00FF00), textColor: const Color(0xFF00FF00),
disabledTextColor: const Color(0xFFFF0000), disabledTextColor: const Color(0xFFFF0000),
color: const Color(0xFF000000), color: const Color(0xFF000000),
...@@ -42,7 +42,7 @@ void main() { ...@@ -42,7 +42,7 @@ void main() {
body: Center( body: Center(
child: OutlineButton( child: OutlineButton(
child: const Text('OutlineButton'), child: const Text('OutlineButton'),
onPressed: () { }, onPressed: () {},
), ),
), ),
), ),
...@@ -108,7 +108,7 @@ void main() { ...@@ -108,7 +108,7 @@ void main() {
); );
} }
await tester.pumpWidget(buildFrame(() { })); await tester.pumpWidget(buildFrame(() {}));
await tester.press(find.byType(OutlineButton)); await tester.press(find.byType(OutlineButton));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
await tester.pumpWidget(buildFrame(null)); await tester.pumpWidget(buildFrame(null));
...@@ -176,7 +176,7 @@ void main() { ...@@ -176,7 +176,7 @@ void main() {
// Pump a new button with a no-op onPressed callback to make it enabled. // Pump a new button with a no-op onPressed callback to make it enabled.
await tester.pumpWidget( await tester.pumpWidget(
buildFrame(onPressed: () { }), buildFrame(onPressed: () {}),
); );
// Wait for the border color to change from disabled to enabled. // Wait for the border color to change from disabled to enabled.
...@@ -236,9 +236,9 @@ void main() { ...@@ -236,9 +236,9 @@ void main() {
child: Material( child: Material(
child: Center( child: Center(
child: OutlineButton( child: OutlineButton(
key: buttonKey, key: buttonKey,
onPressed: () { }, onPressed: () {},
child: const Text('ABC'), child: const Text('ABC'),
), ),
), ),
), ),
...@@ -259,7 +259,7 @@ void main() { ...@@ -259,7 +259,7 @@ void main() {
child: Material( child: Material(
child: Center( child: Center(
child: OutlineButton( child: OutlineButton(
onPressed: () { }, onPressed: () {},
child: const Text('ABC'), child: const Text('ABC'),
), ),
), ),
...@@ -300,7 +300,7 @@ void main() { ...@@ -300,7 +300,7 @@ void main() {
data: const MediaQueryData(textScaleFactor: 1.0), data: const MediaQueryData(textScaleFactor: 1.0),
child: Center( child: Center(
child: OutlineButton( child: OutlineButton(
onPressed: () { }, onPressed: () {},
child: const Text('ABC'), child: const Text('ABC'),
), ),
), ),
...@@ -321,7 +321,7 @@ void main() { ...@@ -321,7 +321,7 @@ void main() {
data: const MediaQueryData(textScaleFactor: 1.3), data: const MediaQueryData(textScaleFactor: 1.3),
child: Center( child: Center(
child: FlatButton( child: FlatButton(
onPressed: () { }, onPressed: () {},
child: const Text('ABC'), child: const Text('ABC'),
), ),
), ),
...@@ -345,7 +345,7 @@ void main() { ...@@ -345,7 +345,7 @@ void main() {
data: const MediaQueryData(textScaleFactor: 3.0), data: const MediaQueryData(textScaleFactor: 3.0),
child: Center( child: Center(
child: FlatButton( child: FlatButton(
onPressed: () { }, onPressed: () {},
child: const Text('ABC'), child: const Text('ABC'),
), ),
), ),
...@@ -369,7 +369,7 @@ void main() { ...@@ -369,7 +369,7 @@ void main() {
home: Scaffold( home: Scaffold(
body: Center( body: Center(
child: OutlineButton( child: OutlineButton(
onPressed: () { }, onPressed: () {},
// Causes the button to be filled with the theme's canvasColor // Causes the button to be filled with the theme's canvasColor
// instead of Colors.transparent before the button material's // instead of Colors.transparent before the button material's
// elevation is animated to 2.0. // elevation is animated to 2.0.
......
...@@ -297,7 +297,7 @@ void main() { ...@@ -297,7 +297,7 @@ void main() {
), ),
); );
await tester.tap(find.text('PUSH')); await tester.tap(find.text('PUSH'));
expect(await tester.pumpAndSettle(const Duration(minutes: 1)), 2); expect(await tester.pumpAndSettle(const Duration(minutes: 1)), 3);
expect(find.text('PUSH'), findsNothing); expect(find.text('PUSH'), findsNothing);
expect(find.text('HELLO'), findsOneWidget); expect(find.text('HELLO'), findsOneWidget);
final Offset helloPosition1 = tester.getCenter(find.text('HELLO')); final Offset helloPosition1 = tester.getCenter(find.text('HELLO'));
...@@ -342,7 +342,7 @@ void main() { ...@@ -342,7 +342,7 @@ void main() {
expect(helloPosition3.dy, helloPosition4.dy); expect(helloPosition3.dy, helloPosition4.dy);
await gesture.moveBy(const Offset(500.0, 0.0)); await gesture.moveBy(const Offset(500.0, 0.0));
await gesture.up(); await gesture.up();
expect(await tester.pumpAndSettle(const Duration(minutes: 1)), 2); expect(await tester.pumpAndSettle(const Duration(minutes: 1)), 3);
expect(find.text('PUSH'), findsOneWidget); expect(find.text('PUSH'), findsOneWidget);
expect(find.text('HELLO'), findsNothing); expect(find.text('HELLO'), findsNothing);
}); });
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
...@@ -189,7 +190,7 @@ void main() { ...@@ -189,7 +190,7 @@ void main() {
expect(find.byKey(key).hitTestable(), findsOneWidget); expect(find.byKey(key).hitTestable(), findsOneWidget);
}); });
testWidgets('RawMaterialButton can be expanded by parent constraints', (WidgetTester tester) async { testWidgets('$RawMaterialButton can be expanded by parent constraints', (WidgetTester tester) async {
const Key key = Key('test'); const Key key = Key('test');
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
...@@ -208,4 +209,61 @@ void main() { ...@@ -208,4 +209,61 @@ void main() {
expect(tester.getSize(find.byKey(key)), const Size(800.0, 48.0)); expect(tester.getSize(find.byKey(key)), const Size(800.0, 48.0));
}); });
testWidgets('$RawMaterialButton handles focus', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode(debugLabel: 'Button Focus');
const Key key = Key('test');
const Color focusColor = Color(0xff00ff00);
await tester.pumpWidget(
MaterialApp(
home: Center(
child: RawMaterialButton(
key: key,
focusNode: focusNode,
focusColor: focusColor,
onPressed: () {},
child: Container(width: 100, height: 100, color: const Color(0xffff0000)),
),
),
),
);
final RenderBox box = Material.of(tester.element(find.byType(InkWell))) as dynamic;
expect(box, isNot(paints..rect(color: focusColor)));
focusNode.requestFocus();
await tester.pumpAndSettle(const Duration(seconds: 1));
expect(box, paints..rect(color: focusColor));
});
testWidgets('$RawMaterialButton handles hover', (WidgetTester tester) async {
const Key key = Key('test');
const Color hoverColor = Color(0xff00ff00);
await tester.pumpWidget(
MaterialApp(
home: Center(
child: RawMaterialButton(
key: key,
hoverColor: hoverColor,
hoverElevation: 10.5,
onPressed: () {},
child: Container(width: 100, height: 100, color: const Color(0xffff0000)),
),
),
),
);
final RenderBox box = Material.of(tester.element(find.byType(InkWell))) as dynamic;
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
expect(box, isNot(paints..rect(color: hoverColor)));
await gesture.moveTo(tester.getCenter(find.byType(RawMaterialButton)));
await tester.pumpAndSettle(const Duration(seconds: 1));
expect(box, paints..rect(color: hoverColor));
await gesture.removePointer();
});
} }
...@@ -101,11 +101,11 @@ void main() { ...@@ -101,11 +101,11 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(delegate.query, ''); expect(delegate.query, '');
expect(delegate.querysForSuggestions.last, ''); expect(delegate.queriesForSuggestions.last, '');
expect(delegate.querysForResults, hasLength(0)); expect(delegate.queriesForResults, hasLength(0));
// Type W o w into search field // Type W o w into search field
delegate.querysForSuggestions.clear(); delegate.queriesForSuggestions.clear();
await tester.enterText(find.byType(TextField), 'W'); await tester.enterText(find.byType(TextField), 'W');
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(delegate.query, 'W'); expect(delegate.query, 'W');
...@@ -116,8 +116,8 @@ void main() { ...@@ -116,8 +116,8 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(delegate.query, 'Wow'); expect(delegate.query, 'Wow');
expect(delegate.querysForSuggestions, <String>['W', 'Wo', 'Wow']); expect(delegate.queriesForSuggestions, <String>['W', 'Wo', 'Wow']);
expect(delegate.querysForResults, hasLength(0)); expect(delegate.queriesForResults, hasLength(0));
}); });
testWidgets('Shows Results and closes search', (WidgetTester tester) async { testWidgets('Shows Results and closes search', (WidgetTester tester) async {
...@@ -143,7 +143,7 @@ void main() { ...@@ -143,7 +143,7 @@ void main() {
final TextField textField = tester.widget(find.byType(TextField)); final TextField textField = tester.widget(find.byType(TextField));
expect(textField.focusNode.hasFocus, isFalse); expect(textField.focusNode.hasFocus, isFalse);
expect(delegate.querysForResults, <String>['Wow']); expect(delegate.queriesForResults, <String>['Wow']);
// Close search // Close search
await tester.tap(find.byTooltip('Back')); await tester.tap(find.byTooltip('Back'));
...@@ -170,13 +170,13 @@ void main() { ...@@ -170,13 +170,13 @@ void main() {
expect(find.text('Results'), findsNothing); expect(find.text('Results'), findsNothing);
// Typing query Wow // Typing query Wow
delegate.querysForSuggestions.clear(); delegate.queriesForSuggestions.clear();
await tester.enterText(find.byType(TextField), 'Wow'); await tester.enterText(find.byType(TextField), 'Wow');
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(delegate.query, 'Wow'); expect(delegate.query, 'Wow');
expect(delegate.querysForSuggestions, <String>['Wow']); expect(delegate.queriesForSuggestions, <String>['Wow']);
expect(delegate.querysForResults, hasLength(0)); expect(delegate.queriesForResults, hasLength(0));
await tester.tap(find.text('Suggestions')); await tester.tap(find.text('Suggestions'));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
...@@ -186,13 +186,13 @@ void main() { ...@@ -186,13 +186,13 @@ void main() {
expect(find.text('Results'), findsOneWidget); expect(find.text('Results'), findsOneWidget);
expect(delegate.query, 'Wow'); expect(delegate.query, 'Wow');
expect(delegate.querysForSuggestions, <String>['Wow']); expect(delegate.queriesForSuggestions, <String>['Wow']);
expect(delegate.querysForResults, <String>['Wow']); expect(delegate.queriesForResults, <String>['Wow']);
TextField textField = tester.widget(find.byType(TextField)); TextField textField = tester.widget(find.byType(TextField));
expect(textField.focusNode.hasFocus, isFalse); expect(textField.focusNode.hasFocus, isFalse);
// Taping search field to go back to suggestions // Tapping search field to go back to suggestions
await tester.tap(find.byType(TextField)); await tester.tap(find.byType(TextField));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
...@@ -201,15 +201,15 @@ void main() { ...@@ -201,15 +201,15 @@ void main() {
expect(find.text('Suggestions'), findsOneWidget); expect(find.text('Suggestions'), findsOneWidget);
expect(find.text('Results'), findsNothing); expect(find.text('Results'), findsNothing);
expect(delegate.querysForSuggestions, <String>['Wow', 'Wow']); expect(delegate.queriesForSuggestions, <String>['Wow', 'Wow']);
expect(delegate.querysForResults, <String>['Wow']); expect(delegate.queriesForResults, <String>['Wow']);
await tester.enterText(find.byType(TextField), 'Foo'); await tester.enterText(find.byType(TextField), 'Foo');
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(delegate.query, 'Foo'); expect(delegate.query, 'Foo');
expect(delegate.querysForSuggestions, <String>['Wow', 'Wow', 'Foo']); expect(delegate.queriesForSuggestions, <String>['Wow', 'Wow', 'Foo']);
expect(delegate.querysForResults, <String>['Wow']); expect(delegate.queriesForResults, <String>['Wow']);
// Go to results again // Go to results again
await tester.tap(find.text('Suggestions')); await tester.tap(find.text('Suggestions'));
...@@ -219,8 +219,8 @@ void main() { ...@@ -219,8 +219,8 @@ void main() {
expect(find.text('Results'), findsOneWidget); expect(find.text('Results'), findsOneWidget);
expect(delegate.query, 'Foo'); expect(delegate.query, 'Foo');
expect(delegate.querysForSuggestions, <String>['Wow', 'Wow', 'Foo']); expect(delegate.queriesForSuggestions, <String>['Wow', 'Wow', 'Foo']);
expect(delegate.querysForResults, <String>['Wow', 'Foo']); expect(delegate.queriesForResults, <String>['Wow', 'Foo']);
textField = tester.widget(find.byType(TextField)); textField = tester.widget(find.byType(TextField));
expect(textField.focusNode.hasFocus, isFalse); expect(textField.focusNode.hasFocus, isFalse);
...@@ -656,12 +656,12 @@ class _TestSearchDelegate extends SearchDelegate<String> { ...@@ -656,12 +656,12 @@ class _TestSearchDelegate extends SearchDelegate<String> {
); );
} }
final List<String> querysForSuggestions = <String>[]; final List<String> queriesForSuggestions = <String>[];
final List<String> querysForResults = <String>[]; final List<String> queriesForResults = <String>[];
@override @override
Widget buildSuggestions(BuildContext context) { Widget buildSuggestions(BuildContext context) {
querysForSuggestions.add(query); queriesForSuggestions.add(query);
return MaterialButton( return MaterialButton(
onPressed: () { onPressed: () {
showResults(context); showResults(context);
...@@ -672,7 +672,7 @@ class _TestSearchDelegate extends SearchDelegate<String> { ...@@ -672,7 +672,7 @@ class _TestSearchDelegate extends SearchDelegate<String> {
@override @override
Widget buildResults(BuildContext context) { Widget buildResults(BuildContext context) {
querysForResults.add(query); queriesForResults.add(query);
return const Text('Results'); return const Text('Results');
} }
......
...@@ -3263,6 +3263,8 @@ void main() { ...@@ -3263,6 +3263,8 @@ void main() {
), ),
), ),
); );
focusNode.requestFocus();
await tester.pump();
const String testValue = 'a big house\njumped over a mouse'; // 11 \n 19 const String testValue = 'a big house\njumped over a mouse'; // 11 \n 19
await tester.enterText(find.byType(TextField), testValue); await tester.enterText(find.byType(TextField), testValue);
......
...@@ -167,7 +167,7 @@ void main() { ...@@ -167,7 +167,7 @@ void main() {
await tester.pump(const Duration(milliseconds: 10)); await tester.pump(const Duration(milliseconds: 10));
expect(tester.hasRunningAnimations, isFalse); expect(tester.hasRunningAnimations, isFalse);
await tester.pumpAndSettle(); expect(await tester.pumpAndSettle(), 1);
transformWidget = tester.firstWidget(find.byType(Transform)); transformWidget = tester.firstWidget(find.byType(Transform));
// Icon has not rotated. // Icon has not rotated.
......
...@@ -631,8 +631,8 @@ void main() { ...@@ -631,8 +631,8 @@ void main() {
FocusScope.of(keyB.currentContext).requestFocus(keyB.currentState.focusNode); FocusScope.of(keyB.currentContext).requestFocus(keyB.currentState.focusNode);
FocusScope.of(keyA.currentContext).requestFocus(keyA.currentState.focusNode); FocusScope.of(keyA.currentContext).requestFocus(keyA.currentState.focusNode);
final FocusScopeNode bScope = FocusScope.of(keyB.currentContext);
final FocusScopeNode aScope = FocusScope.of(keyA.currentContext); final FocusScopeNode aScope = FocusScope.of(keyA.currentContext);
final FocusScopeNode bScope = FocusScope.of(keyB.currentContext);
WidgetsBinding.instance.focusManager.rootScope.setFirstFocus(bScope); WidgetsBinding.instance.focusManager.rootScope.setFirstFocus(bScope);
WidgetsBinding.instance.focusManager.rootScope.setFirstFocus(aScope); WidgetsBinding.instance.focusManager.rootScope.setFirstFocus(aScope);
...@@ -777,8 +777,8 @@ void main() { ...@@ -777,8 +777,8 @@ void main() {
FocusScope.of(keyB.currentContext).requestFocus(keyB.currentState.focusNode); FocusScope.of(keyB.currentContext).requestFocus(keyB.currentState.focusNode);
FocusScope.of(keyA.currentContext).requestFocus(keyA.currentState.focusNode); FocusScope.of(keyA.currentContext).requestFocus(keyA.currentState.focusNode);
final FocusScopeNode aScope = FocusScope.of(keyA.currentContext);
final FocusScopeNode bScope = FocusScope.of(keyB.currentContext); final FocusScopeNode bScope = FocusScope.of(keyB.currentContext);
final FocusScopeNode aScope = FocusScope.of(keyA.currentContext);
WidgetsBinding.instance.focusManager.rootScope.setFirstFocus(bScope); WidgetsBinding.instance.focusManager.rootScope.setFirstFocus(bScope);
WidgetsBinding.instance.focusManager.rootScope.setFirstFocus(aScope); WidgetsBinding.instance.focusManager.rootScope.setFirstFocus(aScope);
......
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="manual_tests - focus" type="FlutterRunConfigurationType" factoryName="Flutter" singleton="false">
<option name="filePath" value="$PROJECT_DIR$/dev/manual_tests/lib/focus.dart" />
<method v="2" />
</configuration>
</component>
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="manual_tests - hover" type="FlutterRunConfigurationType" factoryName="Flutter" singleton="false">
<option name="filePath" value="$PROJECT_DIR$/dev/manual_tests/lib/hover.dart" />
<method v="2" />
</configuration>
</component>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment