新按钮和按钮主题
概述
#Flutter 中新增了一套基本的 Material 按钮组件和小部件主题。原始类已被弃用,并最终将被移除。总体目标是使按钮更灵活,并且更易于通过构造函数参数或主题进行配置。
FlatButton
、RaisedButton
和 OutlineButton
小部件已分别被 TextButton
、ElevatedButton
和 OutlinedButton
替代。每个新的按钮类都有自己的主题:TextButtonTheme
、ElevatedButtonTheme
和 OutlinedButtonTheme
。原始的 ButtonTheme
类已不再使用。按钮的外观由 ButtonStyle
对象指定,而非一大堆小部件参数和属性。这大致类似于使用 TextStyle
对象定义文本外观的方式。新的按钮主题也通过 ButtonStyle
对象进行配置。ButtonStyle
本身只是一组视觉属性的集合。这些属性中的许多都使用 MaterialStateProperty
定义,这意味着它们的值可以取决于按钮的状态。
背景
#我们没有尝试就地演进现有的按钮类及其主题,而是引入了新的替代按钮小部件和主题。除了使我们摆脱了就地演进现有类所带来的向后兼容性困境之外,新名称还使 Flutter 与 Material Design 规范保持同步,该规范使用了按钮组件的新名称。
旧小部件 | 旧主题 | 新小部件 | 新主题 |
---|---|---|---|
FlatButton | ButtonTheme | 文本按钮 | TextButtonTheme |
RaisedButton | ButtonTheme | 凸起按钮 | ElevatedButtonTheme |
OutlineButton | ButtonTheme | OutlinedButton | OutlinedButtonTheme |
新主题遵循了 Flutter 大约一年前为新的 Material 小部件采用的“规范化”模式。主题属性和小部件构造函数参数默认为 null。非 null 的主题属性和小部件参数指定了对组件默认值的覆盖。实现和记录默认值是按钮组件小部件的唯一责任。默认值本身主要基于整体 Theme 的 colorScheme 和 textTheme。
在视觉上,新按钮看起来略有不同,因为它们符合当前的 Material Design 规范,并且它们的颜色是根据整体 Theme 的 ColorScheme 配置的。在内边距、圆角半径以及悬停/焦点/按下反馈方面也有其他细微差别。
许多应用程序将能够直接用新类名替换旧类名。带有黄金图像测试或按钮外观已通过构造函数参数或原始 ButtonTheme
配置的应用程序可能需要查阅迁移指南和随后的介绍材料。
API 变更:使用 ButtonStyle 而非单独的样式属性
#除了简单的用例,新按钮类的 API 与旧类不兼容。新按钮和主题的视觉属性通过单个 ButtonStyle
对象进行配置,类似于 TextField
或 Text
小部件可以使用 TextStyle
对象进行配置的方式。大多数 ButtonStyle
属性都使用 MaterialStateProperty
定义,因此单个属性可以根据按钮的按下/聚焦/悬停/等状态表示不同的值。
按钮的 ButtonStyle
不定义按钮的视觉属性,它定义了对按钮默认视觉属性的覆盖,其中默认属性由按钮小部件本身计算。例如,要覆盖 TextButton
在所有状态下的默认前景(文本/图标)颜色,可以这样编写
TextButton(
style: ButtonStyle(
foregroundColor: MaterialStateProperty.all<Color>(Colors.blue),
),
onPressed: () { },
child: Text('TextButton'),
)
这种覆盖很常见;然而,在许多情况下,还需要覆盖文本按钮用于指示其悬停/焦点/按下状态的覆盖颜色。这可以通过将 overlayColor
属性添加到 ButtonStyle
来实现。
TextButton(
style: ButtonStyle(
foregroundColor: MaterialStateProperty.all<Color>(Colors.blue),
overlayColor: MaterialStateProperty.resolveWith<Color?>(
(Set<MaterialState> states) {
if (states.contains(MaterialState.hovered))
return Colors.blue.withOpacity(0.04);
if (states.contains(MaterialState.focused) ||
states.contains(MaterialState.pressed))
return Colors.blue.withOpacity(0.12);
return null; // Defer to the widget's default.
},
),
),
onPressed: () { },
child: Text('TextButton')
)
颜色 MaterialStateProperty
只需为应被覆盖的默认颜色返回一个值。如果它返回 null,则将使用小部件的默认值。例如,只覆盖文本按钮的焦点覆盖颜色
TextButton(
style: ButtonStyle(
overlayColor: MaterialStateProperty.resolveWith<Color?>(
(Set<MaterialState> states) {
if (states.contains(MaterialState.focused))
return Colors.red;
return null; // Defer to the widget's default.
}
),
),
onPressed: () { },
child: Text('TextButton'),
)
styleFrom()
ButtonStyle 实用方法
#Material Design 规范根据配色方案的主色定义按钮的前景色和覆盖色。主色以不同的不透明度呈现,具体取决于按钮的状态。为了简化创建包含所有依赖于配色方案颜色的属性的按钮样式,每个按钮类都包含一个静态 styleFrom()
方法,该方法从一组简单的值(包括其所依赖的 ColorScheme
颜色)构造一个 ButtonStyle
。
此示例创建一个按钮,它使用指定的主色和 Material Design 规范中的不透明度来覆盖其前景颜色和覆盖颜色。
TextButton(
style: TextButton.styleFrom(
foregroundColor: Colors.blue,
),
onPressed: () { },
child: Text('TextButton'),
)
TextButton
文档指出,当按钮被禁用时,前景颜色基于配色方案的 disabledForegroundColor
颜色。要使用 styleFrom()
也覆盖它,可以这样
TextButton(
style: TextButton.styleFrom(
foregroundColor: Colors.blue,
disabledForegroundColor: Colors.red,
),
onPressed: null,
child: Text('TextButton'),
)
如果您正在尝试创建 Material Design 变体,使用 styleFrom()
方法是创建 ButtonStyle
的首选方式。最灵活的方法是直接定义 ButtonStyle
,其中包含您想要覆盖外观的状态的 MaterialStateProperty
值。
ButtonStyle 默认值
#新的按钮类等小部件根据整体主题的 colorScheme
和 textTheme
以及按钮的当前状态来 *计算* 其默认值。在少数情况下,它们还会考虑整体主题的配色方案是亮色还是暗色。每个按钮都有一个受保护的方法,可以根据需要计算其默认样式。尽管应用程序不会直接调用此方法,但其 API 文档解释了所有默认值。当按钮或按钮主题指定 ButtonStyle
时,只有按钮样式中的非 null 属性会覆盖计算出的默认值。按钮的 style
参数会覆盖相应按钮主题中指定的非 null 属性。例如,如果 TextButton
样式的 foregroundColor
属性为非 null,它将覆盖 TextButtonTheme
样式的相同属性。
如前所述,每个按钮类都包含一个名为 styleFrom
的静态方法,它从一组简单的值(包括其所依赖的 ColorScheme
颜色)构造一个 ButtonStyle
。在许多常见情况下,使用 styleFrom
创建一个覆盖默认值的一次性 ButtonStyle
是最简单的。当自定义样式旨在覆盖配色方案中的某个颜色(例如默认样式所依赖的 primary
或 onPrimary
)时,尤其如此。对于其他情况,您可以直接创建一个 ButtonStyle
对象。这样做使您能够控制按钮所有可能状态(如按下、悬停、禁用和聚焦)下的视觉属性(如颜色)的值。
迁移指南
#使用以下信息将您的按钮迁移到新的 API。
恢复原始按钮的视觉效果
#在许多情况下,可以直接从旧按钮类切换到新按钮类。这假定尺寸/形状的微小变化以及颜色上可能更大的变化不是问题。
为了在这些情况下保留原始按钮的外观,您可以定义尽可能与原始按钮匹配的按钮样式。例如,以下样式使 TextButton
看起来像默认的 FlatButton
final ButtonStyle flatButtonStyle = TextButton.styleFrom(
foregroundColor: Colors.black87,
minimumSize: Size(88, 36),
padding: EdgeInsets.symmetric(horizontal: 16),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(2)),
),
);
TextButton(
style: flatButtonStyle,
onPressed: () { },
child: Text('Looks like a FlatButton'),
)
类似地,要使 ElevatedButton
看起来像默认的 RaisedButton
final ButtonStyle raisedButtonStyle = ElevatedButton.styleFrom(
foregroundColor: Colors.black87,
backgroundColor: Colors.grey[300],
minimumSize: Size(88, 36),
padding: EdgeInsets.symmetric(horizontal: 16),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(2)),
),
);
ElevatedButton(
style: raisedButtonStyle,
onPressed: () { },
child: Text('Looks like a RaisedButton'),
)
OutlinedButton
的 OutlineButton
样式稍微复杂一些,因为当按钮被按下时,轮廓的颜色会变为主题色。轮廓的外观由 BorderSide
定义,您将使用 MaterialStateProperty
来定义按下时的轮廓颜色
final ButtonStyle outlineButtonStyle = OutlinedButton.styleFrom(
foregroundColor: Colors.black87,
minimumSize: Size(88, 36),
padding: EdgeInsets.symmetric(horizontal: 16),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(2)),
),
).copyWith(
side: MaterialStateProperty.resolveWith<BorderSide?>(
(Set<MaterialState> states) {
if (states.contains(MaterialState.pressed)) {
return BorderSide(
color: Theme.of(context).colorScheme.primary,
width: 1,
);
}
return null;
},
),
);
OutlinedButton(
style: outlineButtonStyle,
onPressed: () { },
child: Text('Looks like an OutlineButton'),
)
要恢复整个应用程序中按钮的默认外观,您可以在应用程序的主题中配置新的按钮主题
MaterialApp(
theme: ThemeData.from(colorScheme: ColorScheme.light()).copyWith(
textButtonTheme: TextButtonThemeData(style: flatButtonStyle),
elevatedButtonTheme: ElevatedButtonThemeData(style: raisedButtonStyle),
outlinedButtonTheme: OutlinedButtonThemeData(style: outlineButtonStyle),
),
)
要在应用程序的某一部分中恢复按钮的默认外观,您可以使用 TextButtonTheme
、ElevatedButtonTheme
或 OutlinedButtonTheme
包装小部件子树。例如
TextButtonTheme(
data: TextButtonThemeData(style: flatButtonStyle),
child: myWidgetSubtree,
)
迁移带有自定义颜色的按钮
#以下部分介绍如何使用以下 FlatButton
、RaisedButton
和 OutlineButton
颜色参数
textColor
disabledTextColor
color
disabledColor
focusColor
hoverColor
highlightColor*
splashColor
新的按钮类不支持单独的高亮颜色,因为它不再是 Material Design 的一部分。
迁移带有自定义前景和背景颜色的按钮
#原始按钮类的两种常见自定义是 FlatButton
的自定义前景颜色,或 RaisedButton
的自定义前景和背景颜色。使用新按钮类实现相同结果很简单
FlatButton(
textColor: Colors.red, // foreground
onPressed: () { },
child: Text('FlatButton with custom foreground/background'),
)
TextButton(
style: TextButton.styleFrom(
foregroundColor Colors.red,
),
onPressed: () { },
child: Text('TextButton with custom foreground'),
)
在这种情况下,TextButton
的前景(文本/图标)颜色及其悬停/聚焦/按下时的覆盖颜色将基于 Colors.red
。默认情况下,TextButton
的背景填充颜色是透明的。
迁移带有自定义前景和背景颜色的 RaisedButton
RaisedButton(
color: Colors.red, // background
textColor: Colors.white, // foreground
onPressed: () { },
child: Text('RaisedButton with custom foreground/background'),
)
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
),
onPressed: () { },
child: Text('ElevatedButton with custom foreground/background'),
)
在这种情况下,按钮对配色方案主色的使用相对于 TextButton
是相反的:primary
是按钮的背景填充颜色,而 onPrimary
是前景(文本/图标)颜色。
迁移带有自定义覆盖颜色的按钮
#覆盖按钮的默认聚焦、悬停、高亮或波纹颜色不那么常见。FlatButton
、RaisedButton
和 OutlineButton
类为这些依赖于状态的颜色提供了单独的参数。新的 TextButton
、ElevatedButton
和 OutlinedButton
类则使用单个 MaterialStateProperty<Color>
参数。新按钮允许为所有颜色指定依赖于状态的值,而原始按钮只支持指定现在称为“overlayColor
”的颜色。
FlatButton(
focusColor: Colors.red,
hoverColor: Colors.green,
splashColor: Colors.blue,
onPressed: () { },
child: Text('FlatButton with custom overlay colors'),
)
TextButton(
style: ButtonStyle(
overlayColor: MaterialStateProperty.resolveWith<Color?>(
(Set<MaterialState> states) {
if (states.contains(MaterialState.focused))
return Colors.red;
if (states.contains(MaterialState.hovered))
return Colors.green;
if (states.contains(MaterialState.pressed))
return Colors.blue;
return null; // Defer to the widget's default.
}),
),
onPressed: () { },
child: Text('TextButton with custom overlay colors'),
)
新版本更灵活,但不够紧凑。在原始版本中,不同状态的优先级是隐式的(且未文档化)和固定的,而在新版本中,它是显式的。对于频繁指定这些颜色的应用程序,最简单的迁移路径是定义一个或多个与上述示例匹配的 ButtonStyle
——并只使用样式参数——或者定义一个封装了三个颜色参数的无状态包装器小部件。
迁移带有自定义禁用颜色的按钮
#这是一种相对罕见的自定义。FlatButton
、RaisedButton
和 OutlineButton
类具有 disabledTextColor
和 disabledColor
参数,它们定义当按钮的 onPressed
回调为 null 时的背景和前景颜色。
默认情况下,所有按钮都使用配色方案的 disabledForegroundColor
颜色,禁用前景色的不透明度为 0.38。只有 ElevatedButton
具有非透明背景颜色,其默认值是 disabledForegroundColor
颜色,不透明度为 0.12。因此在许多情况下,只需使用 styleFrom
方法即可覆盖禁用颜色
RaisedButton(
disabledColor: Colors.red.withOpacity(0.12),
disabledTextColor: Colors.red.withOpacity(0.38),
onPressed: null,
child: Text('RaisedButton with custom disabled colors'),
),
ElevatedButton(
style: ElevatedButton.styleFrom(disabledForegroundColor: Colors.red),
onPressed: null,
child: Text('ElevatedButton with custom disabled colors'),
)
要完全控制禁用颜色,必须明确地以 MaterialStateProperties
的形式定义 ElevatedButton
的样式
RaisedButton(
disabledColor: Colors.red,
disabledTextColor: Colors.blue,
onPressed: null,
child: Text('RaisedButton with custom disabled colors'),
)
ElevatedButton(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.resolveWith<Color?>(
(Set<MaterialState> states) {
if (states.contains(MaterialState.disabled))
return Colors.red;
return null; // Defer to the widget's default.
}),
foregroundColor: MaterialStateProperty.resolveWith<Color?>(
(Set<MaterialState> states) {
if (states.contains(MaterialState.disabled))
return Colors.blue;
return null; // Defer to the widget's default.
}),
),
onPressed: null,
child: Text('ElevatedButton with custom disabled colors'),
)
与上一个案例一样,在经常出现此迁移的应用程序中,有明显的方法可以使新版本更紧凑。
迁移带有自定义高程的按钮
#这也是一种相对罕见的自定义。通常,只有 ElevatedButton
(最初称为 RaisedButtons
)包含高程变化。对于与基线高程成比例的高程(根据 Material Design 规范),可以非常简单地覆盖所有这些高程。
默认情况下,禁用按钮的高程为 0,其余状态相对于基线 2 定义
disabled: 0
hovered or focused: baseline + 2
pressed: baseline + 6
因此,要迁移已定义所有高程的 RaisedButton
RaisedButton(
elevation: 2,
focusElevation: 4,
hoverElevation: 4,
highlightElevation: 8,
disabledElevation: 0,
onPressed: () { },
child: Text('RaisedButton with custom elevations'),
)
ElevatedButton(
style: ElevatedButton.styleFrom(elevation: 2),
onPressed: () { },
child: Text('ElevatedButton with custom elevations'),
)
要任意覆盖其中一个高程,例如按下时的高程
RaisedButton(
highlightElevation: 16,
onPressed: () { },
child: Text('RaisedButton with a custom elevation'),
)
ElevatedButton(
style: ButtonStyle(
elevation: MaterialStateProperty.resolveWith<double?>(
(Set<MaterialState> states) {
if (states.contains(MaterialState.pressed))
return 16;
return null;
}),
),
onPressed: () { },
child: Text('ElevatedButton with a custom elevation'),
)
迁移带有自定义形状和边框的按钮
#原始的 FlatButton
、RaisedButton
和 OutlineButton
类都提供了一个 shape
参数,该参数定义了按钮的形状及其轮廓的外观。相应的新类及其主题支持使用 OutlinedBorder shape
和 BorderSide side
参数分别指定按钮的形状和边框。
在此示例中,原始 OutlineButton
版本为其高亮(按下)状态的边框指定了与其它状态相同的颜色。
OutlineButton(
shape: StadiumBorder(),
highlightedBorderColor: Colors.red,
borderSide: BorderSide(
width: 2,
color: Colors.red
),
onPressed: () { },
child: Text('OutlineButton with custom shape and border'),
)
OutlinedButton(
style: OutlinedButton.styleFrom(
shape: StadiumBorder(),
side: BorderSide(
width: 2,
color: Colors.red
),
),
onPressed: () { },
child: Text('OutlinedButton with custom shape and border'),
)
新的 OutlinedButton
小部件的大多数样式参数,包括其形状和边框,都可以使用 MaterialStateProperty
值指定,也就是说它们可以根据按钮的状态具有不同的值。要在按钮按下时指定不同的边框颜色,请执行以下操作
OutlineButton(
shape: StadiumBorder(),
highlightedBorderColor: Colors.blue,
borderSide: BorderSide(
width: 2,
color: Colors.red
),
onPressed: () { },
child: Text('OutlineButton with custom shape and border'),
)
OutlinedButton(
style: ButtonStyle(
shape: MaterialStateProperty.all<OutlinedBorder>(StadiumBorder()),
side: MaterialStateProperty.resolveWith<BorderSide>(
(Set<MaterialState> states) {
final Color color = states.contains(MaterialState.pressed)
? Colors.blue
: Colors.red;
return BorderSide(color: color, width: 2);
}
),
),
onPressed: () { },
child: Text('OutlinedButton with custom shape and border'),
)
时间线
#发布版本:1.20.0-0.0.pre
稳定版本:2.0.0
参考资料
#API 文档
ButtonStyle
ButtonStyleButton
凸起按钮
ElevatedButtonTheme
ElevatedButtonThemeData
OutlinedButton
OutlinedButtonTheme
OutlinedButtonThemeData
文本按钮
TextButtonTheme
TextButtonThemeData
相关 PR