主题

Theme Widget可以为Material APP定义主题数据(ThemeData),Material组件库里很多Widget都使用了主题数据,如导航栏颜色、标题字体、Icon样式等。Theme内会使用InheritedWidget来为其子树Widget共享样式数据。

ThemeData

ThemeData是Material Design Widget库的主题数据,Material库的Widget需要遵守相应的设计规范,而这些规范可自定部分都定义在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指定的颜色,还有一些相似的属性如accentColorindicatorColor等也会受primarySwatch影响。

示例

我们实现一个路由换肤功能:

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

运行后点击右下角悬浮按钮则可以切换主题:

Screenshot_1536838018Screenshot_1536838021

需要注意的有三点:

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

    1. static ThemeData of(BuildContext context, { bool shadowThemeOnly = false }) {
    2. // 简化代码,并非源码
    3. return context.inheritFromWidgetOfExactType(_InheritedTheme)
    4. }

    context.inheritFromWidgetOfExactType 会在widget树中从当前位置向上查找第一个类型为_InheritedTheme的Widget。所以当局部使用Theme后,其子树中Theme.of()找到的第一个_InheritedTheme便是该Theme的。

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