前言
一個朋友跟我說,他們公司的變態產品經理又提了新的需求,要他實現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.// 原來如此,666cm.setSaturation(0f);paint.setColorFilter(new ColorMatrixColorFilter(cm));view.setLayerType(View.LAYER_TYPE_HARDWARE, paint);
沒了解過的可以看這兩篇複習下:
App 黑白化實現探索,有一行程式碼實現的方案嗎?
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.7ColorMatrix 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
對!就是給App穿裙子。除了上面這些效果,我們能夠玩的還有很多,比如增強紅色,R, G, B互換等。
大家有什麼問題或者想法可 以一起來維護這個Lib,用或者嘗試的人越多,暴露出來的問題就越多,等著問題修復後,這個庫也就越穩定,以後如果遇到類似的需求,我們用起來就方便了。
最後看完這篇文,是不是再也不怕產品經理提新的需求了?
人生不可能一帆風順,有高峰自然有低谷,要相信,那些打不倒我們的,終將使我們更強大,要做自己的擺渡人。
這裡整合了很多底層原理的知識,還有我認為比較重要的學習方向和知識點,整理成了PDF文件,私信我關鍵字“資料”,免費獲取《Android元件化強化專案實戰(附原始碼) 》還有《最全Android相關原始碼解析》、《Android效能最佳化學習手冊》、《BATZ大廠面試真題》,歡迎大家一起學習進步!