新按钮和按钮主题
概述
#Flutter 新增了一套基本的 Material Design 按钮组件和主题。旧的类已被弃用,并将最终被移除。总体目标是使按钮更加灵活,并且可以通过构造函数参数或主题轻松配置。
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 的主题属性和组件参数指定了对组件默认值的覆盖。实现和记录默认值是按钮组件的唯一职责。默认值主要基于整体主题的 colorScheme 和 textTheme。
在视觉上,新按钮看起来略有不同,因为它们符合当前的 Material Design 规范,并且它们的颜色是根据整体主题的 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'),
)
ButtonStyle 的 styleFrom() 实用方法
#Material Design 规范根据 colorScheme 的 primary color 定义了按钮的前景和叠加颜色。primary color 会根据按钮的状态以不同的不透明度渲染。为了简化创建包含所有依赖于 colorScheme 颜色的属性的按钮样式,每个按钮类都包含一个静态的 styleFrom() 方法,该方法从一组简单值(包括它依赖的 ColorScheme
颜色)构建一个 ButtonStyle
。
此示例创建了一个按钮,该按钮使用指定的 primary color 和 Material Design 规范中的不透明度来覆盖其前景颜色和叠加颜色。
TextButton(
style: TextButton.styleFrom(
foregroundColor: Colors.blue,
),
onPressed: () { },
child: Text('TextButton'),
)
TextButton
文档表明,按钮被禁用时的前景颜色基于 colorScheme 的 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
以及按钮的当前状态来*计算*其默认值。在少数情况下,它们还会考虑整体主题的 color scheme 是浅色还是深色。每个按钮都有一个受保护的方法,该方法会在需要时计算其默认样式。虽然应用程序不会直接调用此方法,但其 API 文档解释了所有默认值。当按钮或按钮主题指定 ButtonStyle
时,只有按钮样式的非 null 属性会覆盖计算出的默认值。按钮的 style
参数会覆盖相应按钮主题指定的非 null 属性。例如,如果 TextButton
样式的 foregroundColor
属性非 null,它将覆盖 TextButonTheme
样式的相同属性。
如前所述,每个按钮类都包含一个名为 styleFrom
的静态方法,该方法从一组简单值(包括它依赖的 ColorScheme
颜色)构建一个 ButtonStyle。在许多常见情况下,使用 styleFrom
创建一个覆盖默认值的单次 ButtonStyle
是最简单的。当自定义样式的目的是覆盖默认样式所依赖的 color scheme 颜色(如 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
样式稍微复杂一些,因为当按钮被按下时,边框的颜色会变为 primary color。边框的外观由 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
新按钮类不再支持单独的 highlight color,因为它已不再是 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
颠倒了 colorScheme 的 primary color 的使用: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'),
)
新版本更灵活,尽管不那么简洁。在旧版本中,不同状态的优先级是隐含的(且未记录)和固定的,而在新版本中,它是显式的。对于频繁指定这些颜色的应用程序,最简单的迁移路径是定义一个或多个 ButtonStyles
来匹配上例,然后只使用 style 参数,或者定义一个无状态包装器小部件来封装三个颜色参数。
迁移具有自定义禁用颜色的按钮
#这是一种相对少见的自定义。FlatButton
、RaisedButton
和 OutlineButton
类具有 disabledTextColor
和 disabledColor
参数,它们定义了当按钮的 onPressed
回调为 null 时的背景和前景颜色。
默认情况下,所有按钮都使用 colorScheme 的 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