本文已授权给 鸿洋的公众号 转发,其他平台转发请注明原文地址:https://www.yuque.com/lenebf/fl1svo/sagoxt/edit

一、看招

之前在鸿洋的公众号看到App黑白化方案的探索,那叫一个妙,我们先回顾下当时的招式

Window window = activity.getWindow();                
if (window == null) {                                
    return;                                          
}                                                    
View view = window.getDecorView();                   
Paint paint = new Paint();                           
ColorMatrix cm = new ColorMatrix(); 
// 关键起作用的代码,Saturation,翻译成中文就是饱和度的意思。
// 官方文档说明:A value of 0 maps the color to gray-scale. 1 is identity.
// 原来如此,666
cm.setSaturation(0f);                                
paint.setColorFilter(new ColorMatrixColorFilter(cm));
view.setLayerType(View.LAYER_TYPE_HARDWARE, paint);  

normal-1.pnggray-2.png

二、拆招

我们的操作对象是 ColorMatrix,它具体是个什么东东官方文档最清楚了,把文档请出来:

4x5 matrix for transforming the color and alpha components of a Bitmap. The matrix can be passed as single array, and is treated as follows:

  [ a, b, c, d, e,

    f, g, h, i, j,

    k, l, m, n, o,

    p, q, r, s, t ]

When applied to a color [R, G, B, A], the resulting color is computed as:

   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;

「人」如其名,它是个 4x5 的矩阵,通过矩阵乘法和加法实现了颜色的转换,没看明白?这样能明白了吧:



那设置饱和度是如何影响颜色的呢?来看看 ColorMatrix.setSaturation 的具体实现

/**
 * Create a new colormatrix initialized to identity (as if reset() had
 * been called).
 */
public ColorMatrix() {
    reset();
}

// 原始矩阵长这样
/**
 * Set this colormatrix to identity:
 * [ 1 0 0 0 0   - red vector
 *   0 1 0 0 0   - green vector
 *   0 0 1 0 0   - blue vector
 *   0 0 0 1 0 ] - alpha vector
 */
public void reset() {
    final float[] a = mArray;
    
    for (int i = 19; i > 0; --i) {
        a[i] = 0;
    }
    a[0] = a[6] = a[12] = a[18] = 1;
}

/**
 * Set the matrix to affect the saturation of colors.
 *
 * @param sat A value of 0 maps the color to gray-scale. 1 is identity.
 */
public void setSaturation(float sat) {
    reset();
    float[] m = mArray;
    final float invSat = 1 - sat;
    final float R = 0.213f * invSat;
    final float G = 0.715f * invSat;
    final float B = 0.072f * invSat;
    m[0] = R + sat; m[1] = G;       m[2] = B;
    m[5] = R;       m[6] = G + sat; m[7] = B;
    m[10] = R;      m[11] = G;      m[12] = B + sat;
}

当我们设置饱和度sat为0时,上面矩阵里的a, f, k都变成了0.213fb, g, l都变成了0.715fc, h, m都变成了0.072f,代入计算公式发现R, G, B取值变成一样了,这不就变成黑白色了吗!

三、亮招

通过前面的分析已经了清楚设置饱和度最终是通过修改矩阵来实现黑白色效果的,那我们直接修改矩阵呢?比如护眼模式,不就是去蓝光吗!上代码:

Window window = activity.getWindow();
if (window == null) {
    return;
}
View view = window.getDecorView();
Paint paint = new Paint();
// 我们把蓝色减弱为原来的0.7
ColorMatrix cm = new ColorMatrix(new float[]{
        1, 0, 0, 0, 0,
        0, 1, 0, 0, 0,
        0, 0, 0.7f, 0, 0,
        0, 0, 0, 1, 0
});
paint.setColorFilter(new ColorMatrixColorFilter(cm));
view.setLayerType(View.LAYER_TYPE_HARDWARE, paint);

eye_protect-1.png哟,真是神奇。

四、连招

如果前面的分析你都看懂了,你可能意识到这个ColorMatrix玩法还有很多,比如夜间模式,可能就是反色+降低亮度,反色代码如下:

Window window = activity.getWindow();
if (window == null) {
    return;
}
View view = window.getDecorView();
view.addOnLayoutChangeListener(this);
if (view instanceof ViewGroup) {
    takeOffColor((ViewGroup) view);
}
Paint paint = new Paint();
ColorMatrix cm = new ColorMatrix(new float[]{
        -1, 0, 0, 0, 255,
        0, -1, 0, 0, 255,
        0, 0, -1, 0, 255,
        0, 0, 0, 1, 0
});
paint.setColorFilter(new ColorMatrixColorFilter(cm));
view.setLayerType(View.LAYER_TYPE_HARDWARE, paint);

night1.png

但是有一个很明显的问题,因为我们是对ActivityDecorView做了颜色转换,ImageView是它的Child,所以图片也被反色了,在购物的场景下我想买黄色的衣服,结果收到货确实蓝色的,闹呢?那我们能不能单独给ImageView设置一个反向的矩阵让图片恢复原来的颜色呢?直接上逆矩阵:

// 遍历查找ImageView,对其设置逆矩阵
int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
    final View childView = parent.getChildAt(i);
    if (childView instanceof ViewGroup) {
        takeOffColor((ViewGroup) childView);
    } else if (childView instanceof ImageView) {
        Paint paint = new Paint();
        ColorMatrix cm = new ColorMatrix(new float[]{
                -1, 0, 0, 0, 255,
                0, -1, 0, 0, 255,
                0, 0, -1, 0, 255,
                0, 0, 0, 1, 0
        });
        paint.setColorFilter(new ColorMatrixColorFilter(cm));
        childView.setLayerType(View.LAYER_TYPE_HARDWARE, paint);
    }
}

night2-1.png完美!!!

五、罩门

所谓罩门,就是功夫练不到的地方。前面看起来招招毙命,其实也有需要注意的地方,颜色转换算法是通过ColorMatrix完成了,但我们还借用了setLayerType将矩阵传递给底层的。

支持的ViewType

通过源码我们发现,三种Type中只有LAYER_TYPE_HARDWARELAYER_TYPE_SOFTWARE支持颜色转换:

public void setLayerType(int layerType, Paint paint) {
    if (layerType < LAYER_TYPE_NONE || layerType > LAYER_TYPE_HARDWARE) {
        throw new IllegalArgumentException("Layer type can only be one of: LAYER_TYPE_NONE, "
                + "LAYER_TYPE_SOFTWARE or LAYER_TYPE_HARDWARE");
    }
    if (layerType == mLayerType) {
        // 1. LAYER_TYPE_NONE 不支持paint参数
        if (layerType != LAYER_TYPE_NONE && paint != mLayerPaint) {
            mLayerPaint = paint == null ? new Paint() : paint;
            invalidateParentCaches();
            invalidate(true);
        }
        return;
    }
    // Destroy any previous software drawing cache if needed
    switch (mLayerType) {
        case LAYER_TYPE_HARDWARE:
            destroyLayer(false);
            // fall through - non-accelerated views may use software layer mechanism instead
        case LAYER_TYPE_SOFTWARE:
            destroyDrawingCache();
            break;
        default:
            break;
    }
    mLayerType = layerType;
    // 2. LAYER_TYPE_NONE 不支持paint参数
    final boolean layerDisabled = mLayerType == LAYER_TYPE_NONE;
    mLayerPaint = layerDisabled ? null : (paint == null ? new Paint() : paint);
    mLocalDirtyRect = layerDisabled ? null : new Rect();
    invalidateParentCaches();
    invalidate(true);
}

硬件加速的限制

当我们使用LAYER_TYPE_HARDWARE,我们就得注意硬件加速的限制了。从 Android 3.0(API 级别 11)开始,Android 2D 渲染管道支持硬件加速,如果您的目标 API 级别为 14 及更高级别,则硬件加速默认处于启用状态。

下表介绍了各种绘制操作在各个 API 级别的支持级别:

image.png

当我们对自绘控件使用上面的招式时,就要注意是否使用到了表格中不支持的绘制操作,如果用了可以换成LAYER_TYPE_SOFTWARE,但会牺牲掉硬件加速带来的性能提升。

六、一起做裙子

说了这么多,不放代码是不是很过分:https://github.com/lenebf/AppDress,对!就是给App穿裙子。除了上面这些效果,我们能够玩的还有很多,比如增强红色,R, G, B互换等。大家有什么问题或者想法可以一起来维护这个Lib,用或者尝试的人越多,暴露出来的问题就越多,等着问题修复后,这个库也就越稳定,以后如果遇到类似的需求,我们用起来就方便了。