4、颜色系统一致性:Flutter ColorScheme与Android ColorStateList、取色算法对齐、暗色模式适配

颜色系统,说白了就是App的「皮肤」。

我见过太多项目,Flutter端和Android原生端颜色对不上。白天模式还好,一切暗色模式,整个界面就「花」了。嗯,今天我们就来彻底解决这个问题。

4.1 为什么颜色系统会「打架」?

先看一个我踩过的坑。

有一次,设计师给了一套色板,Android原生用ColorStateList处理按钮按压态,Flutter端用MaterialStateColor。结果呢?同一个蓝色,在原生端按下去变深蓝,在Flutter端按下去变灰蓝。设计师当场崩溃。

为什么会这样?

因为两边的颜色处理机制,本质上就不是一套东西。

  • Android原生:依赖ColorStateList,根据状态(按下、禁用、聚焦)返回不同颜色。
  • Flutter:依赖ColorScheme,是一整套语义化颜色体系,比如primarysecondarysurface

说白了,一个是「状态驱动」,一个是「语义驱动」。你要让它们对齐,就得在中间搭一座桥。

4.2 核心对齐策略:从设计Token到代码

我个人习惯,先定义一套「设计Token」。Token就是颜色的唯一ID,比如color_primary_500color_primary_700

然后,两端都从这套Token生成各自的颜色配置。

4.2.1 Android端:用ColorStateList封装

// 定义一个按钮颜色的ColorStateList
ColorStateList buttonColorList = new ColorStateList(
    new int[][]{
        new int[]{android.R.attr.state_pressed},  // 按下
        new int[]{android.R.attr.state_enabled},  // 可用
        new int[]{-android.R.attr.state_enabled}  // 禁用
    },
    new int[]{
        getColor(R.color.color_primary_700),  // 按下变深
        getColor(R.color.color_primary_500),  // 正常
        getColor(R.color.color_grey_300)      // 禁用变灰
    }
);

4.2.2 Flutter端:用MaterialStateColor映射

// Flutter端,用MaterialStateColor.resolveWith
MaterialStateColor.resolveWith((Set<MaterialState> states) {
  if (states.contains(MaterialState.pressed)) {
    return AppColors.primary700; // 对应Android的color_primary_700
  }
  if (states.contains(MaterialState.disabled)) {
    return AppColors.grey300;
  }
  return AppColors.primary500;
});

关键点:两端的颜色值必须从同一个源读取。我建议用JSON或YAML文件维护色板,两端各自解析。

4.3 取色算法对齐:别让「看起来一样」骗了你

你想想看,同一个十六进制颜色值,在Android和Flutter上显示效果一样吗?

不一定。

因为两端的色彩空间处理方式不同。Android原生用的是sRGB,Flutter默认也是sRGB,但如果你用了Color.fromRGBO,或者涉及透明度混合,结果就可能不一样。

我曾经遇到一个问题:一个半透明遮罩层,Android端看起来偏冷,Flutter端偏暖。查了半天,发现是两端的预乘Alpha处理方式不同。

避坑指南

  • Flutter中,Color.withOpacityColor.fromRGBO 的透明度处理,与Android的Color.argb 不完全一致。
  • 建议统一使用Color.fromARGB,并确保两端使用相同的Alpha值。

4.3.1 取色算法示例

假设我们要实现一个「按压变暗」的效果,两端算法要对齐。

// Android: 颜色变暗算法
public static int darkenColor(int color, float factor) {
    float[] hsv = new float[3];
    Color.colorToHSV(color, hsv);
    hsv[2] = hsv[2] * factor; // 降低明度
    return Color.HSVToColor(hsv);
}

// Flutter: 颜色变暗算法
Color darkenColor(Color color, double factor) {
    double hue = color.hue;
    double saturation = color.saturation;
    double lightness = color.lightness * factor; // 降低明度
    return HSLColor.fromAHSL(color.opacity, hue, saturation, lightness).toColor();
}

提示:我建议两端都使用HSL模型来处理颜色变化,这样更直观,也更容易对齐。

4.4 暗色模式适配:不只是换个背景色

暗色模式,说白了就是给用户一个「夜间友好」的界面。但很多团队只把背景色从白变黑,结果文字看不清,图标消失,按钮像幽灵。

嗯,这里要注意:暗色模式是一整套颜色体系的切换,不是简单的反色。

4.4.1 Android端:使用夜间限定资源

Android原生支持values-night目录,你可以在里面放一套暗色主题的colors.xml

<!-- values-night/colors.xml -->
<color name="color_background">#121212</color>
<color name="color_on_background">#FFFFFF</color>
<color name="color_surface">#1E1E1E</color>

4.4.2 Flutter端:使用Brightness感知

Flutter的ColorScheme天然支持brightness属性。你只需要在ThemeData中指定brightness: Brightness.dark,然后系统会自动切换颜色。

ThemeData(
  brightness: Brightness.dark,
  colorScheme: ColorScheme.dark(
    primary: AppColors.primary500,
    secondary: AppColors.secondary500,
    surface: AppColors.surfaceDark,
    onSurface: AppColors.onSurfaceDark,
  ),
);

核心原则:暗色模式下,颜色的对比度要足够。我一般会确保文字和背景的对比度不低于4.5:1(WCAG AA标准)。

4.5 知识体系总览

下面这张图,是我梳理的颜色系统一致性核心逻辑。你可以把它当作「检查清单」来用。

颜色系统一致性核心逻辑 设计Token(色板JSON/YAML) Android: ColorStateList Flutter: ColorScheme 取色算法(HSL对齐) 取色算法(HSL对齐) 暗色模式(values-night) 暗色模式(Brightness.dark) UI一致性输出

4.6 实战建议

最后,分享几个我个人的实战经验:

  • 统一色板源:不要手动维护两套色板。用脚本从设计Token生成Android的colors.xml和Flutter的app_colors.dart
  • 自动化对比测试:我写过一个脚本,截图两端同一页面,然后逐像素对比颜色差异。超过阈值就报警。
  • 暗色模式要早做:别等界面都写完了再适配暗色模式。从第一天起就用ColorSchemeColorStateList,后面切换只是改个配置的事。

一个小技巧:如果你发现两端颜色怎么都对不上,试试把两边的颜色值都转成HSL,然后对比H、S、L三个分量。通常问题出在「明度L」上。

颜色系统的一致性,说白了就是「同一个Token,同一套算法,同一个暗色逻辑」。做到这三点,你的App就不会在颜色上「翻车」。


公众号:蓝海资料掘金营,微信deep3321