本篇分享一个简单轻松的内容: ​​ThemeExtensions ​​ 和 ​​Material3​ ,它们都是 Flutter 3.0 中的重要组成部分,相信后面的小知识你可能还没了解过~。

ThemeExtensions

相信大家都用过 Flutter 里的 ​​ Theme​​​ ,在 Flutter 里可以通过修改全局的 ​​ ThemeData​​​ 就来实现一些样式上的调整,比如 :全局去除 ​​InkWell​​​ 和 ​​TextButton​​ 的点击效果。

theme: ThemeData(
primarySwatch: Colors.blue,
// 去掉 InkWell 的点击水波纹效果
splashFactory: NoSplash.splashFactory,
// 去除 InkWell 点击的 highlight
highlightColor: Colors.transparent,
textButtonTheme: TextButtonThemeData(
// 去掉 TextButton 的水波纹效果
style: ButtonStyle(splashFactory: NoSplash.splashFactory),
),
),

当然,开发者也可以通过 ​​Theme.of(context)​​​ 去读取 ​​ThemeData​​ 的一些全局样式,从而让自己的控件配置更加灵活,但是如果 ​​ThemeData​​ 里没有符合你需求的参数,或者你希望这个参数只被特定控件是用,那该怎么办

Flutter 3 给我们提供了一个解决方案: ​​ThemeExtensions ​​ 。

开发者可以通过继承 ​​ThemeExtension​​​ 并 override 对应的 ​​copyWith​​​ 和 ​​lerp​​​ 方法来自定义需要拓展的 ​​ThemeData​​ 参数,比如这样:

@immutable
class StatusColors extends ThemeExtension<StatusColors> {
static const light = StatusColors(open: Colors.green, closed: Colors.red);
static const dark = StatusColors(open: Colors.white, closed: Colors.brown);

const StatusColors({required this.open, required this.closed});

final Color? open;
final Color? closed;

@override
StatusColors copyWith({
Color? success,
Color? info,
}) {
return StatusColors(
open: success ?? this.open,
closed: info ?? this.closed,
);
}

@override
StatusColors lerp(ThemeExtension<StatusColors>? other, double t) {
if (other is! StatusColors) {
return this;
}
return StatusColors(
open: Color.lerp(open, other.open, t),
closed: Color.lerp(closed, other.closed, t),
);
}

@override
String toString() => 'StatusColors('
'open: $open, closed: $closed'
')';
}

之后就可以将上面的 ​​StatusColors​​​ 配置到 ​​Theme​​​ 的 ​​extensions​​​ 上,然后通过 ​​ Theme.of(context).extension<StatusColors>()​​ 读取配置的参数。

theme: ThemeData(
primarySwatch: Colors.blue,
extensions: <ThemeExtension<dynamic>>[
StatusColors.light,
],
),

·····

@override
Widget build(BuildContext context) {

/// get status color from ThemeExtensions
final statusColors = Theme.of(context).extension<StatusColors>();

return Scaffold(
extendBody: true,
body: Container(
alignment: Alignment.center,
child: new ElevatedButton(
style: TextButton.styleFrom(
backgroundColor: statusColors?.open,
),
onPressed: () {},
child: new Text("Button")),
),
);
}

是不是很简单?通过 ​​ThemeExtensions ​​ ,第三方 package 在编写控件时,也可以提供对应的 ​​ThemeExtensions​​ 对象,实现更灵活的样式配置支持

Material3

Material3 又叫 MaterialYou , 是谷歌在 Android 12 时提出的全新 UI 设计规范,现在 Flutter 3.0 里你可以通过 ​​useMaterial3: true​​ 打开配置支持。

theme: ThemeData(
primarySwatch: Colors.blue,
///打开 useMaterial3 样式
useMaterial3: true,
),

当然,在你开启 Material3 之前,你需要对它有一定了解,因为它对 UI 风格的影响还是很大的,知己知彼才能不被背后捅刀

如下图所示,是在 ​​primarySwatch: Colors.blue​​​ 的情况下,​​AppBar​​​ 、​​Card​​​、​​TextButton​​​、 ​​ElevatedButton​​ 的样式区别:

Flutter 小技巧之 Flutter 3 下的 ThemeExtensions 和 Material3_Android

可以看到圆角和默认的颜色都发生了变化,并且除了 UI 更加圆润之外,交互效果也发生了一些改变,比如:

  • 点击效果和​​Dialog​​ 的默认样式都发生了变化;
  • Android 上列表滚动的默认​​OverscrollIndicator​​ 效果也发生了改变;

交互

列表

Flutter 小技巧之 Flutter 3 下的 ThemeExtensions 和 Material3_flutter_02

Flutter 小技巧之 Flutter 3 下的 ThemeExtensions 和 Material3_sed_03

目前在 Flutter 3 中受到 ​​useMaterial3​​ 影响的主要有以下这些 Widget ,可以看到主要影响的还是具有交互效果的 Widget 居多:

  • [AlertDialog]
  • [AppBar]
  • [Card]
  • [Dialog]
  • [ElevatedButton]
  • [FloatingActionButton]
  • [Material]
  • [NavigationBar]
  • [NavigationRail]
  • [OutlinedButton]
  • [StretchingOverscrollIndicator]
  • [GlowingOverscrollIndicator]
  • [TextButton]

那 Material3 和之前的 Material2 有什么区别呢

以 ​​AppBar​​​ 举例,可以看到在 M2 和 M3 中背景颜色的获取方式就有所不同,在 M3 下没有了 ​​Brightness.dark​​ 的判断,那是说明 M3 不支持暗黑模式吗?

Flutter 小技巧之 Flutter 3 下的 ThemeExtensions 和 Material3_Flutter_04

回答这个问题之前,我们先看 ​​_TokeDefaultsM3​​​ 有什么特别之处,从源码注释里可以看到 ​​_TokeDefaultsM3​​​ 是通过脚本自动生成,并且目前版本号是 ​​v0_92​​ ,所以 M3 和 M2 最大的不同之一就是它的样式代码现在是自动生成

Flutter 小技巧之 Flutter 3 下的 ThemeExtensions 和 Material3_Flutter_05

在 Flutter 的 gen_defaults​ 下就可以看到,基本上涉及 M3 的默认样式,都是通过 ​​data​​​ 下的数据利用模版自动生成,比如 ​​Appbar​​​ 的 ​​backgroundColor​​​ 指向的就是 ​​surface​​ 。

Flutter 小技巧之 Flutter 3 下的 ThemeExtensions 和 Material3_flutter_06

而之所以 M3 的默认样式不再需要 ​​Brightness.dark​​ 的判断,是因为在 M3 使用的 ​​ColorScheme​​ 里已经做了判断

Flutter 小技巧之 Flutter 3 下的 ThemeExtensions 和 Material3_iOS_07

事实上现在 Flutter 3.0 里 ​​colorScheme​​ 才是主题颜色的核心,而 ​​primaryColorBrightness​​ 和 ​​primarySwatch​​ 等参数在未来将会被弃用,所以如果目前你还在使用 ​​primarySwatch​​​ ,在 ​​ThemeData​​​ 内部会通过 ​​ColorScheme.fromSwatch​​​ 方法转换为 ​​ColorScheme​​ 。

ColorScheme.fromSwatch(
primarySwatch: primarySwatch,
primaryColorDark: primaryColorDark,
accentColor: accentColor,
cardColor: cardColor,
backgroundColor: backgroundColor,
errorColor: errorColor,
brightness: effectiveBrightness,
);

另外你也可以通过 ​​ColorScheme.fromSeed​​​ 或者 ​​colorSchemeSeed ​​​ 来直接配置 ​​ThemeData​​​ 里的 ​​ColorScheme​​ ,那 ​​ColorScheme​​ 又是什么

theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Color(0xFF4285F4)),
///打开 useMaterial3 样式
useMaterial3: true,
),

这里其实就涉及到一个很有趣的知识点:Material3 下的 HCT 颜色包: ​material-color-utilities

在 Material3 下颜色其实不是完全按照 RGB 去计算,而是会经过 material-color-utilities​ 的转化,通过内部的 ​​CorePalette​​ 对象,RGB 会转化为 HCT 相关的值去计算显示。

Flutter 小技巧之 Flutter 3 下的 ThemeExtensions 和 Material3_Android_08

对于 HCT 其实是 Hue、Chroma、Tone 三个单词的缩写,可以解释为色相、色度和色调,通过谷歌开源的 material-color-utilities 插件就可以方便实现 HCT 颜色空间的接入,目前该 repo 已支持 Dart、Java 和 Typecript 等语言,另外 C/C++ 和 Object-C 也在即将支持。

Flutter 小技巧之 Flutter 3 下的 ThemeExtensions 和 Material3_Android_09

得益于 HCT ,例如我们前面的 ​​ColorScheme.fromSeed(seedColor: Color(0xFF4285F4))​​,就可以通过一个 seedColor 直接生成一系列主题颜色,这就是 Material3 里可以拥有更丰富的主题色彩的原因。

Flutter 小技巧之 Flutter 3 下的 ThemeExtensions 和 Material3_iOS_10


更多可见 《HCT 的色彩原理》


最后

最后我们回顾一下,今天的小技巧有:

  • 通过​​ThemeExtensions​​​ 拓展想要的自定义​​ThemeData​
  • 通过​​useMaterial3​​​ 启用 Material3 ,并通过​​ColorScheme​​ 配置更丰富的 HCT 颜色

好了,现在你可以去问你的设计师:你知道什么是 HCT 么?