App 黑白化實現探索,有一行代碼實現的方案嗎?

本文已受權公衆號 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

  1. 換膚;
  2. 展示 server 下發的圖片,還須要單獨作灰度處理;

這麼看起來工做量仍是很大的。markdown

後來我就在思考,既然 web 端能夠這麼給整個頁面加一個灰度的效果,咱們 app 應該也能夠呀?app

那咱們如何給app頁面加一個灰度效果呢?

咱們的 app 頁面正常狀況下,其實也是 Canvas 繪製出來的對吧?

Canvas 對應的相關 API 確定也是支持灰度的。

那麼是否是咱們在控件繪製的時候,好比 draw 以前設置個灰度效果就能夠呢?

好像發現了什麼玄機。

1. 嘗試給 ImageView 上個灰度效果

那麼咱們首先經過 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能夠嗎?

2. 嘗試觸類旁通

咱們來試試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嗎?

這聽起來成本也挺高呀。

再想一想還有更簡單的嗎?

3. 往上看一眼

雖然剛纔的佈局文件很簡單,可是邀請你再去看一眼剛纔的佈局文件,我要問你問題了:

看好了吧。

  1. 請問上面的 xml 中,ImageView的父 View 是誰?
  2. TextView 的父 View 是誰?
  3. Button 的父 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 相關邏輯。

有沒有那種不須要去修改什麼流程的方案?

4. LayoutInflater 中的細節

還真是有的。

咱們的 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?

...

5. 找個 App驗證下

說到如今,都沒有脫離出一個 Activity。

咱們找個複雜點的項目驗證下好吧。

我去 github 找個 wanandroid 的 Java 開源項目:

選中了:

github.com/jenly1314/W…

導入後,只要在 BaseActivity 裏面添加咱們剛纔的代碼就能夠了。

運行效果圖:

恩,沒錯,webview 裏面的文字,圖片都黑白化了。

沒發現啥問題,這樣一個 app 就徹底黑白化了。

等等,我發現狀態欄沒變,狀態欄是否是有 API,本身在 BaseActivity 裏面調用一行代碼處理哈。

號內回覆:「文章寫的真好」,獲取黑白化後的 apk,本身體驗。

6. 真的沒問題了嗎?

其實沒運行出來問題有些遺憾。

那我自爆幾個問題吧。

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黑白化的方案探索,若是用於線上項目必定要充分測試,針對一些問題,後續有進展我再更新,公衆號是沒法修改了。

但願你能從中獲取到足夠的知識,拜了個拜!

相關文章
相關標籤/搜索