跳至主要内容

新按钮和按钮主题

摘要

#

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

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

上下文

#

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

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

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

从视觉上看,新的按钮看起来略有不同,因为它们与当前的 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; // 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() 方法,该方法从一组简单的值构建 ButtonStyle,包括它所依赖的 ColorScheme 颜色。

此示例创建了一个按钮,该按钮使用指定的主题颜色和 Material Design 规范中的不透明度覆盖其前景色及其覆盖颜色。

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

TextButton 文档指出,按钮禁用时的前景色基于颜色方案的 onSurface 颜色。要使用 styleFrom() 覆盖它:

dart
TextButton(
  style: TextButton.styleFrom(
    primary: Colors.blue,
    onSurface: 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 的静态方法,该方法从一组简单的值(包括它所依赖的 ColorScheme 颜色)构建 ButtonStyle。在许多常见情况下,使用 styleFrom 创建一个覆盖默认值的临时 ButtonStyle 最简单。当自定义样式的目标是覆盖默认样式所依赖的颜色方案颜色(如 primaryonPrimary)时,尤其如此。对于其他情况,您可以直接创建 ButtonStyle 对象。这样做使您能够控制所有按钮可能的状态(如按下、悬停、禁用和聚焦)的视觉属性(如颜色)的值。

迁移指南

#

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

恢复原始按钮视觉效果

#

在许多情况下,只需从旧按钮类切换到新按钮类即可。假设大小/形状的细微变化以及颜色可能发生的较大变化不是问题。

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

dart
final ButtonStyle flatButtonStyle = TextButton.styleFrom(
  primary: 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(
  onPrimary: Colors.black87,
  primary: 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(
    primary: Colors.red, // foreground
  ),
  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(
    primary: Colors.red, // background
    onPrimary: Colors.white, // foreground
  ),
  onPressed: () { },
  child: Text('ElevatedButton with custom foreground/background'),
)

在这种情况下,按钮对颜色方案的主色的使用与 TextButton 相反:主色是按钮的背景填充颜色,onPrimary 是前景色(文本/图标)颜色。

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

#

覆盖按钮的默认聚焦、悬停、高亮或水波纹颜色不太常见。FlatButtonRaisedButtonOutlineButton 类具有这些状态相关颜色的单独参数。新的 TextButtonElevatedButtonOutlinedButton 类改为使用单个 MaterialStateProperty 参数。新的按钮允许指定所有颜色的状态相关值,原始按钮仅支持指定现在称为“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'),
)

新版本虽然不太紧凑,但更灵活。在原始版本中,不同状态的优先级是隐式(且未记录)且固定的,在新版本中,它是显式的。对于经常指定这些颜色的应用程序,最简单的迁移路径是定义一个或多个与上述示例匹配的 ButtonStyles - 并只使用样式参数 - 或定义一个无状态包装部件来封装三个颜色参数。

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

#

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

默认情况下,所有按钮都使用颜色方案的 onSurface 颜色,禁用前景色不透明度为 0.38。只有 ElevatedButton 具有非透明背景颜色,其默认值为 onSurface 颜色,不透明度为 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(onSurface: 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