Skip to main content

新的按钮和按钮主题

摘要

#

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

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

背景

#

与其尝试就地演化现有的按钮类及其主题,我们引入了新的替代按钮部件和主题。除了让我们免于就地演化现有类所带来的向后兼容性难题之外,新的名称还使 Flutter 与 Material Design 规范同步,该规范使用新的名称来表示按钮组件。

旧部件旧主题新部件新主题
FlatButtonButtonThemeTextButtonTextButtonTheme
RaisedButtonButtonThemeElevatedButtonElevatedButtonTheme
OutlineButtonButtonThemeOutlinedButtonOutlinedButtonTheme

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

从视觉上看,新的按钮外观略有不同,因为它们符合当前的 Material Design 规范,并且它们的颜色是根据整体主题的 ColorScheme 配置的。填充、圆角半径以及悬停/焦点/按下反馈方面也存在其他细微差异。

许多应用程序只需将新的类名替换为旧的类名即可。具有黄金图像测试或其外观已使用构造函数参数或原始 ButtonTheme 配置的按钮的应用程序可能需要查阅迁移指南和后续的介绍性资料。

API 变更:使用 ButtonStyle 代替单个样式属性

#

除了简单的用例外,新按钮类的 API 与旧类不兼容。新按钮和主题的视觉属性使用单个 ButtonStyle 对象进行配置,类似于如何使用 TextStyle 对象配置 TextFieldText 部件。大多数 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; // 委托给部件的默认值。
      },
    ),
  ),
  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; // 委托给部件的默认值。
      }
    ),
  ),
  onPressed: () { },
  child: Text('TextButton'),
)

styleFrom() ButtonStyle 工具方法

#

Material Design 规范根据颜色方案的主颜色定义按钮的前景色和叠加颜色。根据按钮的状态,主颜色会以不同的不透明度呈现。为了简化创建包含所有依赖于颜色方案颜色的属性的按钮样式,每个按钮类都包含一个静态 styleFrom() 方法,该方法从一组简单的值构造 ButtonStyle,包括它所依赖的 ColorScheme 颜色。

此示例创建一个按钮,它使用指定的主颜色和 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 的,则它会覆盖 TextButonTheme 样式的同一属性。

如前所述,每个按钮类都包含一个名为 styleFrom 的静态方法,该方法从一组简单的值构造 ButtonStyle,包括它所依赖的 ColorScheme 颜色。在许多常见情况下,使用 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,
)

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

#

以下部分涵盖以下 FlatButton 的使用,

迁移具有自定义颜色参数的 FlatButtonRaisedButtonOutlineButton

#
dart
textColor
disabledTextColor
color
disabledColor
focusColor
hoverColor
highlightColor*
splashColor

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

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

#

原始按钮类的两种常见自定义方式是 FlatButton 的自定义前景色,或 RaisedButton 的自定义前景色和背景色。使用新的按钮类可以轻松实现相同的结果:

dart
FlatButton(
  textColor: Colors.red, // 前景色
  onPressed: () { },
  child: Text('具有自定义前景色/背景色的 FlatButton'),
)

TextButton(
  style: TextButton.styleFrom(
    foregroundColor: Colors.red,
  ),
  onPressed: () { },
  child: Text('具有自定义前景色的 TextButton'),
)

在这种情况下,TextButton 的前景色(文本/图标)颜色及其悬停/聚焦/按下叠加颜色将基于 Colors.red。默认情况下,TextButton 的背景填充颜色是透明的。

迁移具有自定义前景色和背景色的 RaisedButton

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

迁移具有自定义叠加颜色的按钮

#

覆盖按钮的默认焦点、悬停、高亮或飞溅颜色并不常见。FlatButtonRaisedButtonOutlineButton 类为这些状态相关的颜色提供了单独的参数。新的 TextButtonElevatedButtonOutlinedButton 类则使用单个 MaterialStateProperty<Color> 参数。新的按钮允许指定所有颜色的状态相关值,而原始按钮仅支持指定现在称为“overlayColor”的内容。

dart
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 参数 - 或定义一个封装了三个颜色参数的无状态包装器部件。

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

#

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

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

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

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

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

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

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

要任意覆盖一个高度,例如按下的高度:

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

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

#

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

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

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

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

相关的 PR: