本文已受權公衆號 hongyangAndroid 原創首發。css
4 月 4 日這一天,很多 網站、App 都經過黑白化,表達了深切的哀悼。html
這篇文章咱們純談技術。android
我在當天,也給wanandroid.com上線了黑白化效果:git
你們可能作 app 比較多,網頁端全站實現這一的效果,只須要一句話:github
html {filter:progid:DXImageTransform.Microsoft.BasicImage(grayscale=1);-webkit-filter: grayscale(100%);}
複製代碼
只要給 html 加一句css 樣式就能夠了,你能夠理解爲給整個頁面添加了一個灰度效果。web
就完成了,真的很方便。canvas
回頭看 app,你們都以爲開發起來比較麻煩,你們廣泛的思路就是:bash
這麼看起來工做量仍是很大的。app
後來我就在思考,既然 web 端能夠這麼給整個頁面加一個灰度的效果,咱們 app 應該也能夠呀?ide
那咱們如何給app頁面加一個灰度效果呢?
咱們的 app 頁面正常狀況下,其實也是 Canvas 繪製出來的對吧?
Canvas 對應的相關 API 確定也是支持灰度的。
那麼是否是咱們在控件繪製的時候,好比 draw 以前設置個灰度效果就能夠呢?
好像發現了什麼玄機。
那麼咱們首先經過 ImageView 來驗證一下灰度效果的可行性。
咱們編寫個自定義的 ImageView,叫作:GrayImageView
佈局文件是這樣的:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".TestActivity">
<ImageView
android:layout_width="100dp"
android:layout_height="wrap_content"
android:src="@mipmap/logo">
</ImageView>
<com.imooc.imooc_wechat_app.view.GrayImageView
android:layout_width="100dp"
android:layout_height="wrap_content"
android:src="@mipmap/logo" />
</LinearLayout>
複製代碼
很簡單,咱們放了一個 ImageView 用來作對比。
看下 GrayImageView 的代碼:
public class GrayImageView extends AppCompatImageView {
private Paint mPaint = new Paint();
public GrayImageView(Context context, AttributeSet attrs) {
super(context, attrs);
ColorMatrix cm = new ColorMatrix();
cm.setSaturation(0);
mPaint.setColorFilter(new ColorMatrixColorFilter(cm));
}
@Override
public void draw(Canvas canvas) {
canvas.saveLayer(null, mPaint, Canvas.ALL_SAVE_FLAG);
super.draw(canvas);
canvas.restore();
}
}
複製代碼
在分析代碼以前,咱們看下效果圖:
很完美,咱們成功把 wanandroid圖標搞成了灰色。
看一眼代碼,代碼很是簡單,咱們複寫了draw 方法,在該方法中給canvas 作了一下特殊處理。
什麼特殊處理呢?其實就是設置了一個灰度效果。
在 App中,咱們對於顏色的處理不少時候會採用顏色矩陣,是一個4*5的矩陣,原理是這樣的:
[ a, b, c, d, e,
f, g, h, i, j,
k, l, m, n, o,
p, q, r, s, t ]
複製代碼
應用到一個具體的顏色[R, G, B, A]上,最終顏色的計算是這樣的:
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;
複製代碼
是否是看起來很難受,沒錯我也很難受,看到代數就煩。
既然你們都難受,那麼Android 就比較貼心了,給咱們搞了個ColorMartrix類,這個類對外提供了不少 API,你們直接調用 API 就能獲得大部分想要的效果了,除非你有特別特殊的操做,那麼能夠本身經過矩陣去運算。
像灰度這樣的效果,咱們能夠經過飽和度 API來操做:
setSaturation(float sat)
複製代碼
傳入 0 就能夠了,你去看源碼,底層傳入了一個特定的矩陣去作的運算。
ok,好了,忘掉上面說的,就記得你有個 API 能把 canvas 繪製出來的東西搞成灰的就好了。
那麼咱們已經實現了把 ImageView 弄成了灰度,TextView 能夠嗎?Button能夠嗎?
咱們來試試TextView、Button。
代碼徹底同樣哈,其實就是換了個實現類,例如 GrayTextView:
public class GrayTextView extends AppCompatTextView {
private Paint mPaint = new Paint();
public GrayTextView(Context context, AttributeSet attrs) {
super(context, attrs);
ColorMatrix cm = new ColorMatrix();
cm.setSaturation(0);
mPaint.setColorFilter(new ColorMatrixColorFilter(cm));
}
@Override
public void draw(Canvas canvas) {
canvas.saveLayer(null, mPaint, Canvas.ALL_SAVE_FLAG);
super.draw(canvas);
canvas.restore();
}
}
複製代碼
沒任何區別,GrayButton 就不貼了,咱們看佈局文件:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".TestActivity">
<ImageView
android:layout_width="100dp"
android:layout_height="wrap_content"
android:src="@mipmap/logo">
</ImageView>
<com.imooc.imooc_wechat_app.view.GrayImageView
android:layout_width="100dp"
android:layout_height="wrap_content"
android:src="@mipmap/logo" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="鴻洋真帥"
android:textColor="@android:color/holo_red_light"
android:textSize="30dp" />
<com.imooc.imooc_wechat_app.view.GrayTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="鴻洋真帥"
android:textColor="@android:color/holo_red_light"
android:textSize="30dp" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="鴻洋真帥"
android:textColor="@android:color/holo_red_light"
android:textSize="30dp" />
<com.imooc.imooc_wechat_app.view.GrayButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="鴻洋真帥"
android:textColor="@android:color/holo_red_light"
android:textSize="30dp" />
</LinearLayout>
複製代碼
對應的效果圖:
能夠看到 TextView,Button 也成功的把紅色的字體換成了灰色。
這個時候你是否是突然感受本身會了?
其實咱們只要把各類相關的 View 換成這種自定義 View,利用 appcompat換膚那一套,不須要 Server 參與了,客戶端搞搞就好了。
是嗎?咱們須要把全部的 View 都換成自定義的 View嗎?
這聽起來成本也挺高呀。
再想一想還有更簡單的嗎?
雖然剛纔的佈局文件很簡單,可是邀請你再去看一眼剛纔的佈局文件,我要問你問題了:
看好了吧。
有沒有一點茅塞頓開!
咱們須要一個個自定義嗎?
父 View 都是 LinearLayout,咱們搞個 GrayLinearLayout 不就好了,其內部的 View 都會變成灰色,畢竟 Canvas 對象是往下傳遞的。
咱們來試試:
GrayLinearLayout:
public class GrayLinearLayout extends LinearLayout {
private Paint mPaint = new Paint();
public GrayLinearLayout(Context context, AttributeSet attrs) {
super(context, attrs);
ColorMatrix cm = new ColorMatrix();
cm.setSaturation(0);
mPaint.setColorFilter(new ColorMatrixColorFilter(cm));
}
@Override
public void draw(Canvas canvas) {
canvas.saveLayer(null, mPaint, Canvas.ALL_SAVE_FLAG);
super.draw(canvas);
canvas.restore();
}
@Override
protected void dispatchDraw(Canvas canvas) {
canvas.saveLayer(null, mPaint, Canvas.ALL_SAVE_FLAG);
super.dispatchDraw(canvas);
canvas.restore();
}
}
複製代碼
代碼很簡單,可是注意有個細節,咱們也複寫了 dispatchDraw,爲何呢?本身思考:
咱們更換下 xml:
<?xml version="1.0" encoding="utf-8"?>
<com.imooc.imooc_wechat_app.view.GrayLinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".TestActivity">
<ImageView
android:layout_width="100dp"
android:layout_height="wrap_content"
android:src="@mipmap/logo" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="鴻洋真帥"
android:textColor="@android:color/holo_red_light"
android:textSize="30dp" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="鴻洋真帥"
android:textColor="@android:color/holo_red_light"
android:textSize="30dp" />
</com.imooc.imooc_wechat_app.view.GrayLinearLayout>
複製代碼
咱們放了藍色 Logo 的 ImageView,紅色字體的 TextView 和 Button,看一眼效果:
完美!
是否是又有點茅塞頓開!
只要咱們換了 咱們設置的Activity 的根佈局就能夠了!
Activity 的根佈局多是 LinearLayout,FrameLayout,RelativeLayout,ConstraintLayout...
換個雞兒...這得換到啥時候,跟剛纔有啥區別。
還有思路嗎,沒什麼肯定的 View 嗎?
再想一想。
咱們的設置的 Activity 的根佈局會放在哪?
android.id.content
複製代碼
是否是這個 Content View 上面?
這個 content view 目前一直是 FrameLayout !
那麼咱們只要在生成這個android.id.content 對應的 FrameLayout,換成 GrayFrameLayout 就能夠了。
怎麼換呢?
appcompat 那一套?去搞 LayoutFactory?
確實能夠哈,可是那樣要設置 LayoutFactory,還須要考慮 appcompat 相關邏輯。
有沒有那種不須要去修改什麼流程的方案?
還真是有的。
咱們的 AppCompatActivity,能夠複寫 onCreateView 的方法,這個方法其實也是LayoutFactory在構建 View 的時候回調出來的,通常對應其內部的mPrivateFactory。
他的優先級低於 Factory、Factory2,相關代碼:
if (mFactory2 != null) {
view = mFactory2.onCreateView(parent, name, context, attrs);
} else if (mFactory != null) {
view = mFactory.onCreateView(name, context, attrs);
} else {
view = null;
}
if (view == null && mPrivateFactory != null) {
view = mPrivateFactory.onCreateView(parent, name, context, attrs);
}
if (view == null) {
final Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = context;
try {
if (-1 == name.indexOf('.')) {
view = onCreateView(parent, name, attrs);
} else {
view = createView(name, null, attrs);
}
} finally {
mConstructorArgs[0] = lastContext;
}
}
複製代碼
可是目前對於 FrameLayout,appcompat 並無特殊處理,也就是說你能夠在 onCreateView 回調中去構造 FrameLayout 對象。
很簡單,就複寫 Activity 的 onCreateView 方法便可:
public class TestActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
}
@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
return super.onCreateView(name, context, attrs);
}
}
複製代碼
咱們在這個方法中把content view 對應的 FrameLayout 換成 GrayFrameLayout.
@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
try {
if ("FrameLayout".equals(name)) {
int count = attrs.getAttributeCount();
for (int i = 0; i < count; i++) {
String attributeName = attrs.getAttributeName(i);
String attributeValue = attrs.getAttributeValue(i);
if (attributeName.equals("id")) {
int id = Integer.parseInt(attributeValue.substring(1));
String idVal = getResources().getResourceName(id);
if ("android:id/content".equals(idVal)) {
GrayFrameLayout grayFrameLayout = new GrayFrameLayout(context, attrs);
// grayFrameLayout.setWindow(getWindow());
return grayFrameLayout;
}
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
return super.onCreateView(name, context, attrs);
}
複製代碼
代碼應該都能看明白吧,咱們找到 id 是 android:id/content 的,換成了咱們的 GrayFrameLayout,這塊代碼邏輯我沒細測,可能會有一些異常,你們本身整理下。
最後看一眼GrayFrameLayout:
public class GrayFrameLayout extends FrameLayout {
private Paint mPaint = new Paint();
public GrayFrameLayout(Context context, AttributeSet attrs) {
super(context, attrs);
ColorMatrix cm = new ColorMatrix();
cm.setSaturation(0);
mPaint.setColorFilter(new ColorMatrixColorFilter(cm));
}
@Override
protected void dispatchDraw(Canvas canvas) {
canvas.saveLayer(null, mPaint, Canvas.ALL_SAVE_FLAG);
super.dispatchDraw(canvas);
canvas.restore();
}
@Override
public void draw(Canvas canvas) {
canvas.saveLayer(null, mPaint, Canvas.ALL_SAVE_FLAG);
super.draw(canvas);
canvas.restore();
}
}
複製代碼
好了,運行一下,看下效果:
效果 ok。
而後把onCreateView 這坨代碼,放到你的 BaseActivity裏面就好了。
什麼,沒有 BaseActivity?
...
說到如今,都沒有脫離出一個 Activity。
咱們找個複雜點的項目驗證下好吧。
我去 github 找個 wanandroid 的 Java 開源項目:
選中了:
導入後,只要在 BaseActivity 裏面添加咱們剛纔的代碼就能夠了。
運行效果圖:
恩,沒錯,webview 裏面的文字,圖片都黑白化了。
沒發現啥問題,這樣一個 app 就徹底黑白化了。
等等,我發現狀態欄沒變,狀態欄是否是有 API,本身在 BaseActivity 裏面調用一行代碼處理哈。
號內回覆:「文章寫的真好」,獲取黑白化後的 apk,本身體驗。
其實沒運行出來問題有些遺憾。
那我自爆幾個問題吧。
1. 若是 Activity的 Window 設置了 background,咋辦呢?
由於咱們處理的是 content view,確定在 window 之下,確定覆蓋不到 window 的 backgroud。
咋辦咋辦?
不要慌。
咱們生成的GrayFrameLayout也是能夠設置 background 的?
if ("android:id/content".equals(idVal)) {
GrayFrameLayout grayFrameLayout = new GrayFrameLayout(context, attrs);
grayFrameLayout.setBackgroundDrawable(getWindow().getDecorView().getBackground());
return grayFrameLayout;
}
複製代碼
注意若是你的getWindow().setBackgroundDrawable調用過早,可能會致使getDecorView().getBackground()取不到,那麼可讓grayFrameLayout.setBackgroundDrawable在實際繪製以前調用。
若是你是theme 中設置的 windowBackground,那麼須要從 theme 裏面提取 drawable,參考代碼以下:
TypedValue a = new TypedValue();
getTheme().resolveAttribute(android.R.attr.windowBackground, a, true);
if (a.type >= TypedValue.TYPE_FIRST_COLOR_INT && a.type <= TypedValue.TYPE_LAST_COLOR_INT) {
// windowBackground is a color
int color = a.data;
} else {
// windowBackground is not a color, probably a drawable
Drawable c = getResources().getDrawable(a.resourceId);
}
複製代碼
來源搜索的 stackoverflow.
2.Dialog 支持嗎?
這個方案默認就已經支持了 Dialog 黑白化,爲何?本身擼一下 Dialog 相關源碼,看看 Dialog 內部的 View 結構是什麼樣子的。
3. 若是 android.R.id.content 不是 FrameLayout 咋辦?
確實有這個可能。
想必你也有辦法把PhoneWindow 的內部 View 搞成這個樣子:
decorView
GrayFrameLayout
android.R.id.content
activity rootView
複製代碼
或者這個樣子:
decorView
android.R.id.content
GrayFrameLayout
activity rootView
複製代碼
能夠吧。
4. 目前已知的問題
原本我測試webview覺得是正常的,沒想到在部分app上,webview效果確實異常...
還要反饋視頻播放異常的。
暫時沒找到好的方案解決。
先收尾了。
建議你們跟着文章作些實驗,以學習知識爲目的;由於通常博客都搞的屌一點,以爲一鍵換掉整個app感受很帥。
實際上在真實項目中,即便肯定了技術方案,也是儘量的定製,追求影響越小越好,因此這類動不動更換全局View的作法,通常風險都很高,在實際項目開發中並不推薦。
本文僅爲app黑白化的方案探索,若是用於線上項目必定要充分測試,針對一些問題,後續有進展我再更新,公衆號是沒法修改了。
但願你能從中獲取到足夠的知識,拜了個拜!