新的按钮和按钮主题
摘要
#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。 查看源代码 或 报告问题。