Flutter 设计百科全书
主题、样式、颜色篇
前言
这篇文章的部分内容我从来没有在互联网上看到过
(至少中文互联网没有 > 😂)
(至少百度搜不到 > 😂)
点名批评
”谁会在意这个,这个东西(颜色)没人注意到,就这样吧“
”设计师(实习生)怎么设计,我就怎么做,最后不好看跟我前端没关系“
本章内容
-
颜色 Color 颜色的设计
阴影的设计 阴影的设计
-
主题 Theme 主题设计
Brightness ColorScheme ThemeExtension
-
样式 Style
ButtonStyle
其他组件的Style
TextStyle
TextSize 、 Color 、 TextStyle 冲突的问题
GetX 中如何使用Theme Style?
一个支持黑白主题的程序完整源码
Color 颜色的设计
虽然这篇文章主要讲的是主题样式,但是颜色却是最先要注意的地方。
- 不要使用 饱和度(鲜艳)过高的颜色
- 注意使用 反色,使用 对比度高的 反色,设置文本颜色
前面2个按钮的问题显而易见,为什么第三个按钮也有问题?
问题就是,不应该用黄色作为按钮的背景色。
卡片阴影的设计
看看谷歌设计的最早的阴影设计,
还有目前MD3的 Card(Flutter 自带组件)卡片质感,
猜猜看,为什么大家拒绝使用谷歌的规范,还不是这东西真太low了。
不是瞎吹,国内设计圈虽然起步慢,抄袭也多,但是不得不说,
对比欧美的 大圆角设计,真的好太多了。
咱们是顶尖的上限不够高,但是平均水平比国外强太多
- 目前大陆的设计圈,开始流行去阴影化,或弱阴影化
- 我记得看到别人说过的话,如果用户注意到阴影了,那么你的阴影就是失败的设计。
- 大陆的阴影设计 都是垂直投影,一般不会有XY轴偏移,如果你使用谷歌Card组件,用户会非常容易注意到差异。
弱阴影设计思路
弱化阴影的设计,让你的阴影没那么突兀,
可以将投影的颜色设置为无限接近于#FFFFFFFF,
常见参考值 可以使用 EE,FE,EF 等,235-245 区间。
范围可以控制在 5-10px像素
BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
boxShadow: const [
BoxShadow(
color: Color(0xFFEFEFEF),
blurRadius: 5,
blurStyle: BlurStyle.outer,
),
],
),
去阴影设计思路(目前最推荐 ♥♥♥♥♥)
越来越多的设计 使用“无边界”,“无卡片”,“无阴影” 的设计。
这种设计 非常的简洁,非常考验 布局能力。
做法就是 使用背景色和卡片 色差,产生阶梯感。
backgroundColor: const Color(0xFFFAFAFA), //背景白色偏灰
color: Colors.white, // 卡片白色
Theme 主题设计
谷歌的MD设计其实只考虑了少部分的按钮和配色,
但是他们没想到,国内的设计师,设计程序时,
能搞出10几种按钮 和 文字 大小颜色 😂
谷歌都气炸了。
所以在中文互联网上,你搜索Flutter 主题时,这块的内容多数是复制粘贴党,
并不是批评 到处复制粘贴的人,只是这种行为 让真正有独特理解的内容 更难被人看到。
谷歌的主题设计
在MaterialApp下面有常用的三个主题相关字段,
theme:白色主题
darkTheme:黑色主题
themeMode:主题模式
enum ThemeMode {
system,// 跟随系统变化
light,// 强制白色主题
dark,// 强制黑色主题
}
初始代码默认只设置了theme,不设置darkTheme,任何时候都不能切换到黑色模式,始终会显示白色主题。
当设置为system模式时,系统主题切换,App会跟随系统变化,这是一个比较优秀的做法。
MaterialApp的theme,是ThemeData,其中的ColorScheme,是Flutter 中的颜色配置信息, 默认拥有ColorScheme.light,ColorScheme.dark,ColorScheme.fromSeed,三种快速配置,比较适合不在意颜色的APP快速使用。
推荐白色主题使用 ColorScheme.light
推荐黑色主题使用 ColorScheme.dark
Brightness 全解析(部分机翻)
enum Brightness {
/// 例如,颜色可能是深灰色,需要白色文本。
dark,
/// 例如,颜色可能是亮白色,需要黑色文本
light,
}
Flutter中黑白相关的区分,都可以使用Brightness, 但是需要注意的是,有些地方Brightness可能是相反并且反直觉的。 比如在系统顶部状态栏中的Brightness和IconBrightness就是一个容易搞混的设置
SystemUiOverlayStyle(
statusBarBrightness: Brightness.light,//状态栏的主题
statusBarIconBrightness: Brightness.dark,//状态栏的图标主题,需要跟状态栏主题相反
)
关于 ColorScheme
- brightness 暗色或亮色
- primary 主要颜色
- onPrimary 在主要颜色上面的颜色
- secondary 次要颜色
- onSecondary 在次要颜色上面的颜色
- error 错误的颜色
- onError 在错误颜色上面的颜色
- surface 表面颜色
- onSurface 在表面颜色上面的颜色
- inverse 关键字:颜色反转,一般用于反色
默认的颜色设计,如果严格按照这种规范来,
程序中 只会有 几种颜色。
Flutter 颜色设置名称非常多 非常难以 运用。
如果你的程序颜色配置较少可以使用上面的关键字。 如果你的程序颜色配置较多,推荐使用ThemeData 的 extensions属性,类型为 ThemeExtension
我们先来讲默认的颜色配置,如果想看扩展主题,请跳到下方扩展主题章节。
我们来设计一套兼容黑白主题的主题吧,这是一个最简单的案例。
我们先给MaterialApp定义一个白色主题theme.
如果我们定义appBarTheme,代表App会在白色主题下 使用这个Appbar的主题设置。
同样的,如果我们在darkTheme中定义appBarTheme,那么黑色主题会使用它。
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
appBarTheme: const AppBarTheme(
backgroundColor: Colors.white,
foregroundColor: Colors.black,
elevation: 0,
scrolledUnderElevation: 0,
surfaceTintColor: Colors.black,
systemOverlayStyle: SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarBrightness: Brightness.light,
statusBarIconBrightness: Brightness.dark,
systemNavigationBarColor: Colors.white,
systemNavigationBarDividerColor: Colors.black,
),
),
接着是颜色配置
colorScheme: const ColorScheme(
brightness: Brightness.dark,
primary: AppColors.themeColor, // 主要颜色,一般是按钮上面会用
onPrimary: AppColors.themeTextColor, // 在主要颜色 上面,就是文字的颜色,我们用白色
secondary: Colors.grey, // 次要颜色,用灰色
onSecondary: Colors.white,// 在次要颜色 上面,就是文字的颜色,我们用白色色
error: Colors.red,//错误颜色
onError: Colors.white,// 错误颜色上面的文字颜色
surface: Colors.black, // 表面颜色
onSurface: Colors.white,// 表面上的文字
),
主题和Style不建议使用静态字段(将它们写到默认Theme或 ThemeExtension 扩展内部),但是颜色和数值类型可以考虑使用 静态字段,在 theme 和 style中引用这些静态变量达到复用的目的,比如
class AppColors {
/// 主题色
static const Color themeColor = Color(0xff00CC74);
/// 文字颜色
static const Color themeTextColor = Color(0xff5FB23F);
}
class AppSizes {
/// 按钮大小
static const int buttonSize = 48;
}
到这里,已经完成了颜色配置,flutter默认的组件style都会从里面取出颜色使用
/// 按钮会自动使用主题色,除非你定义了ButtonTheme中的ButtonStyle并修改了背景色
FilledButton(
onPressed: () {},
child: const Text("FilledButton"),
),
Switch(
value: false,
onChanged: (value) {},
),
Row(
children: [
const Chip(
label: Text("Chip"),
),
const ChoiceChip(label: Text("ChoiceChip"), selected: true),
Radio(
value: 1,
groupValue: 1,
onChanged: (value) {},
)
],
)
效果如下(图片和上方颜色代码不一致,仅供参考):
当然我们也可以直接取到这个颜色值使用。
/// 获取默认ThemeData下面的主题色
Theme.of(context).colorScheme.primary
/// 直接使用默认 Theme中的颜色
/// 当前 我建议你使用textTheme,而不是直接使用颜色值,具体内容查看本章内容
body: Text(
"body",
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
),
)
如何使用主题的颜色完成自定义样式的组件呢? -> 具体使用方法查看本章Style模块,
从上面的内容上你也看出来了,如果你的程序 颜色 非常单一,谷歌这套设计也是能够满足需求的,
可惜国内的设计环境就不是这样,各种颜色,各种尺寸,
这里有3种方案,
-
第一种是 跟设计师商量,减少 部分颜色 和 样式设计,让最终的按钮大概就是 2-3种 ,文字颜色 也是 2-3 种。
-
第二种是 全局静态Style
在静态类中,写固定函数,返回固定组件的样式和颜色,比如定义一个 BlackButton,在函数中申明它的背景色为黑色。这种做法它的缺陷就是无法跟随主题变化,如果你的程序只有一种主题,那么它也是一种可行的方案。 -
第三种是 使用扩展 ThemeExtension,查看下方。
扩展主题设计 ThemeExtension
/// 自定义的主题扩展
class DiyTheme extends ThemeExtension<DiyTheme> {
const DiyTheme({
required this.brandColor,
required this.danger,
required this.dangerTextStyle,
});
final Color? brandColor;
final Color? danger;
final TextStyle? dangerTextStyle;
@override
DiyTheme copyWith({Color? brandColor, Color? danger}) {
return DiyTheme(
brandColor: brandColor ?? this.brandColor,
danger: danger ?? this.danger,
dangerTextStyle : dangerTextStyle ?? this.dangerTextStyle,
);
}
@override
DiyTheme lerp(DiyTheme? other, double t) {
if (other is! DiyTheme) {
return this;
}
return DiyTheme(
brandColor: Color.lerp(brandColor, other.brandColor, t),
danger: Color.lerp(danger, other.danger, t),
dangerTextStyle : *** 省略,
);
}
@override
String toString() => 'DiyTheme(brandColor: $brandColor, danger: $danger)';
}
//创建方式
theme: ThemeData.light().copyWith(
extensions: <ThemeExtension<dynamic>>[
const DiyTheme(
brandColor: Color(0xFF1E88E5),
danger: Color(0xFFE53935),
dangerTextStyle : *** 省略
),
],
),
// 使用扩展颜色的方式
final DiyTheme diy = Theme.of(context).extension<DiyTheme>()!;
Color brandColor = diy.brandColor;
// 使用扩展主题中的样式
Text(
"自定义样式",
style: diy.dangerTextStyle,
),
Theme 和 Style 区别是什么?
区分一下 Theme 和 Style
在Flutter中,主题是一些列Style的组合。
所以 并不是换一个颜色 就是一个主题换肤(😀)。
让Theme获取颜色和属性配置,
从Theme中获取Style给组件。
ButtonStyle 如何设计和使用
在Flutter的主题设计中,你需要为最常用的按钮风格设置默认的主题,
总不可能 每写一个按钮 是使用 DiyTheme.buttonStyle吧?这样多麻烦不是吗?
FilledButton(
style: Theme.of(context).extension<DiyTheme>()!.buttonStyle,
onPressed: () {},
child: Text("按钮"),
)
如果所有的按钮都像上面这样写,开发者会疯掉,
我这里分享我推荐的做法,它不一定是最优,但是是我目前想到的比较好的解决方案。
- 第一步,我们需要对常用的几种按钮进行Style定义,常见的按钮有
- TextButton FilledButton ElevatedButton OutlinedButton IconButton
- 我们需要对 程序中最常用的按钮进行 样式编写,将它编写到默认的ThemeData中。
比如:
// 亮色 主题
theme: ThemeData(
colorScheme: const ColorScheme.light(),
appBarTheme: const AppBarTheme(),
textButtonTheme: TextButtonThemeData(
style: ButtonStyle(
splashFactory: NoSplash.splashFactory,
overlayColor: WidgetStatePropertyAll(Colors.transparent),
foregroundColor: WidgetStateProperty.resolveWith((state) {
if (state.contains(WidgetState.disabled)) {
return Colors.black26;
}
return AppColors.themeColor;
}),
),
),
filledButtonTheme: FilledButtonThemeData(
style: ButtonStyle(
shape: WidgetStatePropertyAll(
RoundedRectangleBorder(borderRadius: 8.borderRadius),
),
minimumSize: const WidgetStatePropertyAll(Size.fromHeight(60)),
backgroundColor: WidgetStateProperty.resolveWith((state) {
if (state.contains(WidgetState.disabled)) {
return AppColors.themeColor.withAlpha(80);
}
return AppColors.themeColor;
}),
foregroundColor: const WidgetStatePropertyAll(Colors.white),
),
),
),
上面的代码中,我们为 常用的2种按钮定义了外观,在我们日常使用中,
直接使用按钮 即可获得对应的外观
TextButton(
onPressed: () {},
child: Text("TextButton"),
),
FilledButton(
onPressed: () {},
child: Text("FilledButton"),
),
如果目前的按钮类型, 不足以满足你App的按钮需求,再根据对应的特性,将它们 扩展到相应按钮Style上,写入ThemeExtension中,参考上方ThemeExtension方法,使用时稍微麻烦一些,但是总体来说这不失为一种合理高效的方案。
FilledButton(
style: Theme.of(context).extension<DiyTheme>()!.buttonStyle,
onPressed: () {},
child: Text("FilledButton"),
),
buttonStyle 这个名字可以自己取,当然,需要和你团队定义规范,你们需要在各自使用的时候,从theme中提取样式使用,而不是自己写到行内或者自己单独定义到文件中。
你需要跟你的开发同伴 一起定义规范,大家都遵守这种规范。
想起了上次我给同事说要做这个事情,结果被他怼,”你想怎么写就怎么写,咱们各写各的,我们之前都这样的“,我竟无言以对。
其他组件的Style 如何设计和使用
//输入框参考:
theme: ThemeData(
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: const Color(0xFFEEEEEE),
hintStyle: const TextStyle(
height: 1,
fontSize: 18,
wordSpacing: 1,
letterSpacing: 1,
fontWeight: FontWeight.normal,
color: Color(0xFF999999),
),
border: OutlineInputBorder(
borderRadius: 8.borderRadius,
borderSide: BorderSide.none,
),
enabledBorder: OutlineInputBorder(
borderRadius: 8.borderRadius,
borderSide: BorderSide.none,
),
prefixIconConstraints: const BoxConstraints(
minWidth: 40,
),
suffixIconConstraints: const BoxConstraints(
minWidth: 50,
),
),
),
//Tabbar参考:
tabBarTheme: TabBarTheme(
dividerHeight: 0,
splashFactory: NoSplash.splashFactory,
overlayColor: WidgetStatePropertyAll(Colors.transparent),
labelStyle: TextStyle(fontWeight: FontWeight.w600),
//unselectedLabelStyle: TextStyle(),
),
上面都是默认的主题样式,当一种主题样式不够使用时,将其他样式写入扩展类型中。
几乎所有的组件都支持Style编写!!!
TextStyle 如何设计和使用
Flutter 中的TextStyle 可以说是最难理解和使用的参数了,如果说colorScheme复杂,至少你还能大概知道它会在什么地方出现,但是TextStyle,那就得一个个的尝试才知道它最终会在什么地方使用到,下面的内容可以收藏一波,基本上每一个项目可能都会用到这部分的文档内容,因为它太难记了!
TextTheme(
/// 最大的显示样式 ,作为屏幕上最大的文本 重要的文字或数字。
displayLarge: TextStyle(),
displayMedium: TextStyle(),
displaySmall: TextStyle(),
/// 标题样式 (小于display)
headlineLarge: TextStyle(),
headlineMedium: TextStyle(),
headlineSmall: TextStyle(),
/// 标题样式(小于headline)
titleLarge: TextStyle(), // AppBar Title (默认字体)
titleMedium: TextStyle(),
titleSmall: TextStyle(),
/// 正文样式
bodyLarge: TextStyle(), // 大 - 输入框 (默认字体)
bodyMedium: TextStyle(), // 字体 默认
bodySmall: TextStyle(), // 小 (部分水印提示信息)
/// 标签样式 标签样式是较小的实用样式,用于UI区域 例如组件内部的文本 或 内容正文,如标题。
labelLarge: TextStyle(), // 按钮 (默认字体)
labelSmall: TextStyle(),
labelMedium: TextStyle(),
)
同样的,为了简单,设计师 应该尽量将 字体的尺寸设计为 上方 15种之一,这样我们就方便使用了(想太多。。。哈哈) 😀
当然如果上面的文字样式 使用起来过于复杂难以记忆,我们依然可以将textStyle放入扩展Theme,使用自定义的名字,就会简单很多。
但是我们遇到修改 某些 组件的字体样式时,先看看默认的textTheme是否能够很好的修改到它(这么做优先级比自定义高)。
关于textTheme 的一些使用案例:
/// 特定的区域中使用时,不需要申明对应的Style
appBar: AppBar(
title: Text("appBar"),// 自动使用textTheme.titleLarge
),
body: Text("body"), // 自动使用textTheme.bodyMedium
/// 你想使用更大字体时样式时
body: Text(
"body",
style: Theme.of(context).textTheme.bodyLarge, // 申明使用textTheme.bodyLarge
),
TextSize 、 Color 、 TextStyle 冲突的问题。
仔细阅读之前的文章,再经过思考后,你就会发现,我们定义了textTheme,ButtonTheme又可以定义 textStyle,这个时候不是冲突了吗?
其实的确有冲突,但是Flutter讲的是就近原则,也就是说 按钮内的text在寻找样式时,如果你写了行内Style,它直接就用了,如果没有,那它会先寻找buttonTheme中的textStyle,如果没有定义,它才会去找textTheme中对应的textStyle,textStyle如果你没有修改,它就用的初始值,整体理解起来也是非常容易。
所以,在按钮的主题Style中,定义文字颜色时,应该放到foregroundColor,而不是定义TextStyle中的Color
你没有在按钮中定义文字大小,它也会去textTheme中获取 默认的文字大小。
不建议在textTheme(或扩展Theme)之外的区域定义文字大小和颜色属性
GetX 中如何使用?
在Getx中,你可以通过Get快速访问theme对象。
Get.theme.colorScheme
Get.theme.extensions
需要注意的是,如果你使用system模式的主题模式,系统主题发生变化时,Get使用的context并不能从当前的主题变化中获得消息,导致界面不会重新渲染(猜测的😔)。
所以建议使用BuildContext context来进行theme获取,这样你可能需要在页面的函数之间传递这个context 或 传递 themeData,如果读到这里,你有更好的方案,请在评论区留言讨论。
一个支持黑白主题的程序完整源码
(有可能你在阅读本文章时,这个源码还没有编写完成,但是你可以先关注一波。) github.com/DMSkin/flut…