新的按钮和按钮主题
摘要
#Flutter 添加了一套新的基本 Material 按钮部件和主题。原始类已弃用,最终将被移除。总体目标是使按钮更灵活,并通过构造函数参数或主题更容易配置。
FlatButton
、RaisedButton
和 OutlineButton
部件已被分别替换为 TextButton
、ElevatedButton
和 OutlinedButton
。每个新的按钮类都有自己的主题:TextButtonTheme
、ElevatedButtonTheme
和 OutlinedButtonTheme
。原始的 ButtonTheme
类不再使用。按钮的外观由 ButtonStyle
对象指定,而不是大量的部件参数和属性。这大致类似于使用 TextStyle
对象定义文本外观的方式。新的按钮主题也使用 ButtonStyle
对象进行配置。ButtonStyle
本身只是一个视觉属性的集合。许多这些属性都是用 MaterialStateProperty
定义的,这意味着它们的值可以取决于按钮的状态。
背景
#与其尝试就地演化现有的按钮类及其主题,我们引入了新的替代按钮部件和主题。除了让我们免于就地演化现有类所带来的向后兼容性难题之外,新的名称还使 Flutter 与 Material Design 规范同步,该规范使用新的名称来表示按钮组件。
旧部件 | 旧主题 | 新部件 | 新主题 |
---|---|---|---|
FlatButton | ButtonTheme | TextButton | TextButtonTheme |
RaisedButton | ButtonTheme | ElevatedButton | ElevatedButtonTheme |
OutlineButton | ButtonTheme | OutlinedButton | OutlinedButtonTheme |
新的主题遵循 Flutter 大约一年前为新的 Material 部件采用的“标准化”模式。主题属性和部件构造函数参数默认为 null。非 null 主题属性和部件参数指定对组件默认值的覆盖。实现和记录默认值是按钮组件部件的唯一责任。默认值本身主要基于整体主题的 colorScheme 和 textTheme。
从视觉上看,新的按钮外观略有不同,因为它们符合当前的 Material Design 规范,并且它们的颜色是根据整体主题的 ColorScheme 配置的。填充、圆角半径以及悬停/焦点/按下反馈方面也存在其他细微差异。
许多应用程序只需将新的类名替换为旧的类名即可。具有黄金图像测试或其外观已使用构造函数参数或原始 ButtonTheme
配置的按钮的应用程序可能需要查阅迁移指南和后续的介绍性资料。
API 变更:使用 ButtonStyle 代替单个样式属性
#除了简单的用例外,新按钮类的 API 与旧类不兼容。新按钮和主题的视觉属性使用单个 ButtonStyle
对象进行配置,类似于如何使用 TextStyle
对象配置 TextField
或 Text
部件。大多数 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; // 委托给部件的默认值。
},
),
),
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; // 委托给部件的默认值。
}
),
),
onPressed: () { },
child: Text('TextButton'),
)
styleFrom()
ButtonStyle 工具方法
#Material Design 规范根据颜色方案的主颜色定义按钮的前景色和叠加颜色。根据按钮的状态,主颜色会以不同的不透明度呈现。为了简化创建包含所有依赖于颜色方案颜色的属性的按钮样式,每个按钮类都包含一个静态 styleFrom()
方法,该方法从一组简单的值构造 ButtonStyle
,包括它所依赖的 ColorScheme
颜色。
此示例创建一个按钮,它使用指定的主颜色和 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 的,则它会覆盖 TextButonTheme
样式的同一属性。
如前所述,每个按钮类都包含一个名为 styleFrom
的静态方法,该方法从一组简单的值构造 ButtonStyle,包括它所依赖的 ColorScheme
颜色。在许多常见情况下,使用 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
的使用,
迁移具有自定义颜色参数的 FlatButton
、RaisedButton
和 OutlineButton
:
#textColor
disabledTextColor
color
disabledColor
focusColor
hoverColor
highlightColor*
splashColor
新的按钮类不支持单独的 highlightColor(高亮颜色),因为它不再是 Material Design 的一部分。
迁移具有自定义前景色和背景色的按钮
#原始按钮类的两种常见自定义方式是 FlatButton
的自定义前景色,或 RaisedButton
的自定义前景色和背景色。使用新的按钮类可以轻松实现相同的结果:
FlatButton(
textColor: Colors.red, // 前景色
onPressed: () { },
child: Text('具有自定义前景色/背景色的 FlatButton'),
)
TextButton(
style: TextButton.styleFrom(
foregroundColor: Colors.red,
),
onPressed: () { },
child: Text('具有自定义前景色的 TextButton'),
)
在这种情况下,TextButton
的前景色(文本/图标)颜色及其悬停/聚焦/按下叠加颜色将基于 Colors.red
。默认情况下,TextButton
的背景填充颜色是透明的。
迁移具有自定义前景色和背景色的 RaisedButton
:
RaisedButton(
color: Colors.red, // 背景色
textColor: Colors.white, // 前景色
onPressed: () { },
child: Text('具有自定义前景色/背景色的 RaisedButton'),
)
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
),
onPressed: () { },
child: Text('具有自定义前景色/背景色的 ElevatedButton'),
)
在这种情况下,按钮对颜色方案的主颜色的使用与 TextButton
相反:主色是按钮的背景填充色,onPrimary
是前景色(文本/图标)颜色。
迁移具有自定义叠加颜色的按钮
#覆盖按钮的默认焦点、悬停、高亮或飞溅颜色并不常见。FlatButton
、RaisedButton
和 OutlineButton
类为这些状态相关的颜色提供了单独的参数。新的 TextButton
、ElevatedButton
和 OutlinedButton
类则使用单个 MaterialStateProperty<Color>
参数。新的按钮允许指定所有颜色的状态相关值,而原始按钮仅支持指定现在称为“overlayColor”的内容。
FlatButton(
focusColor: Colors.red,
hoverColor: Colors.green,
splashColor: Colors.blue,
onPressed: () { },
child: Text('具有自定义叠加颜色的 FlatButton'),
)
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; // 委托给部件的默认值。
}),
),
onPressed: () { },
child: Text('具有自定义叠加颜色的 TextButton'),
)
新版本虽然不那么紧凑,但更灵活。在原始版本中,不同状态的优先级是隐式(且未记录)且固定的,在新版本中,它是显式的。对于经常指定这些颜色的应用程序,最简单的迁移路径是定义一个或多个与上述示例匹配的 ButtonStyle
- 并只使用 style 参数 - 或定义一个封装了三个颜色参数的无状态包装器部件。
迁移具有自定义禁用颜色的按钮
#这是一个相对罕见的自定义。FlatButton
、RaisedButton
和 OutlineButton
类具有 disabledTextColor
和 disabledColor
参数,它们定义了按钮的 onPressed
回调为 null 时的背景色和前景色。
默认情况下,所有按钮都使用颜色方案的 disabledForegroundColor
颜色,禁用前景色的不透明度为 0.38。只有 ElevatedButton
具有非透明背景色,其默认值为不透明度为 0.12 的 disabledForegroundColor
颜色。因此,在许多情况下,只需使用 styleFrom
方法即可覆盖禁用的颜色:
RaisedButton(
disabledColor: Colors.red.withOpacity(0.12),
disabledTextColor: Colors.red.withOpacity(0.38),
onPressed: null,
child: Text('具有自定义禁用颜色的 RaisedButton'),
),
ElevatedButton(
style: ElevatedButton.styleFrom(disabledForegroundColor: Colors.red),
onPressed: null,
child: Text('具有自定义禁用颜色的 ElevatedButton'),
)
要完全控制禁用的颜色,必须根据 MaterialStateProperties
明确定义 ElevatedButton
的样式:
RaisedButton(
disabledColor: Colors.red,
disabledTextColor: Colors.blue,
onPressed: null,
child: Text('具有自定义禁用颜色的 RaisedButton'),
)
ElevatedButton(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.resolveWith<Color?>(
(Set<MaterialState> states) {
if (states.contains(MaterialState.disabled))
return Colors.red;
return null; // 委托给部件的默认值。
}),
foregroundColor: MaterialStateProperty.resolveWith<Color?>(
(Set<MaterialState> states) {
if (states.contains(MaterialState.disabled))
return Colors.blue;
return null; // 委托给部件的默认值。
}),
),
onPressed: null,
child: Text('具有自定义禁用颜色的 ElevatedButton'),
)
与之前的案例一样,在经常出现此迁移的应用程序中,有明显的途径可以使新版本更紧凑。
迁移具有自定义高度的按钮
#这也是一个相对罕见的自定义。通常,只有 ElevatedButton
(最初称为 RaisedButtons
)包含高度变化。对于与基准高度成比例的高度(根据 Material Design 规范),可以非常简单地覆盖所有高度。
默认情况下,禁用按钮的高度为 0,其余状态相对于基准 2 定义:
disabled: 0
hovered 或 focused: baseline + 2
pressed: baseline + 6
因此,要迁移已定义所有高度的 RaisedButton
:
RaisedButton(
elevation: 2,
focusElevation: 4,
hoverElevation: 4,
highlightElevation: 8,
disabledElevation: 0,
onPressed: () { },
child: Text('具有自定义高度的 RaisedButton'),
)
ElevatedButton(
style: ElevatedButton.styleFrom(elevation: 2),
onPressed: () { },
child: Text('具有自定义高度的 ElevatedButton'),
)
要任意覆盖一个高度,例如按下的高度:
RaisedButton(
highlightElevation: 16,
onPressed: () { },
child: Text('具有自定义高度的 RaisedButton'),
)
ElevatedButton(
style: ButtonStyle(
elevation: MaterialStateProperty.resolveWith<double?>(
(Set<MaterialState> states) {
if (states.contains(MaterialState.pressed))
return 16;
return null;
}),
),
onPressed: () { },
child: Text('具有自定义高度的 ElevatedButton'),
)
迁移具有自定义形状和边框的按钮
#原始的 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'),
)
OutlinedButton(
style: OutlinedButton.styleFrom(
shape: StadiumBorder(),
side: BorderSide(
width: 2,
color: Colors.red
),
),
onPressed: () { },
child: Text('具有自定义形状和边框的 OutlinedButton'),
)
大多数新的 OutlinedButton
部件的样式参数(包括其形状和边框)都可以使用 MaterialStateProperty
值指定,也就是说,它们可以根据按钮的状态具有不同的值。要指定按钮按下时不同的边框颜色,请执行以下操作:
OutlineButton(
shape: StadiumBorder(),
highlightedBorderColor: Colors.blue,
borderSide: BorderSide(
width: 2,
color: Colors.red
),
onPressed: () { },
child: Text('具有自定义形状和边框的 OutlineButton'),
)
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'),
)
时间线
#包含的版本:1.20.0-0.0.pre
稳定版本:2.0.0
参考
#API 文档:
ButtonStyle
ButtonStyleButton
ElevatedButton
ElevatedButtonTheme
ElevatedButtonThemeData
OutlinedButton
OutlinedButtonTheme
OutlinedButtonThemeData
TextButton
TextButtonTheme
TextButtonThemeData
相关的 PR:
除非另有说明,否则本网站上的文档反映的是 Flutter 的最新稳定版本。页面最后更新于 2025-01-30。 查看源代码 或 报告问题。