前言
一個朋友跟我說,他們公司的變態產品經理又提了新的需求,要他實現APP的顏色主題切換,問我有沒有什麼好的建議。
我掏出了收藏夾塵封已久的精華。
作者:lenebf
原文地址:https://www.yuque.com/lenebf/fl1svo/sagoxt
看招
之前在鴻洋的公衆號看到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);
沒了解過的可以看這兩篇複習下:
App 黑白化實現探索2, 發現了一種更方便的方案,我被錘了!
拆招
我們的操作對象是 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’ = aR + bG + cB + dA + e;
G’ = fR + gG + hB + iA + j;
B’ = kR + lG + mB + nA + o;
A’ = pR + qG + rB + sA + 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.213f,b, g, l都變成了0.715f,c, 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);
喲,真是神奇。
連招
如果前面的分析你都看懂了,你可能意識到這個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);
但是有一個很明顯的問題,因爲我們是對Activity的DecorView做了顏色轉換,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);
}
}
完美!!!
罩門
所謂罩門,就是功夫練不到的地方。前面看起來招招斃命,其實也有需要注意的地方,顏色轉換算法是通過ColorMatrix完成了,但我們還借用了setLayerType將矩陣傳遞給底層的。
支持的ViewType
通過源碼我們發現,三種Type中只有LAYER_TYPE_HARDWARE和LAYER_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 級別的支持級別:
當我們對自繪控件使用上面的招式時,就要注意是否使用到了表格中不支持的繪製操作,如果用了可以換成LAYER_TYPE_SOFTWARE,但會犧牲掉硬件加速帶來的性能提升。
硬件加速官方說明文檔:
https://developer.android.com/guide/topics/graphics/hardware-accel
一起做裙子
說了這麼多,不放代碼是不是很過分:
https://github.com/xieyuliang/Note-Android
對!就是給App穿裙子。除了上面這些效果,我們能夠玩的還有很多,比如增強紅色,R, G, B互換等。
大家有什麼問題或者想法可 以一起來維護這個Lib,用或者嘗試的人越多,暴露出來的問題就越多,等着問題修復後,這個庫也就越穩定,以後如果遇到類似的需求,我們用起來就方便了。
最後
看完這篇文,是不是再也不怕產品經理提新的需求了?
人生不可能一帆風順,有高峯自然有低谷,要相信,那些打不倒我們的,終將使我們更強大,要做自己的擺渡人。
這裏整合了很多底層原理的知識,還有我認爲比較重要的學習方向和知識點,放在了我的GitHub,歡迎大家一起學習進步。