这一期接了一个“适配深色模式”的需求,在做之前过了一遍自己负责的几个界面,发现系统的反色基本上把所有的工作都做完了。少了一项需求,心里美滋滋。提测以后测试拿着一堆切了深色模式以后还是白花花的界面来找到我,我一脸不可置信的看着测试。得,系统的反色突然失效了。没办法,只好抛开系统自己来做一些针对深色模式的适配。
对于深色模式的适配主要有以下两方面的工作需要进行
- 对于Res的图片资源进行适配
- 对于界面颜色等进行适配
针对图片等资源文件的深色模式的适配,没有什么好办法,只能通过新建drawable-night文件夹来进行适配
针对界面颜色的,正常情况下,我们可以通过以下几种方式对界面的颜色进行深色模式的适配
-
依赖系统的反色适配
这是三种方法里改动最小的方法,系统UI会判断当前模式是否为深色模式,从而修改View绘制时使用的Paint颜色,以达到反色的目的。但是在厂商定制的系统当中,可能各家对于反色都有着不同的算法,所以最终的结果是可能会导致相同的颜色在不同厂家的深色模式当中会展现为不同的颜色 -
设置-night
和Drawable文件一样,color资源也可以使用-night来对深色模式进行适配。如果是一个新的项目或者是color资源不多的情况下,使用这种方式显然会更好一些。但是如果是color资源文件相当多的情况下,这种方式想想就头大= = -
设置Style
和上面设置color-night类似,甚至还比上面的方式更繁琐一些- -
在对深色模式的适配过程中,我原本打算只依赖系统的反色来进行适配的。但是在测试过程当中发现,不同的系统版本的反色可能对相同的界面产生不同的效果。在万般无奈之下,只好引入了一个DarkmodeUtil来对整个页面的深色模式进行辅助适配,以求完美的效果.
如何得到一个可以在深色模式当中令人舒服的颜色
这是我在做这个工具遇到的第一个问题,在我们日常生活当中,我们通常会使用RGB或者是CMYK的方式来描述一个颜色的具体的数值。但是根据我对深色模式下通过系统的反色和原色对比发现。系统并不是通过RGB方式来计算深色模式到底应该使用哪个颜色的。经过查找资料发现,系统使用了一种叫Lab的颜色计量方式来描述一种颜色
什么是Lab颜色
可以参考百度百科对Lab颜色模式的描述
https://baike.baidu.com/item/%E9%A2%9C%E8%89%B2%E6%A8%A1%E5%9E%8B/7558583?fromtitle=Lab&fromid=1514615#2_9
找到了合适的颜色计算方式,接下来就是如何把RGB转化为Lab了,在这里我参考了下面这个博主的一个方法。
根据这个博主所言,RGB与Lab不能直接进行转化,所以这个博主引入了XYZ中间量来辅助转化。但是这个博主的方法用在此处仍然有一点点小问题,在我接下来给出方法中将这些问题给修复了
private double[] sRGB2XYZ(double[] sRGB) {
double[] xyz = new double[3];
double sR = sRGB[0] / 255;
double sG = sRGB[1] / 255;
double sB = sRGB[2] / 255;
if (sR <= 0.04045) {
sR = sR / 12.92;
} else {
sR = Math.pow(((sR + 0.055) / 1.055), 2.4);
}
if (sG <= 0.04045) {
sG = sG / 12.92;
} else {
sG = Math.pow(((sG + 0.055) / 1.055), 2.4);
}
if (sB <= 0.04045) {
sB = sB / 12.92;
} else {
sB = Math.pow(((sB + 0.055) / 1.055), 2.4);
}
xyz[0] = 41.24 * sR + 35.76 * sG + 18.05 * sB;
xyz[1] = 21.26 * sR + 71.52 * sG + 7.2 * sB;
xyz[2] = 1.93 * sR + 11.92 * sG + 95.05 * sB;
return xyz;
}
XYZ转Lab
private double[] XYZ2Lab(@Size(3) double[] xyz) {
double[] Lab = new double[3];
double x = xyz[0], y = xyz[1], z = xyz[2];
double Xn = 95.04, Yn = 100, Zn = 108.89;
double XXn, YYn, ZZn;
XXn = x / Xn;
YYn = y / Yn;
ZZn = z / Zn;
double fx, fy, fz;
if (XXn > 0.008856) {
fx = Math.pow(XXn, 0.333333);
} else {
fx = 7.787 * XXn + 0.137931;
}
if (YYn > 0.008856) {
fy = Math.pow(YYn, 0.333333);
} else {
fy = 7.787 * YYn + 0.137931;
}
if (ZZn > 0.008856) {
fz = Math.pow(ZZn, 0.333333);
} else {
fz = 7.787 * ZZn + 0.137931;
}
/**
* 在这里可能会出现Lab[0]超过0-100这个范围的情况
* 所以需要这里进行一个限制
*/
Lab[0] = Math.min(Math.max(116 * fy - 16, 0), 100);
Lab[1] = Math.min(Math.max((500 * (fx - fy)), -128), 127);
Lab[2] = Math.min(Math.max((200 * (fy - fz)), -128), 127);
return Lab;
}
这样通过这两步的转化,就可以把一个RGB颜色转换为Lab进行表示。
在Lab颜色模式当中,L代表当前颜色的亮度。我们可以针对性的对颜色进行设置
譬如#00FF00这个绿色,其L的数值为87,经过变换后,其L的数值为13,效果如下
上面一行为原色,下面一行为变化后的颜色
可以看到,在经过对L的变化后,绿色就显得不是这么的艳丽,整个效果也更加的符合深色模式的基调
对颜色进行换算的代码如下
private int getDarkColor(int colorNum, boolean isNeedLight) {
int a = alpha(colorNum);
int r = red(colorNum);
int g = green(colorNum);
int b = blue(colorNum);
double[] lab = XYZ2Lab(sRGB2XYZ(new double[]{r, g, b}));
double l = changeL(lab[0]);
if (isNeedLight) {
lab[0] = Math.max(l, lab[0]);
} else {
lab[0] = Math.min(l, lab[0]);
}
int[] rgb = XYZ2sRGB(Lab2XYZ(lab));
return Color.argb(a, rgb[0], rgb[1], rgb[2]);
}
在对颜色进行亮度变化完成后需要再将颜色从Lab模式变成RGB模式
private double[] Lab2XYZ(@Size(3) double[] Lab) {
double[] xyz = new double[3];
double l = Lab[0], a = Lab[1], b = Lab[2];
double Xn = 95.04, Yn = 100, Zn = 108.89;
double fx, fy, fz;
fy = (l + 16) / 116;
fx = a / 500 + fy;
fz = fy - b / 200;
if (fx > 0.2069) {
xyz[0] = Xn * Math.pow(fx, 3);
} else {
xyz[0] = Xn * (fx - 0.1379) * 0.1284;
}
if ((fy > 0.2069) || (l > 8)) {
xyz[1] = Yn * Math.pow(fy, 3);
} else {
xyz[1] = Yn * (fy - 0.1379) * 0.1284;
}
if (fz > 0.2069) {
xyz[2] = Zn * Math.pow(fz, 3);
} else {
xyz[2] = Zn * (fz - 0.1379) * 0.1284;
}
return xyz;
}
private int[] XYZ2sRGB(@Size(3) double[] xyz) {
int[] sRGB = new int[3];
double x = xyz[0], y = xyz[1], z = xyz[2];
double dr = 0.032406 * x - 0.015371 * y - 0.0049895 * z;
double dg = -0.0096891 * x + 0.018757 * y + 0.00041914 * z;
double db = 0.00055708 * x - 0.0020401 * y + 0.01057 * z;
if (dr <= 0.00313) {
dr = dr * 12.92;
} else {
dr = Math.exp(Math.log(dr) / 2.4) * 1.055 - 0.055;
}
if (dg <= 0.00313) {
dg = dg * 12.92;
} else {
dg = Math.exp(Math.log(dg) / 2.4) * 1.055 - 0.055;
}
if (db <= 0.00313) {
db = db * 12.92;
} else {
db = Math.exp(Math.log(db) / 2.4) * 1.055 - 0.055;
}
dr = dr * 255;
dg = dg * 255;
db = db * 255;
dr = Math.min(255, dr);
dg = Math.min(255, dg);
db = Math.min(255, db);
/**
* 这里也是,需要对其范围进行一个限制
*/
sRGB[0] = (int) Math.min(Math.max((dr + 0.5), 0), 255);
sRGB[1] = (int) Math.min(Math.max((dg + 0.5), 0), 255);
sRGB[2] = (int) Math.min(Math.max((db + 0.5), 0), 255);
return sRGB;
}
整体思路就是将原来的颜色获取RGB的值以后将其转换成Lab模式,然后在对其进行一个亮度的换算。在换算完成后再将其转化为RGB模式。在这里有两个点需要特殊说明一下
- changeL()函数是用来换算对应的L的关系的,在这里可以直接使用 y = -x + 100 这个函数来对L进行简单的换算。
-
isNeedLight的意义
这个参数是用来判断当前颜色到底是应该变亮还是变黑一些的。在深色模式下,并不是所有的颜色都需要变黑,对于内容的呈现部分(文字),是需要对其进行变亮处理的,譬如上图的文字所示。但是由于可能系统会对某些地方进行反色处理,如果在系统反色之后我们再手动的进行一次反色处理,那么还不如不变色。。。。。所以需要对当前颜色的亮度和变化之后的亮度进行分别判断取值。如果是需要变亮的区域,我们就将其和变化后的亮度取最大值,反之取最小值。
通过这个函数变化后的整体效果为
上面一行为原色,下面一行为变化后的颜色和经过系统反色后的效果进行对比
上面一行为原色,下面一行为变化后的颜色
可以看到,差距仍然是存在的,但是相较而言,已经处在一种可以接受的范围之内了。只需要针对性的再调整一下换算亮度L的函数即可完美对深色模式进行辅助适配,而不用重新写大量的color
在新建一个函数来传入需要变化颜色的View或者是ViewGroup
public void setParentView(View v) {
if (v.getBackground() instanceof ColorDrawable) {
ColorDrawable cd = (ColorDrawable) v.getBackground();
v.setBackgroundColor(getDarkColor(cd.getColor(), false));
} else if (v.getBackground() instanceof GradientDrawable) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
GradientDrawable gd = (GradientDrawable) v.getBackground();
ColorStateList csL = gd.getColor();
if (csL != null) {
int color = csL.getDefaultColor();
gd.setColor(getDarkColor(color, false));
}
int[] colors = gd.getColors();
if (colors != null) {
for (int i = 0; i < colors.length; i++) {
colors[i] = getDarkColor(colors[i], false);
}
gd.setColors(colors);
}
v.setBackground(gd);
}
}
if (v instanceof ViewGroup) {
for (int i = 0; i < ((ViewGroup) v).getChildCount(); i++) {
setParentView(((ViewGroup) v).getChildAt(i));
}
} else {
if (v instanceof TextView) {
int colorNumber = ((TextView) v).getCurrentTextColor();
((TextView) v).setTextColor(getDarkColor(colorNumber, true));
}
}
}
这样就可以针对性的对部分系统反色不起作用的View进行补充适配,以达到一个相对完美适配深色模式的效果
网友评论