概述

#

Flutter 中新增了一套基本的 Material 按钮组件和小部件主题。原始类已被弃用,并最终将被移除。总体目标是使按钮更灵活,并且更易于通过构造函数参数或主题进行配置。

FlatButtonRaisedButtonOutlineButton 小部件已分别被 TextButtonElevatedButtonOutlinedButton 替代。每个新的按钮类都有自己的主题:TextButtonThemeElevatedButtonThemeOutlinedButtonTheme。原始的 ButtonTheme 类已不再使用。按钮的外观由 ButtonStyle 对象指定,而非一大堆小部件参数和属性。这大致类似于使用 TextStyle 对象定义文本外观的方式。新的按钮主题也通过 ButtonStyle 对象进行配置。ButtonStyle 本身只是一组视觉属性的集合。这些属性中的许多都使用 MaterialStateProperty 定义,这意味着它们的值可以取决于按钮的状态。

背景

#

我们没有尝试就地演进现有的按钮类及其主题,而是引入了新的替代按钮小部件和主题。除了使我们摆脱了就地演进现有类所带来的向后兼容性困境之外,新名称还使 Flutter 与 Material Design 规范保持同步,该规范使用了按钮组件的新名称。

旧小部件旧主题新小部件新主题
FlatButtonButtonTheme文本按钮TextButtonTheme
RaisedButtonButtonTheme凸起按钮ElevatedButtonTheme
OutlineButtonButtonThemeOutlinedButtonOutlinedButtonTheme

新主题遵循了 Flutter 大约一年前为新的 Material 小部件采用的“规范化”模式。主题属性和小部件构造函数参数默认为 null。非 null 的主题属性和小部件参数指定了对组件默认值的覆盖。实现和记录默认值是按钮组件小部件的唯一责任。默认值本身主要基于整体 Theme 的 colorScheme 和 textTheme。

在视觉上,新按钮看起来略有不同,因为它们符合当前的 Material Design 规范,并且它们的颜色是根据整体 Theme 的 ColorScheme 配置的。在内边距、圆角半径以及悬停/焦点/按下反馈方面也有其他细微差别。

许多应用程序将能够直接用新类名替换旧类名。带有黄金图像测试或按钮外观已通过构造函数参数或原始 ButtonTheme 配置的应用程序可能需要查阅迁移指南和随后的介绍材料。

API 变更:使用 ButtonStyle 而非单独的样式属性

#

除了简单的用例,新按钮类的 API 与旧类不兼容。新按钮和主题的视觉属性通过单个 ButtonStyle 对象进行配置,类似于 TextFieldText 小部件可以使用 TextStyle 对象进行配置的方式。大多数 ButtonStyle 属性都使用 MaterialStateProperty 定义,因此单个属性可以根据按钮的按下/聚焦/悬停/等状态表示不同的值。

按钮的 ButtonStyle 不定义按钮的视觉属性,它定义了对按钮默认视觉属性的覆盖,其中默认属性由按钮小部件本身计算。例如,要覆盖 TextButton 在所有状态下的默认前景(文本/图标)颜色,可以这样编写

dart
TextButton(
  style: ButtonStyle(
    foregroundColor: MaterialStateProperty.all<Color>(Colors.blue),
  ),
  onPressed: () { },
  child: Text('TextButton'),
)

这种覆盖很常见;然而,在许多情况下,还需要覆盖文本按钮用于指示其悬停/焦点/按下状态的覆盖颜色。这可以通过将 overlayColor 属性添加到 ButtonStyle 来实现。

dart
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,则将使用小部件的默认值。例如,只覆盖文本按钮的焦点覆盖颜色

dart
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 规范中的不透明度来覆盖其前景颜色和覆盖颜色。

dart
TextButton(
  style: TextButton.styleFrom(
    foregroundColor: Colors.blue,
  ),
  onPressed: () { },
  child: Text('TextButton'),
)

TextButton 文档指出,当按钮被禁用时,前景颜色基于配色方案的 disabledForegroundColor 颜色。要使用 styleFrom() 也覆盖它,可以这样

dart
TextButton(
  style: TextButton.styleFrom(
    foregroundColor: Colors.blue,
    disabledForegroundColor: Colors.red,
  ),
  onPressed: null,
  child: Text('TextButton'),
)

如果您正在尝试创建 Material Design 变体,使用 styleFrom() 方法是创建 ButtonStyle 的首选方式。最灵活的方法是直接定义 ButtonStyle,其中包含您想要覆盖外观的状态的 MaterialStateProperty 值。

ButtonStyle 默认值

#

新的按钮类等小部件根据整体主题的 colorSchemetextTheme 以及按钮的当前状态来 *计算* 其默认值。在少数情况下,它们还会考虑整体主题的配色方案是亮色还是暗色。每个按钮都有一个受保护的方法,可以根据需要计算其默认样式。尽管应用程序不会直接调用此方法,但其 API 文档解释了所有默认值。当按钮或按钮主题指定 ButtonStyle 时,只有按钮样式中的非 null 属性会覆盖计算出的默认值。按钮的 style 参数会覆盖相应按钮主题中指定的非 null 属性。例如,如果 TextButton 样式的 foregroundColor 属性为非 null,它将覆盖 TextButtonTheme 样式的相同属性。

如前所述,每个按钮类都包含一个名为 styleFrom 的静态方法,它从一组简单的值(包括其所依赖的 ColorScheme 颜色)构造一个 ButtonStyle。在许多常见情况下,使用 styleFrom 创建一个覆盖默认值的一次性 ButtonStyle 是最简单的。当自定义样式旨在覆盖配色方案中的某个颜色(例如默认样式所依赖的 primaryonPrimary)时,尤其如此。对于其他情况,您可以直接创建一个 ButtonStyle 对象。这样做使您能够控制按钮所有可能状态(如按下、悬停、禁用和聚焦)下的视觉属性(如颜色)的值。

迁移指南

#

使用以下信息将您的按钮迁移到新的 API。

恢复原始按钮的视觉效果

#

在许多情况下,可以直接从旧按钮类切换到新按钮类。这假定尺寸/形状的微小变化以及颜色上可能更大的变化不是问题。

为了在这些情况下保留原始按钮的外观,您可以定义尽可能与原始按钮匹配的按钮样式。例如,以下样式使 TextButton 看起来像默认的 FlatButton

dart
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

dart
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'),
)

OutlinedButtonOutlineButton 样式稍微复杂一些,因为当按钮被按下时,轮廓的颜色会变为主题色。轮廓的外观由 BorderSide 定义,您将使用 MaterialStateProperty 来定义按下时的轮廓颜色

dart
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'),
)

要恢复整个应用程序中按钮的默认外观,您可以在应用程序的主题中配置新的按钮主题

dart
MaterialApp(
  theme: ThemeData.from(colorScheme: ColorScheme.light()).copyWith(
    textButtonTheme: TextButtonThemeData(style: flatButtonStyle),
    elevatedButtonTheme: ElevatedButtonThemeData(style: raisedButtonStyle),
    outlinedButtonTheme: OutlinedButtonThemeData(style: outlineButtonStyle),
  ),
)

要在应用程序的某一部分中恢复按钮的默认外观,您可以使用 TextButtonThemeElevatedButtonThemeOutlinedButtonTheme 包装小部件子树。例如

dart
TextButtonTheme(
  data: TextButtonThemeData(style: flatButtonStyle),
  child: myWidgetSubtree,
)

迁移带有自定义颜色的按钮

#

以下部分介绍如何使用以下 FlatButtonRaisedButtonOutlineButton 颜色参数

dart
textColor
disabledTextColor
color
disabledColor
focusColor
hoverColor
highlightColor*
splashColor

新的按钮类不支持单独的高亮颜色,因为它不再是 Material Design 的一部分。

迁移带有自定义前景和背景颜色的按钮

#

原始按钮类的两种常见自定义是 FlatButton 的自定义前景颜色,或 RaisedButton 的自定义前景和背景颜色。使用新按钮类实现相同结果很简单

dart
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

dart
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 是前景(文本/图标)颜色。

迁移带有自定义覆盖颜色的按钮

#

覆盖按钮的默认聚焦、悬停、高亮或波纹颜色不那么常见。FlatButtonRaisedButtonOutlineButton 类为这些依赖于状态的颜色提供了单独的参数。新的 TextButtonElevatedButtonOutlinedButton 类则使用单个 MaterialStateProperty<Color> 参数。新按钮允许为所有颜色指定依赖于状态的值,而原始按钮只支持指定现在称为“overlayColor”的颜色。

dart
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——并只使用样式参数——或者定义一个封装了三个颜色参数的无状态包装器小部件。

迁移带有自定义禁用颜色的按钮

#

这是一种相对罕见的自定义。FlatButtonRaisedButtonOutlineButton 类具有 disabledTextColordisabledColor 参数,它们定义当按钮的 onPressed 回调为 null 时的背景和前景颜色。

默认情况下,所有按钮都使用配色方案的 disabledForegroundColor 颜色,禁用前景色的不透明度为 0.38。只有 ElevatedButton 具有非透明背景颜色,其默认值是 disabledForegroundColor 颜色,不透明度为 0.12。因此在许多情况下,只需使用 styleFrom 方法即可覆盖禁用颜色

dart
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 的样式

dart
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 定义

dart
disabled: 0
hovered or focused: baseline + 2
pressed: baseline + 6

因此,要迁移已定义所有高程的 RaisedButton

dart
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'),
)

要任意覆盖其中一个高程,例如按下时的高程

dart
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'),
)

迁移带有自定义形状和边框的按钮

#

原始的 FlatButtonRaisedButtonOutlineButton 类都提供了一个 shape 参数,该参数定义了按钮的形状及其轮廓的外观。相应的新类及其主题支持使用 OutlinedBorder shapeBorderSide side 参数分别指定按钮的形状和边框。

在此示例中,原始 OutlineButton 版本为其高亮(按下)状态的边框指定了与其它状态相同的颜色。

dart
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 值指定,也就是说它们可以根据按钮的状态具有不同的值。要在按钮按下时指定不同的边框颜色,请执行以下操作

dart
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 文档

相关 PR