×

Android 颜色处理

96
zhuguohui
2018.02.26 15:28* 字数 1726

需求

在最近的项目开发中遇到了这种UI,顶部有一组彩色圆形按钮。选中以后颜色会加深。这样的按钮一共有十二个。

这里写图片描述

而设计师切的图就是把所有的按钮全部切下来了。

这里写图片描述

最简单的实现方式就是使用selector来实现,按下状态和选中状态不同图片的显示。但是这样就会涉及到一个问题。这样的图片有24张,这样的selector有12个。而这些顶部channel数量如果增加又该怎么办。不是很灵活。于是我就想写一个drawable。它需要满足以下需要:

  1. 能根据背景色自动计算一个加深后的颜色。这样就只需要设置背景色就可以使用了。
  2. 能对按压和选中状态由响应。
  3. 能设置选择后颜色变深的区域大小。

实现

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)

g3.png

作用在图片上效果是这样的。

这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述

而ColorMatrix可以通过构造函数直接专递一个float数组。但是对于普通人来说,是很难知道这些参数是怎么调的。于是它也提供了一些简便的函数。

  1. 色调调节setRotate(int axis, float degrees): 其中第一个参数axis是固定可选的,为0、1、2,分别表示改变Red、Green、Blue三个颜色分量,第二个参数degrees表示旋转角度,由旋转角度通过三角函数变换得到不同的矩阵,其中a为角度,单位为°。
  2. 饱和度调节setSaturation(float sat)
  3. 亮度调节setScale(float rScale, float gScale, float bScale,float aScale)
  4. 效果叠加 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的实现原理

Android开发
Web note ad 1