Android 颜色处理
需求
在最近的项目开发中遇到了这种UI,顶部有一组彩色圆形按钮。选中以后颜色会加深。这样的按钮一共有十二个。
这里写图片描述而设计师切的图就是把所有的按钮全部切下来了。
这里写图片描述最简单的实现方式就是使用selector来实现,按下状态和选中状态不同图片的显示。但是这样就会涉及到一个问题。这样的图片有24张,这样的selector有12个。而这些顶部channel数量如果增加又该怎么办。不是很灵活。于是我就想写一个drawable。它需要满足以下需要:
- 能根据背景色自动计算一个加深后的颜色。这样就只需要设置背景色就可以使用了。
- 能对按压和选中状态由响应。
- 能设置选择后颜色变深的区域大小。
实现
1.颜色的变化
尝试1ColorMatrix
在Android中是可以对颜色就行调整的。可以使用的ColorMatrix。其本质是一个长度为20的int数组,用来表示一个4X5的矩阵。因为位图中每一个像素点都是由 红黄蓝和透明度确定的。也就是R,G,B,A。 可以把它表示成一个四维向量 但是一般使用[R,G,B,A,1] 来表示。
设颜色矩阵为m,颜色分量矩阵为C。
g1.png颜色分量C’是颜色矩阵m乘以颜色分量矩阵新加一列值为1的5x1的矩阵所得的4x1矩阵,矩阵乘法公式可看注解
g2.png通过不同的矩阵和位图的每一个颜色进行运算,会得到各种不同的效果。下面分别是。
灰度效果(a)、图像反转(b)、怀旧效果(c)、高饱和度(d)
作用在图片上效果是这样的。
这里写图片描述 这里写图片描述 这里写图片描述 这里写图片描述而ColorMatrix可以通过构造函数直接专递一个float数组。但是对于普通人来说,是很难知道这些参数是怎么调的。于是它也提供了一些简便的函数。
- 色调调节setRotate(int axis, float degrees): 其中第一个参数axis是固定可选的,为0、1、2,分别表示改变Red、Green、Blue三个颜色分量,第二个参数degrees表示旋转角度,由旋转角度通过三角函数变换得到不同的矩阵,其中a为角度,单位为°。
- 饱和度调节setSaturation(float sat)
- 亮度调节setScale(float rScale, float gScale, float bScale,float aScale)
- 效果叠加 preConcat(ColorMatrix prematrix)和postConcat(ColorMatrix postmatrix)两个方法分别是将目标效果矩阵放在本矩阵之前和放在
使用方式如下。
// 创建副本,用于将处理过的图片展示出来而不影响原图,Android系统也不允许直接修改原图
Bitmap bmp = Bitmap.createBitmap(bitmap.getWidth(),bitmap.getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bmp);
Paint paint = new Paint();
// 修改色调,即色彩矩阵围绕某种颜色分量旋转
ColorMatrix rotateMatrix = new ColorMatrix();
// 0,1,2分别代表像素点颜色矩阵中的Red,Green,Blue分量
rotateMatrix.setRotate(0,rotate);
rotateMatrix.setRotate(1,rotate);
rotateMatrix.setRotate(2,rotate);
// 修改饱和度
ColorMatrix saturationMatrix = new ColorMatrix();
saturationMatrix.setSaturation(saturation);
// 修改亮度,即某种颜色分量的缩放
ColorMatrix scaleMatrix = new ColorMatrix();
// 分别代表三个颜色分量的亮度
scaleMatrix.setScale(scale,scale,scale,1);
//将三种效果结合
ColorMatrix imageMatrix = new ColorMatrix();
imageMatrix.postConcat(rotateMatrix);
imageMatrix.postConcat(saturationMatrix);
imageMatrix.postConcat(scaleMatrix);
paint.setColorFilter(new ColorMatrixColorFilter(imageMatrix));
canvas.drawBitmap(bitmap,0,0,paint);
return bmp;
由于我这里只需要获取一种深色,而不是修改一张图片。所以我直接把float数组取出,把背景色分离成[R,G,B,1]的形式。通过矩阵乘法,计算得到的颜色。这里通过设置饱和度的方式来获取深色。
ColorMatrix colorMatrix = new ColorMatrix();
colorMatrix.setSaturation(pressSaturation);
float[] m = colorMatrix.getArray();
int R = Color.red(normalColor);
int G = Color.green(normalColor);
int B = Color.blue(normalColor);
int A = Color.alpha(normalColor);
/**
* [ a, b, c, d, e,
* f, g, h, i, j,
* k, l, m, n, o,
* p, q, r, s, t ]
*
* R = a*R + b*G + c*B + d*A + e;
* G = f*R + g*G + h*B + i*A + j;
* B= k*R + l*G + m*B + n*A + o;
* A = p*R + q*G + r*B + s*A + t
*/
int nR = (int) (m[0] * R + m[1] * G + m[2] * B + m[3] * A + m[4]);
int nG = (int) (m[5] * R + m[6] * G + m[7] * B + m[8] * A + m[9]);
int nB = (int) (m[10] * R + m[11] * G + m[12] * B + m[13] * A + m[14]);
int nA = (int) (m[15] * R + m[16] * G + m[17] * B + m[18] * A + m[19]);
pressColor=Color.argb(nA,nR,nG,nB);
但是得到的效果是这样的。
这里写图片描述
第一个按钮深色区域变成深绿了。饱和度为0.4。我觉得不对于是设置为2.0结果成这样了。变得更绿了。
这里写图片描述后来我想了下,为什么不去为设计师怎么变化的呢。结果设计师告诉我她是靠眼睛选出来的。没有公式。但是像我这样懒的人肯定不能这样噻。
尝试2 HSV色彩空间
HSV(Hue, Saturation, Value)是根据颜色的直观特性由A. R. Smith在1978年创建的一种颜色空间, 也称六角锥体模型(Hexcone Model)。
这个模型中颜色的参数分别是:色调(H),饱和度(S),明度(V)。
色调H
用角度度量,取值范围为0°~360°,从红色开始按逆时针方向计算,红色为0°,绿色为120°,蓝色为240°。它们的补色是:黄色为60°,青色为180°,品红为300°;
饱和度S
饱和度S表示颜色接近光谱色的程度。一种颜色,可以看成是某种光谱色与白色混合的结果。其中光谱色所占的比例愈大,颜色接近光谱色的程度就愈高,颜色的饱和度也就愈高。饱和度高,颜色则深而艳。光谱色的白光成分为0,饱和度达到最高。通常取值范围为0%~100%,值越大,颜色越饱和。
明度V
明度表示颜色明亮的程度,对于光源色,明度值与发光体的光亮度有关;对于物体色,此值和物体的透射比或反射比有关。通常取值范围为0%(黑)到100%(白)。
至于HSV和RGB的关系可以看下图。
这是RGB的色彩空间 ,三个坐标轴分别表示RGB
这里写图片描述但是如果我们以黄色,紫色,青色为坐标轴。则得到的就是SHV色彩空间。
这里写图片描述样子如下。
这里写图片描述而HSV和RGB的转换关系如图。
这里写图片描述看起来很复杂,但是使用起来很简单。在Android的Color类中有如下方法。
这里写图片描述而由于S是负责色彩鲜艳度的。那么我们增加S就行了。
/**
* @param normalColor 正常颜色
* @param darkRatio 加深度 0-1.0
* @param darkAreaRatio 按下时深色区域占整drawable的大小
*/
public DarkColorDrawable(int normalColor, float darkRatio, float darkAreaRatio) {
paint = new Paint();
paint.setColor(normalColor);
paint.setStyle(Paint.Style.FILL);
paint.setAntiAlias(true);
this.normalColor = normalColor;
this.darkAreaRatio = darkAreaRatio;
float[] hsv = new float[3];
Color.colorToHSV(normalColor, hsv);
hsv[1] += darkRatio;
pressColor = Color.HSVToColor(hsv);
}
效果就正常了
这里写图片描述2.Drawable响应不同状态
要响应不同的状态就得搞清楚,View是如何更具不同的状态刷新drawable的。关键代码在这里。
View的源码
当View状态发生变化的时候,比如被点击以后。会触发这个方法。这个方法的作用是询问drawable是否支持不同状态,如果支持,询问drawable当前这个状态是否需要刷新。
protected void drawableStateChanged() {
final int[] state = getDrawableState();
boolean changed = false;
final Drawable bg = mBackground;
//isStateful() 判断是否支持不同状态
if (bg != null && bg.isStateful()) {
//如果setState()返回true表示,要刷新。
//而这里的state是一个int数租,每一个值表示特定的状态。
//比如android.R.attr.state_pressed等于16843324
//如果这个数组中包含16843324 那么表示当前View的状态是有点击的。
//我们可以把自己需要响应的所有状态的int值,写入到一个数组中。
//如果指明不能是该状态,则使用相应的负数。比如-16843324。表示不能是在点击状态。
changed |= bg.setState(state);
}
//其他同理
final Drawable hl = mDefaultFocusHighlight;
if (hl != null && hl.isStateful()) {
changed |= hl.setState(state);
}
final Drawable fg = mForegroundInfo != null ? mForegroundInfo.mDrawable : null;
if (fg != null && fg.isStateful()) {
changed |= fg.setState(state);
}
if (mScrollCache != null) {
final Drawable scrollBar = mScrollCache.scrollBar;
if (scrollBar != null && scrollBar.isStateful()) {
changed |= scrollBar.setState(state)
&& mScrollCache.state != ScrollabilityCache.OFF;
}
}
if (mStateListAnimator != null) {
mStateListAnimator.setState(state);
}
if (changed) {
invalidate();
}
}
而Drawable在需要更新自己的时候调用invalidateSelf() 就行了。该方法会调用 callback.invalidateDrawable()。
public void invalidateSelf() {
final Callback callback = getCallback();
if (callback != null) {
callback.invalidateDrawable(this);
}
}
而View 真是实现的该Callback。所有View后收到消息。发起重绘,实现状态的改变。
最终的Drawable代码如下。
/**
* Created by zhuguohui on 2018/2/24.
*/
public class DarkColorDrawable extends Drawable {
Paint paint;
private static final int[] PRESS_SET = new int[]{android.R.attr.state_pressed};
private static final int[] SELECTED_SET = new int[]{android.R.attr.state_selected};
private int r;
private final int pressColor;
private boolean press = false;
private int pressR;
private int normalColor;
private float darkAreaRatio = 1.0f;
/**
* @param normalColor 正常颜色
* @param darkRatio 加深度 0-1.0
* @param darkAreaRatio 按下时深色区域占整drawable的大小
*/
public DarkColorDrawable(int normalColor, float darkRatio, float darkAreaRatio) {
paint = new Paint();
paint.setColor(normalColor);
paint.setStyle(Paint.Style.FILL);
paint.setAntiAlias(true);
this.normalColor = normalColor;
this.darkAreaRatio = darkAreaRatio;
float[] hsv = new float[3];
Color.colorToHSV(normalColor, hsv);
hsv[1] += darkRatio;
pressColor = Color.HSVToColor(hsv);
}
@Override
protected void onBoundsChange(Rect bounds) {
int size = Math.min(getBounds().width(), getBounds().height());
r = size / 2;
pressR = (int) (r * darkAreaRatio);
}
@Override
public void draw(@NonNull Canvas canvas) {
paint.setColor(normalColor);
canvas.drawCircle(getBounds().centerX(), getBounds().centerY(), r, paint);
if (press) {
paint.setColor(pressColor);
canvas.drawCircle(getBounds().centerX(), getBounds().centerY(), pressR, paint);
}
}
@Override
public void setAlpha(int alpha) {
}
@Override
public void setColorFilter(@Nullable ColorFilter colorFilter) {
}
@Override
public int getOpacity() {
return PixelFormat.OPAQUE;
}
//支持不同状态
@Override
public boolean isStateful() {
return true;
}
@Override
protected boolean onStateChange(int[] state) {
if (StateSet.stateSetMatches(PRESS_SET, state) || StateSet.stateSetMatches(SELECTED_SET, state)) {
press = true;
} else {
press = false;
}
invalidateSelf();
return true;
}
}
总结
这短短的93行代码,就实现了24张图,12个selector的功能。而且更高效,更易拓展。这就是知识深入,运用灵活的结果。未来还会严格要求自己,向高级工程师迈进。
参考
Android图片色彩处理ColorMatrix
百度百科HSV
由RGB到HSV颜色空间的理解
Android Selector的实现原理