颜色

在介绍主题前我们先了解一些Flutter中的 Color类
Color类 中颜色以一个int值保存,我们知道显示器颜色是由 红、绿、蓝 三基色组成,每种颜色占 8比特,存储结构如下:

Bit(位) 颜色
0-7 蓝色
8-15 绿色
16-23 红色
24-31 Alpha (不透明度)

上面表格中的的字段在 Color类 中都有对应的属性,而Color中的众多方法也就是操作这些属性的,由于大多比较简单,读者可以查看类定义了解。在此我们主要讨论两点:色值转换和亮度

如何将颜色字符串转成Color对象

如Web开发中的色值通常是一个字符串如”#dc380d”,它是一个RGB值,我们可以通过下面这些方法将其转为Color类:

  1. Color(0xffdc380d); //如果颜色固定可以直接使用整数值
  2. //颜色是一个字符串变量
  3. var c = "dc380d";
  4. Color(int.parse(c,radix:16)|0xFF000000) //通过位运算符将Alpha设置为FF
  5. Color(int.parse(c,radix:16)).withAlpha(255) //通过方法将Alpha设置为FF

理解JS中(2**24-1).toString(16)

颜色亮度

假如,我们要实现一个背景颜色和Title可以自定义的导航栏,并且

  • 背景色为深色时我们应该让Title显示为浅色;
  • 背景色为浅色时,Title显示为深色。要实现这个功能

我们就需要来计算背景色的亮度,然后动态来确定Title的颜色。Color类中提供了一个computeLuminance()方法,它可以返回一个[0-1]的一个值,数字越大颜色就越浅,我们可以根据它来动态确定Title的颜色,下面是导航栏NavBar的简单实现:

  1. import 'package:flutter/material.dart';
  2. void main() => runApp(new MyApp());
  3. class MyApp extends StatelessWidget {
  4. @override
  5. Widget build(BuildContext context) {
  6. return MaterialApp(
  7. debugShowCheckedModeBanner: true,
  8. title: '颜色亮度',
  9. theme: ThemeData(
  10. primarySwatch: Colors.blue,
  11. ),
  12. home: Scaffold(
  13. appBar: AppBar(title: Text("颜色亮度")),
  14. body: TestComputeLuminance(),
  15. ));
  16. }
  17. }
  18. class NavBar extends StatelessWidget {
  19. final String title;
  20. final Color color; //背景颜色
  21. NavBar({
  22. Key key,
  23. this.color,
  24. this.title,
  25. });
  26. @override
  27. Widget build(BuildContext context) {
  28. return Container(
  29. alignment: Alignment.centerLeft,
  30. padding: EdgeInsets.all(10.0),
  31. constraints: BoxConstraints(
  32. minHeight: 52,
  33. minWidth: double.infinity,
  34. ),
  35. decoration: BoxDecoration(
  36. color: color,
  37. boxShadow: [
  38. //阴影
  39. BoxShadow(
  40. color: Colors.black26,
  41. offset: Offset(0, 3),
  42. blurRadius: 3,
  43. ),
  44. ],
  45. ),
  46. child: Text(
  47. title,
  48. style: TextStyle(
  49. fontWeight: FontWeight.bold,
  50. //根据背景色亮度来确定Title颜色
  51. color: color.computeLuminance() < 0.5 ? Colors.white : Colors.black,
  52. ),
  53. ),
  54. );
  55. }
  56. }
  57. //测试代码
  58. class TestComputeLuminance extends StatelessWidget {
  59. const TestComputeLuminance({Key key}) : super(key: key);
  60. @override
  61. Widget build(BuildContext context) {
  62. return Column(children: <Widget>[
  63. //背景为蓝色,则title自动为白色
  64. NavBar(color: Colors.blue, title: "标题"),
  65. //背景为白色,则title自动为黑色
  66. NavBar(color: Colors.white, title: "标题"),
  67. ]);
  68. }
  69. }

运行效果如图所示:
颜色和主题 - 图1

MaterialColor

MaterialColor是实现Material Design中的颜色的类,它包含一种颜色的10个级别的渐变色。

  • MaterialColor 通过 [] 运算符的索引值来代表颜色的深度
  • 有效的索引有:50,100,200,…,900,数字越大,颜色越深。
  • MaterialColor默认颜色的索引值为:500

举个例子,Colors.blue是预定义的一个MaterialColor类对象,定义如下:

  1. static const MaterialColor blue = MaterialColor(
  2. _bluePrimaryValue,
  3. <int, Color>{
  4. 50: Color(0xFFE3F2FD),
  5. 100: Color(0xFFBBDEFB),
  6. 200: Color(0xFF90CAF9),
  7. 300: Color(0xFF64B5F6),
  8. 400: Color(0xFF42A5F5),
  9. 500: Color(_bluePrimaryValue),
  10. 600: Color(0xFF1E88E5),
  11. 700: Color(0xFF1976D2),
  12. 800: Color(0xFF1565C0),
  13. 900: Color(0xFF0D47A1),
  14. },
  15. );
  16. static const int _bluePrimaryValue = 0xFF2196F3;

Colors.blue[50]到Colors.blue[100]的色值从浅蓝到深蓝渐变,效果如图所示:
颜色和主题 - 图2

Theme

Theme 组件可以为Material APP定义主题数据(ThemeData)。Material组件库里很多组件都使用了主题数据,如:

  • 导航栏颜色
  • 标题字体
  • Icon样式等。

Theme 内会使用 InheritedWidget 来为其子树共享样式数据。

ThemeData

ThemeData用于保存是Material 组件库的主题数据,Material组件需要遵守相应的设计规范,而这些规范可自定义部分都定义在ThemeData中了,所以我们可以通过ThemeData来自定义应用主题。在子组件中,我们可以通过Theme.of 方法来获取当前的ThemeData。

⚠️ 注意:Material Design 设计规范中有些是不能自定义的,如导航栏高度,ThemeData只包含了可自定义部分。

我们看看ThemeData部分数据定义:

  1. ThemeData({
  2. Brightness brightness, //深色还是浅色
  3. MaterialColor primarySwatch, //主题颜色样本,见下面介绍
  4. Color primaryColor, //主色,决定导航栏颜色
  5. Color accentColor, //次级色,决定大多数Widget的颜色,如进度条、开关等。
  6. Color cardColor, //卡片颜色
  7. Color dividerColor, //分割线颜色
  8. ButtonThemeData buttonTheme, //按钮主题
  9. Color cursorColor, //输入框光标颜色
  10. Color dialogBackgroundColor,//对话框背景颜色
  11. String fontFamily, //文字字体
  12. TextTheme textTheme,// 字体主题,包括标题、body等文字样式
  13. IconThemeData iconTheme, // Icon的默认样式
  14. TargetPlatform platform, //指定平台,应用特定平台控件风格
  15. ...
  16. })

上面只是ThemeData的一小部分属性,完整的数据定义读者可以查看SDK。
上面属性中需要说明的是 primarySwatch,它是主题颜色的一个”样本色”,通过这个样本色可以在一些条件下生成一些其它的属性,例如,如果没有指定primaryColor,并且当前主题不是深色主题,那么primaryColor就会默认为primarySwatch指定的颜色,还有一些相似的属性如 accentColor、indicatorColor等也会受 primarySwatch 影响。

实现一个路由换肤功能的示例

  1. import 'package:flutter/material.dart';
  2. void main() => runApp(new MyApp());
  3. class MyApp extends StatelessWidget {
  4. @override
  5. Widget build(BuildContext context) {
  6. return MaterialApp(
  7. debugShowCheckedModeBanner: true,
  8. title: '主题测试',
  9. // theme: ThemeData(
  10. // primarySwatch: Colors.blue,
  11. // ),
  12. home: Scaffold(
  13. // appBar: AppBar(title: Text("主题测试")),
  14. body: ThemeTestRoute(),
  15. ));
  16. }
  17. }
  18. class ThemeTestRoute extends StatefulWidget {
  19. @override
  20. _ThemeTestRouteState createState() => new _ThemeTestRouteState();
  21. }
  22. class _ThemeTestRouteState extends State<ThemeTestRoute> {
  23. Color _themeColor = Colors.teal; //当前路由主题色
  24. @override
  25. Widget build(BuildContext context) {
  26. ThemeData themeData = Theme.of(context);
  27. return Theme(
  28. data: ThemeData(
  29. primarySwatch: _themeColor, //用于导航栏、FloatingActionButton的背景色等
  30. iconTheme: IconThemeData(color: _themeColor) //用于Icon颜色
  31. ),
  32. child: Scaffold(
  33. appBar: AppBar(title: Text("主题测试")),
  34. body: Column(
  35. mainAxisAlignment: MainAxisAlignment.center,
  36. children: <Widget>[
  37. //第一行Icon使用主题中的iconTheme
  38. Row(
  39. mainAxisAlignment: MainAxisAlignment.center,
  40. children: <Widget>[
  41. Icon(Icons.favorite),
  42. Icon(Icons.airport_shuttle),
  43. Text(" 颜色跟随主题")
  44. ]
  45. ),
  46. //为第二行Icon自定义颜色(固定为黑色)
  47. Theme(
  48. data: themeData.copyWith(
  49. iconTheme: themeData.iconTheme.copyWith(
  50. color: Colors.black
  51. ),
  52. ),
  53. child: Row(
  54. mainAxisAlignment: MainAxisAlignment.center,
  55. children: <Widget>[
  56. Icon(Icons.favorite),
  57. Icon(Icons.airport_shuttle),
  58. Text(" 颜色固定黑色")
  59. ]
  60. ),
  61. ),
  62. ],
  63. ),
  64. floatingActionButton: FloatingActionButton(
  65. onPressed: () => //切换主题
  66. setState(() =>
  67. _themeColor =
  68. _themeColor == Colors.teal ? Colors.blue : Colors.teal
  69. ),
  70. child: Icon(Icons.palette)
  71. ),
  72. ),
  73. );
  74. }
  75. }

运行后点击右下角悬浮按钮则可以切换主题,如图所示:
颜色和主题 - 图3颜色和主题 - 图4
需要注意的有三点:

  • 可以通过局部主题覆盖全局主题,正如代码中通过Theme为第二行图标指定固定颜色(黑色)一样,这是一种常用的技巧,Flutter中会经常使用这种方法来自定义子树主题。那么为什么局部主题可以覆盖全局主题?这主要是因为widget中使用主题样式时是通过Theme.of(BuildContext context)来获取的,我们看看其简化后的代码:

    1. static ThemeData of(BuildContext context, { bool shadowThemeOnly = false }) {
    2. // 简化代码,并非源码
    3. return context.inheritFromWidgetOfExactType(_InheritedTheme).theme.data
    4. }
  • context.inheritFromWidgetOfExactType 会在widget树中从当前位置向上查找第一个类型为_InheritedTheme的widget。所以当局部指定Theme后,其子树中通过Theme.of()向上查找到的第一个_InheritedTheme便是我们指定的Theme。

  • 本示例是对单个路由换肤,如果想要对整个应用换肤,则可以去修改MaterialApp的theme属性