flutter:样式管理ThemeData与Color

ThemeData 样式对象

常见属性分类

在 Flutter 中,ThemeData 是一个 全局样式配置对象,用于统一定义 App 的颜色、字体、按钮风格、图标大小、卡片样式等。它通常配合 MaterialApp 使用。

分类 属性 作用
颜色相关 primaryColor AppBar、TabBar、按钮等主要颜色
primarySwatch 一组主色调(Material 设计推荐),会自动生成深浅变化
accentColor(旧,改为 colorScheme.secondary 强调色,用于 FloatingActionButton 等
backgroundColor Scaffold 的背景色
scaffoldBackgroundColor 页面背景色
dividerColor Divider 的颜色
亮/暗模式 brightness 设置亮色(light)或暗色(dark)主题
字体与文本 fontFamily 全局字体
textTheme 全局文字样式,如 headline1bodyText1
AppBar appBarTheme 定制 AppBar 的样式,如背景色、文字颜色
按钮 buttonTheme(旧) 按钮的全局样式
elevatedButtonTheme ElevatedButton 的样式
textButtonTheme TextButton 的样式
outlinedButtonTheme OutlinedButton 的样式
输入框 inputDecorationTheme 定制 TextField 的样式(边框、hint、label)
卡片 cardTheme 全局卡片样式(阴影、圆角、颜色)
图标 iconTheme 图标默认颜色和大小
底部导航栏 bottomNavigationBarTheme BottomNavigationBar 的样式

primaryColor vs primarySwatch

primaryColor

  • 单一颜色 (Color)
  • 用来指定主题的主色调(AppBar、按钮、进度条等的主要颜色)。
  • 只能表示一个固定的颜色,没有深浅变化。

primarySwatch

  • 一组颜色 (MaterialColor)
  • 表示一个完整的“颜色调色板”,包含不同深浅的色阶(50、100 … 900)。
  • Flutter 在生成主题(比如按钮按下、禁用、悬停时的效果)时,需要用到不同层级的颜色,所以它要求的是 MaterialColor

关系:

  • primaryColor 相当于 primarySwatch[500]
  • 也就是说,如果你只设置了 primarySwatchprimaryColor 会自动取它的中间色(通常是 500)。
  • 如果你只设置了 primaryColor,那么你只给了 Flutter 一个颜色,Flutter 就没法自动生成完整的色阶。

组件 / 场景 只设置 primaryColor 设置 primarySwatch
AppBar 使用固定的 primaryColor 使用 primarySwatch[500]
FAB (FloatingActionButton) 颜色可能跟随 primaryColor,但禁用/按下时只会加透明度 背景用 swatch[500],按下时 swatch[700],禁用时 swatch[100]
ElevatedButton 背景 = primaryColor,按下时颜色基本不变,只是透明度变化 背景用 swatch[500],按下时自动变 swatch[700],禁用时自动变浅色 swatch[100]
Switch / Slider / ProgressIndicator 只会用 primaryColor,状态变化不明显 自动根据 swatch 的不同深浅色阶适配各种状态
整体主题色层次 没有层次,所有地方都是同一个颜色 有 10 个色阶,能适配亮/暗/禁用/按下等场景,层次更丰富
  • primaryColor:单一颜色 → 主题简单,状态变化弱。
  • primarySwatch:一整套色阶 → Flutter 能帮你自动适配按钮按下、禁用、悬浮等状态,看起来更专业。

一个案例

import 'package:flutter/material.dart';
import 'package:flutter_quickstart_learn/theme.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Quick Start',

      // 样式
      theme: ThemeData(
        // 主题颜色
        primarySwatch: Colors.brown,
        // appBar颜色
        appBarTheme: ThemeData.light().appBarTheme.copyWith(
          // appBar背景色
          backgroundColor: Colors.green,
          // appBar前景色
          // 前景色就是文字和图标的颜色
          foregroundColor: Colors.white,
        ),
        // 按钮颜色
        elevatedButtonTheme: ElevatedButtonThemeData(
          // 按钮样式
          style: ElevatedButton.styleFrom(
            // 按钮背景色
            foregroundColor: Colors.white,
            // 按钮前景色
            backgroundColor: Colors.amber,
          ),
        ),
      ),

      // page
      home: const ThemePage(),

      // 关闭 debug 标签
      debugShowCheckedModeBanner: false,
    );
  }
}


import 'package:flutter/material.dart';

class ThemePage extends StatelessWidget {
  const ThemePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('ThemePage')),
      body: Center(
        child: ElevatedButton(onPressed: () {}, child: const Text('Theme')),
      ),
    );
  }
}

使用 Theme.of(context)

在 Widget 内部,可以通过 Theme.of(context) 获取当前主题,然后使用里面的样式:

class ThemePage extends StatelessWidget {
  const ThemePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('ThemePage')),
      body: Center(
        child: ElevatedButton(
          onPressed: () {},
          style: ButtonStyle(
            foregroundColor: WidgetStateProperty.all(
              // 这里使用Theme.of(context)获取了theme的相关配置
              Theme.of(context).appBarTheme.backgroundColor,
            ),
          ),
          child: const Text('Theme'),
        ),
      ),
    );
  }
}

Color 与 MaterialColor

Color

https://api.flutter.dev/flutter/material/Colors-class.html

Color 是 Flutter 中最基本的颜色类,用于表示一个具体的颜色值。

本质上就是一个 32 位 ARGB 整数值

  • A (Alpha, 透明度)
  • R (Red, 红色分量)
  • G (Green, 绿色分量)
  • B (Blue, 蓝色分量)
Color(0xFF42A5F5)   // 蓝色(0xAARRGGBB)
Colors.red          // Flutter 内置常量(其实也是一个 Color)
Colors.black.withOpacity(0.5) // 半透明黑色

MaterialColor

https://api.flutter.dev/flutter/material/MaterialColor-class.html

MaterialColorMaterial Design 颜色系统中的一种特殊颜色类。

它不仅包含一个主颜色,还包含 从浅到深的多个色阶(shade),比如 50、100、200 … 900。

常用于 primarySwatch,因为 Flutter 需要一整套颜色来生成不同组件的效果。

Colors.blue          // MaterialColor,蓝色的主色
Colors.blue[100]     // 浅蓝
Colors.blue[900]     // 深蓝

Colors.blue 并不是一个单一的 Color,而是一个 MaterialColor 对象,里面内置了 10 个不同深浅的蓝色。

字符串转 Color

// 字符串转 Color
Color stringToColor(String source) {
  return Color(int.parse(source, radix: 16) | 0xFF000000);
}

Color c = stringToColor("40c254");

int.parse(source, radix: 16)

  • source 是一个字符串,比如 "40c254"
  • radix: 16 表示把它按 16进制 解析成整数。
    • "40c254"0x40C254 → 十进制 = 4242260

所以得到的数字是 0x0040C254


| 0xFF000000

  • 在 Flutter 里,Color 的构造函数需要一个 32位 ARGB 整数
    • 格式是 0xAARRGGBB
      • AA = Alpha(透明度)
      • RR = Red
      • GG = Green
      • BB = Blue
  • 但我们传进来的 "40c254" 只有 RGB 部分(6位),没有透明度信息。
  • | 0xFF000000 的意思是 按位或运算,强行把最高的 AA(透明度)位置成 FF,也就是不透明。

所以 0x0040C254 | 0xFF000000 = 0xFF40C254


Color(0xFF40C254)

  • 最终生成的 Color 对象就是:
    • Alpha = FF (完全不透明)
    • Red = 40 (64)
    • Green = C2 (194)
    • Blue = 54 (84)

也就是一个绿色系的颜色。

字符串转 MaterialColor

// 字符串转 MaterialColor
// 字符串转 MaterialColor
static MaterialColor stringToMaterialColor(String source) {
  Color color = stringToColor(source);

  List<double> strengths = <double>[.05];
  Map<int, Color> swatch = <int, Color>{};
  // 'red' is deprecated and shouldn't be used. Use (*.r * 255.0).round() & 0xff.
  // color.red 也过期了,应该使用 color.r
  final int r = (color.r * 255.0).round() & 0xFF,
      g = (color.g * 255.0).round() & 0xFF,
      b = (color.b * 255.0).round() & 0xFF;

  for (int i = 1; i < 10; i++) {
    strengths.add(0.1 * i);
  }
  for (var strength in strengths) {
    final double ds = 0.5 - strength;
    swatch[(strength * 1000).round()] = Color.fromRGBO(
      r + ((ds < 0 ? r : (255 - r)) * ds).round(),
      g + ((ds < 0 ? g : (255 - g)) * ds).round(),
      b + ((ds < 0 ? b : (255 - b)) * ds).round(),
      1,
    );
  }
  // color.value // 过期
  // 官方提示:'value' is deprecated and shouldn't be used.
  //Use component accessors like .r or .g, or toARGB32 for an explicit conversion.
  //
  return MaterialColor(color.toARGB32(), swatch);
}

MaterialColor mc = stringToColor("40c254");

Color color = stringToColor(source):调用你之前写的 stringToColor("40c254"),把字符串转成 Color 对象。上面 "40c254" 会得到 Color(0xFF40C254)

List<double> strengths = <double>[.05]

  • .05 就是 0.05(Dart 允许省略小数点前的 0)。所以这行等价于:List<double> strengths = [0.05];

  • 创建了一个列表,列表里面有一个值0.05

final int r = (color.r * 255.0).round() & 0xFF

  • 前面有一个字符串转成Color的类,然后获得这个类的 RGB 值

  • 在 Flutter 3.7+,Color.red / .green / .blue 被标记为过期。取而代之的是 .r.g.b.a,它们 返回 0.0 ~ 1.0 的 double,而不是 0~255 的整数。

  • * 255.0:将 0~1 的浮点数映射到 0~255 的整数范围

  • .round():四舍五入,得到最接近的整数

  • & 0xFF

    • 0xFF 是十六进制表示,等于 255。二进制形式:11111111(8 位全 1)

    • (x & 0xFF) 的作用:

    • 会把整数 x低 8 位保留,高位全部清零。也就是说,只保留 0~255 的部分。比如:

      int x = 300;        // 二进制:1 0010 1100
      int y = x & 0xFF;   // 二进制:0010 1100 = 44
      

strengths.add(0.1 * i):生成 [0.05, 0.1, 0.2, ..., 0.9] 这些比例。


for (var strength in strengths) {
  final double ds = 0.5 - strength;
  swatch[(strength * 1000).round()] = Color.fromRGBO(
    r + ((ds < 0 ? r : (255 - r)) * ds).round(),
    g + ((ds < 0 ? g : (255 - g)) * ds).round(),
    b + ((ds < 0 ? b : (255 - b)) * ds).round(),
    1,
  );
}
  1. (strength * 1000).round().round() 是取整函数,它会把一个 double 四舍五入成最近的 int。最后得到:

    0.05 → 50
    
    0.1 → 100
    
    0.2 → 200
    
    ...
    
    0.9 → 900
    
  2. Color.fromRGBO:在 Flutter 里,Color 是一个用来表示颜色的类,它有一个工厂构造方法:

    Color.fromRGBO(
      int r,      // 红色通道 (0~255)
      int g,      // 绿色通道 (0~255)
      int b,      // 蓝色通道 (0~255)
      double opacity, // 透明度 (0.0 ~ 1.0)
    )
    

    r / g / b

    • 红、绿、蓝的通道值,取值范围是 0 ~ 255
    • 0 = 没有该通道,255 = 最强。
    • 例如 (255, 0, 0) 就是纯红色。

    opacity

    • 透明度,取值范围 0.0 ~ 1.0
    • 0.0 → 完全透明
    • 1.0 → 完全不透明
  3. ds < 0 ? r : (255 - r)

    • 如果 ds < 0,说明 strength > 0.5(比较偏深色),那么用 r(或者 g / b)去调暗;
    • 否则(ds >= 0,偏浅色),用 255 - r(或者 g / b)去调亮。
    • 这样保证生成的色阶既有比原色更亮的版本,也有比原色更暗的版本。
  4. ((ds < 0 ? r : (255 - r)) * ds)

    • 这一步决定偏移的幅度。
    • ds 越大 → 偏移越明显(更亮 / 更暗)。
    • ds 越小 → 偏移越接近原色。
  5. r + (调整量)

    • 调整量 = ((ds < 0 ? r : (255 - r)) * ds).round()
    • 如果是调亮(ds >= 0),就会在原值上往 255 方向靠近;
    • 如果是调暗(ds < 0),就会在原值上往 0 方向靠近。

return MaterialColor(color.toARGB32(), swatch):构造 MaterialColor

  • color.toARGB32() = 主色(比如 0xFF40C254
  • swatch = 不同深浅的变体。

代码

class ColorPage extends StatelessWidget {
  const ColorPage({super.key});

  @override
  Widget build(BuildContext context) {
    var c = ColorUtils.stringToColor("FFB822");
    var mc = ColorUtils.stringToMaterialColor("5C78FF");

    return Scaffold(
      body: SizedBox.expand(
        child: Column(
          children: [
            // Color
            Container(color: c, height: 50),

            // MaterialColor
            for (var i = 1; i < 10; i++)
              Container(color: mc[i * 100], height: 50),
          ],
        ),
      ),
    );
  }
}

×

喜欢就点赞,疼爱就打赏